Skip to main content
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 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
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);
  }
}

Performance Considerations

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

Build docs developers (and LLMs) love