Skip to main content
BillingEntitlements

Entitlements.

An entitlement answers one question: is this customer allowed to use this feature right now? You define a feature with a string key (for example pro or api_access), and check it with a single call. The answer comes from two independent sources: an active subscription on a price that carries the same key, or a manual grant you wrote directly. You never have to map plans to features yourself in your own code.

The two sources

A check returns active when either source says so:

  • subscriptionthe customer has an active or trialing subscription on a price whose entitlementKey matches the key. This is computed live from the subscription state — there is no row to keep in sync. When the subscription ends, the entitlement stops being active on its own.
  • manualyou granted the key directly with a grant call. Manual grants are independent of any subscription — useful for comps, lifetime access, support overrides, or access bought outside a recurring plan.
Identify the customer however you like. Every entitlement call takes a key plus exactly one customer reference: customerId (OpenSettle's id) or appUserId (your own user id). Set appUserIdon a customer at create or update so you can check entitlements with the same id your app already uses, without storing OpenSettle's.

Tag a price with a key

To make a subscription grant access, set entitlementKey on the price when you create it. Any active or trialing subscription on that price then grants the matching key — no further wiring.

price.ts
const price = await os.products.createPrice("prod_...", {
  amount: 2900,            // $29.00 / cycle, minor units
  currency: "USD",
  interval: "month",
  entitlementKey: "pro",   // subscriptions on this price grant "pro"
});

price.entitlementKey;      // "pro"

Set an app user id on a customer

Pass appUserId when you create or update a customer. It is readable back on the customer object, and any entitlement call accepts it in place of customerId.

customer.ts
const customer = await os.customers.create({
  email: "ada@example.com",
  appUserId: "user_42",   // your own user id
});

customer.appUserId;       // "user_42"

Check access

Check on every gated request. It is available to any workspace member, takes no idempotency key, and returns the resolved entitlement. When active, source tells you why — a subscription (with its subscriptionId) or a manual grant; when inactive, source is null.

check.ts
const entitlement = await os.entitlements.check({
  key: "pro",
  appUserId: "user_42",   // or: customerId: "cus_..."
});

if (!entitlement.active) {
  // gate the feature
}

entitlement.active;          // true
entitlement.key;             // "pro"
entitlement.source;          // "subscription" | "manual" | null
entitlement.subscriptionId;  // set when source is "subscription"
entitlement.expiresAt;       // ISO string or null
check.sh
curl -X POST https://api.opensettle.io/v1/workspaces/$WORKSPACE_ID/entitlements/check \
  -H "Authorization: Bearer $OPENSETTLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "pro", "appUserId": "user_42" }'

Grant manually

A manual grant gives a key to a customer independent of any subscription. It requires the developer role and takes an Idempotency-Key header so a retried grant is safe. Pass an optional expiresAt for a time-boxed grant (omit it for one that does not expire) and optional metadata. It returns 201 with the created entitlement.

grant.ts
const entitlement = await os.entitlements.grant({
  key: "pro",
  appUserId: "user_42",                 // or: customerId: "cus_..."
  expiresAt: "2027-01-01T00:00:00Z",    // optional; omit for no expiry
  metadata: { reason: "lifetime_comp" },// optional
});

entitlement.id;          // "ent_..."
entitlement.status;      // "active"
entitlement.source;      // "manual"
grant.sh
curl -X POST https://api.opensettle.io/v1/workspaces/$WORKSPACE_ID/entitlements/grant \
  -H "Authorization: Bearer $OPENSETTLE_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "key": "pro", "appUserId": "user_42" }'

Revoke a manual grant

Revoke removes a manual grant of a key from a customer. It requires the developer role, is idempotent (revoking an already-revoked or never-granted key is not an error), and returns the entitlement.

revoke.ts
await os.entitlements.revoke({
  key: "pro",
  appUserId: "user_42",   // or: customerId: "cus_..."
});
revoke.sh
curl -X POST https://api.opensettle.io/v1/workspaces/$WORKSPACE_ID/entitlements/revoke \
  -H "Authorization: Bearer $OPENSETTLE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "key": "pro", "appUserId": "user_42" }'
Revoke does not stop subscription access. Subscription-derived access is computed live from the subscription, so a revoke only clears a manual grant — it cannot cancel a subscription. If a customer still has an active subscription on a matching price, a check stays active after you revoke. To stop access that comes from a subscription, cancel the subscription.

List a customer's entitlements

List every entitlement for one customer — both subscription-derived and manual — keyed by customerId or appUserId.

list.ts
const entitlements = await os.entitlements.list({
  appUserId: "user_42",   // or: customerId: "cus_..."
});

for (const e of entitlements) {
  e.key;     // "pro"
  e.active;  // true
  e.source;  // "subscription" | "manual" | null
}
list.sh
curl -H "Authorization: Bearer $OPENSETTLE_KEY" \
  "https://api.opensettle.io/v1/workspaces/$WORKSPACE_ID/entitlements?appUserId=user_42"

Webhooks

Manual grant and revoke emit these events so you can sync access in your own system.

  • entitlement.granteda key was granted to a customer with a manual grant call.
  • entitlement.revokeda manual grant of a key was revoked from a customer.

These cover the manual grant lifecycle. Subscription-derived access has no separate entitlement event — it is computed live, so follow the subscription events (created, renewed, canceled) to know when that access starts and stops.