The Customer State API provides a complete overview of a customer’s status, including active subscriptions, granted benefits, and usage meters. This is the ideal endpoint for entitlement checks and access control.
Overview
The Customer State endpoint returns comprehensive information about a customer:
Active Subscriptions : All current subscriptions with billing details
Granted Benefits : Active benefit grants and their properties
Active Meters : Current consumption and credits for usage-based features
This endpoint is optimized for frequent access control checks. Use it to determine what features a customer can access in your application.
Endpoints
There are two ways to retrieve customer state:
By Customer ID GET /v1/customers/{id}/state
Use when you have Polar’s internal customer ID.
By External ID GET /v1/customers/external/{external_id}/state
Use when tracking customers by your own external ID.
Basic Usage
curl (by Customer ID)
curl (by External ID)
TypeScript
Python
PHP
Go
curl https://api.polar.sh/v1/customers/cus_01h2s3f4g5h6j7k8m9n0p1q2/state \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response Structure
The customer state response includes all customer information plus additional state data:
{
"id" : "cus_01h2s3f4g5h6j7k8m9n0p1q2" ,
"email" : "[email protected] " ,
"name" : "Jane Doe" ,
"external_id" : "user_12345" ,
"created_at" : "2025-01-03T13:37:00Z" ,
"modified_at" : "2025-02-15T10:20:00Z" ,
"metadata" : {
"internal_user_id" : "usr_abc123" ,
"signup_source" : "organic"
},
"active_subscriptions" : [
{
"id" : "sub_01h2s3f4g5h6j7k8m9n0p1q2" ,
"status" : "active" ,
"amount" : 2900 ,
"currency" : "usd" ,
"recurring_interval" : "month" ,
"current_period_start" : "2025-02-03T13:37:00Z" ,
"current_period_end" : "2025-03-03T13:37:00Z" ,
"cancel_at_period_end" : false ,
"started_at" : "2025-01-03T13:37:00Z" ,
"product_id" : "prod_01h2s3f4g5h6j7k8m9n0p1q2" ,
"meters" : [
{
"meter_id" : "mtr_01h2s3f4g5h6j7k8m9n0p1q2" ,
"consumed_units" : 2500.0 ,
"spending" : 750 ,
"currency" : "usd"
}
]
}
],
"granted_benefits" : [
{
"id" : "bg_01h2s3f4g5h6j7k8m9n0p1q2" ,
"granted_at" : "2025-01-03T13:37:00Z" ,
"benefit_id" : "ben_01h2s3f4g5h6j7k8m9n0p1q2" ,
"benefit_type" : "custom" ,
"benefit_metadata" : {
"feature" : "premium_api_access"
},
"properties" : {
"note" : "Premium API access granted"
}
}
],
"active_meters" : [
{
"meter_id" : "mtr_01h2s3f4g5h6j7k8m9n0p1q2" ,
"consumed_units" : 2500.0 ,
"credited_units" : 5000 ,
"balance" : 2500.0
}
]
}
Common Use Cases
Access Control
Check if a customer has access to a specific feature:
async function hasFeatureAccess (
customerId : string ,
featureName : string
) : Promise < boolean > {
const state = await polar . customers . getState ({ id: customerId });
// Check if any granted benefit includes this feature
return state . grantedBenefits . some (
( benefit ) => benefit . benefitMetadata ?. feature === featureName
);
}
// Usage
const hasAccess = await hasFeatureAccess (
"cus_01h2s3f4g5h6j7k8m9n0p1q2" ,
"premium_api_access"
);
if ( hasAccess ) {
// Allow access to premium API
}
Subscription Status Check
Verify if a customer has an active subscription:
function hasActiveSubscription ( customerId : string ) : Promise < boolean > {
const state = await polar . customers . getState ({ id: customerId });
return state . activeSubscriptions . length > 0 ;
}
// Check for specific product
function hasSubscriptionToProduct (
customerId : string ,
productId : string
) : Promise < boolean > {
const state = await polar . customers . getState ({ id: customerId });
return state . activeSubscriptions . some (
( sub ) => sub . productId === productId
);
}
Usage-Based Billing Check
Check remaining credits for usage-based features:
async function getRemainingCredits (
customerId : string ,
meterId : string
) : Promise < number > {
const state = await polar . customers . getState ({ id: customerId });
const meter = state . activeMeters . find (
( m ) => m . meterId === meterId
);
return meter ?. balance ?? 0 ;
}
// Usage
const remaining = await getRemainingCredits (
"cus_01h2s3f4g5h6j7k8m9n0p1q2" ,
"mtr_api_calls"
);
if ( remaining > 0 ) {
// Allow API call
} else {
// Show upgrade prompt
}
Subscription Details Display
Show subscription information to the customer:
function formatSubscriptionInfo ( customerId : string ) {
const state = await polar . customers . getState ({ id: customerId });
if ( state . activeSubscriptions . length === 0 ) {
return "No active subscription" ;
}
const sub = state . activeSubscriptions [ 0 ];
const amount = ( sub . amount / 100 ). toFixed ( 2 );
const renewDate = new Date ( sub . currentPeriodEnd ). toLocaleDateString ();
return {
plan: sub . productId ,
amount: `$ ${ amount } ${ sub . currency . toUpperCase () } ` ,
interval: sub . recurringInterval ,
renewDate ,
willCancel: sub . cancelAtPeriodEnd ,
};
}
Caching Strategies
The customer state can change when:
Subscriptions are created, updated, or canceled
Benefits are granted or revoked
Usage credits are consumed or credited
Short-lived Cache (Recommended)
Cache customer state for 1-5 minutes to balance freshness and performance: import { createCache } from 'cache-manager' ;
const stateCache = createCache ({ ttl: 300 }); // 5 minutes
async function getCachedCustomerState ( customerId : string ) {
const cacheKey = `customer_state: ${ customerId } ` ;
let state = await stateCache . get ( cacheKey );
if ( ! state ) {
state = await polar . customers . getState ({ id: customerId });
await stateCache . set ( cacheKey , state );
}
return state ;
}
Invalidate cache when receiving relevant webhooks: // Webhook handler
app . post ( '/webhooks/polar' , async ( req , res ) => {
const event = req . body ;
// Invalidate cache on subscription/benefit changes
if ([
'subscription.created' ,
'subscription.updated' ,
'subscription.canceled' ,
'benefit_grant.created' ,
'benefit_grant.revoked' ,
]. includes ( event . type )) {
const customerId = event . data . customer_id ;
await stateCache . del ( `customer_state: ${ customerId } ` );
}
res . sendStatus ( 200 );
});
For web applications, cache within a single request: // Middleware to attach state to request
app . use ( async ( req , res , next ) => {
if ( req . user ?. customerId ) {
req . customerState = await polar . customers . getState ({
id: req . user . customerId ,
});
}
next ();
});
Don’t cache customer state indefinitely. Always set a reasonable TTL or use webhook-driven invalidation to ensure entitlements are up to date.
Best Practices
Use External IDs Store your internal user ID as the customer’s external_id for easier lookups without managing Polar IDs.
Check Specific Benefits Query for specific benefit types or metadata rather than checking all benefits on every request.
Handle Missing Customers Gracefully handle 404 responses when a customer doesn’t exist in Polar yet.
Cache Appropriately Balance freshness and performance with a 1-5 minute cache TTL for customer state.
Error Handling
Handle common error scenarios:
try {
const state = await polar . customers . getState ({
id: customerId ,
});
// Process state
} catch ( error ) {
if ( error . statusCode === 404 ) {
// Customer doesn't exist - create one or deny access
console . log ( 'Customer not found' );
} else if ( error . statusCode === 401 ) {
// Invalid or expired token
console . error ( 'Authentication failed' );
} else if ( error . statusCode === 429 ) {
// Rate limit exceeded - implement retry with backoff
console . error ( 'Rate limit exceeded' );
} else {
// Other errors
console . error ( 'Failed to fetch customer state:' , error );
}
}
The customer state endpoint is optimized for fast access:
Typical response time: 50-200ms
Includes denormalized data to avoid multiple queries
Uses Redis caching internally for frequently accessed customers
Minimize API calls by:
Implementing client-side caching
Using webhooks for state changes
Batching entitlement checks when possible
Loading state once per session rather than per request
For checking multiple customers, make concurrent requests: const states = await Promise . all (
customerIds . map ( id => polar . customers . getState ({ id }))
);
Integration Patterns
Middleware Pattern
Create middleware to inject customer state into requests:
import { Request , Response , NextFunction } from 'express' ;
async function customerStateMiddleware (
req : Request ,
res : Response ,
next : NextFunction
) {
if ( ! req . user ?. customerId ) {
return next ();
}
try {
req . customerState = await polar . customers . getState ({
id: req . user . customerId ,
});
} catch ( error ) {
// Log error but don't block request
console . error ( 'Failed to load customer state:' , error );
}
next ();
}
app . use ( customerStateMiddleware );
Feature Flag Pattern
Use customer state as feature flags:
class FeatureFlags {
constructor ( private customerState : CustomerState ) {}
hasFeature ( featureName : string ) : boolean {
return this . customerState . grantedBenefits . some (
( benefit ) => benefit . benefitMetadata ?. feature === featureName
);
}
getPlanTier () : string {
const sub = this . customerState . activeSubscriptions [ 0 ];
return sub ?. productId ?? 'free' ;
}
getRemainingQuota ( meterId : string ) : number {
const meter = this . customerState . activeMeters . find (
( m ) => m . meterId === meterId
);
return meter ?. balance ?? 0 ;
}
}
Next Steps
Webhooks Set up webhooks to receive customer state change notifications
Customer API Explore the complete Customer API reference
Benefits Learn about benefit types and management
Usage-Based Billing Implement usage-based billing with meters