customer.state_changed
Aggregate entitlement event — what can this customer access right now.
Payload
All webhook payloads follow a consistent top-level structure with event-specific data nested within the data object.
The customer ID. Returns your externalId if you provided one when creating the customer, otherwise returns the Commet publicId.
What caused the transition. One of: subscription_created, subscription_activated, subscription_canceled, plan_change, past_due, trial_started, trial_converted, trial_expired, cancellation_scheduled, cancellation_revoked, seats_updated, addon_activated, addon_deactivated, credits_depleted, balance_depleted, quota_exceeded.
The customer's current subscription status, or "none" when no live subscription exists. Grant access only when trialing or active.
The live subscription ID, or null when status is none.
The current plan (id and name), or null when status is none.
The current billing interval.
The plan's consumption model: metered, credits, or balance.
Current feature access, one entry per plan feature: code, name, type, allowed, enabled, current, included, remaining, overageQuantity, overageUnitPrice, unlimited, overageEnabled, billedQuantity. Fields that do not apply to a feature type are null.
Summary of seats-type features: code, current, included, remaining, unlimited.
For credits plans: planCredits, purchasedCredits, totalCredits. Null otherwise.
For balance plans: currentBalance in rate scale (10000 = $1.00). Null otherwise.
{
"event": "customer.state_changed",
"timestamp": "2026-03-25T14:32:00.000Z",
"organizationId": "org_abc123",
"mode": "live",
"apiVersion": "2026-05-25",
"data": {
"customerId": "user_123",
"trigger": "subscription_activated",
"status": "active",
"subscriptionId": "sub_1a2b3c4d",
"plan": {
"id": "plan_pro_monthly",
"name": "Pro"
},
"billingInterval": "monthly",
"consumptionModel": "metered",
"features": [
{
"code": "api_calls",
"name": "API Calls",
"type": "usage",
"allowed": true,
"enabled": null,
"current": 120,
"included": 1000,
"remaining": 880,
"overageQuantity": 0,
"overageUnitPrice": 50,
"unlimited": false,
"overageEnabled": true,
"billedQuantity": null
},
{
"code": "editors",
"name": "Editors",
"type": "seats",
"allowed": true,
"enabled": null,
"current": 3,
"included": 5,
"remaining": 2,
"overageQuantity": 0,
"overageUnitPrice": null,
"unlimited": false,
"overageEnabled": false,
"billedQuantity": null
}
],
"seats": [
{
"code": "editors",
"current": 3,
"included": 5,
"remaining": 2,
"unlimited": false
}
],
"credits": null,
"balance": null
}
}One event to sync access
Instead of handling every lifecycle event (subscription.activated, subscription.canceled, trial.expired, ...) to keep your access flags in sync, handle this single event. Every entitlement transition fires it with the customer's current state, computed at delivery time:
| trigger | When |
|---|---|
subscription_created | A subscription was created (status pending_payment — no access yet). |
subscription_activated | A payment confirmed the subscription. |
trial_started | A trial began. |
trial_converted | A trialing customer converted to paid via plan change. |
trial_expired | A trial ran out and regular billing began. |
plan_change | A plan change executed (immediate or scheduled). |
cancellation_scheduled | A cancellation was scheduled — access continues until period end. |
cancellation_revoked | A scheduled cancellation was reverted. |
subscription_canceled | The subscription terminated — status becomes none. |
past_due | A recurring payment failed — access is cut immediately. |
Handling the payload
Use status as the access gate (trialing and active grant access), features for per-feature limits, and credits/balance for consumption headroom on credits/balance plans. The payload reflects the state at delivery time — if two transitions happen back to back, the later event always carries the final state, so processing events in timestamp order converges to the correct result.
How is this guide?