Gate access with entitlements

An entitlement is a boolean access right: a key, a status, and a soft link to whatever granted it. It answers the only question your feature-gate cares about — does this user have access to X? Credits answer "how much have they used" (metering); entitlements answer "can they at all".

1. Decide the key

The entitlement key is derived from the Product's metadata at grant time, in this priority (identical for one-time and subscription grants):

  1. metadata.entitlements — a string array → grants one entitlement per key,
  2. else metadata.entitlementKey — a single string,
  3. else the product.code as a fallback.

So a "Pro" product with code: "pro" and no metadata grants the key pro. A bundle can grant several: metadata.entitlements = ["pro", "api_access", "priority_support"].

Selling per item (one price for any chapter, episode, report)

When every unit costs the same, make one product and let the key name the unit:

{ "entitlementTemplate": "mysite:chapter:{book}:{chapter}",
  "itemParams": { "book": "^[a-z0-9-]{1,60}$", "chapter": "^[A-Za-z0-9_-]{1,40}$" } }

Your site names the unit when it opens the checkout, with its service key:

POST /v1/checkout/sessions
x-api-key: <service key>
{ "priceCode": "mysite-chapter-usd", "mode": "PAYMENT", "userId": "did:…",
  "metadata": { "item": { "book": "celestial-code", "chapter": "ch-3" } } }
  • Billing checks item against itemParams (whole values only, nothing extra) and answers 400 otherwise.
  • Only the site can name the item: a checkout the buyer opens with their own token is refused for such a product, so nobody can buy one chapter and get another.
  • On payment the buyer gets exactly one key, mysite:chapter:celestial-code:ch-3. A refund takes it back like any order's entitlement.
  • Everything else is as for any order: card, PIX, crypto, x402Url for agents, paying from the INITE wallet.

2. It's granted automatically on payment

You don't call a "grant" endpoint. When an order or subscription reaches paid, the orchestrator grants the entitlement(s) and emits billing.entitlement.granted. The value JSON records the source:

{ "user_id": "user-123", "key": "pro", "source": "subscription",
  "subscription_id": "sub1", "expires_at": "2026-08-01T00:00:00.000Z" }

source is subscription (recurring; carries subscription_id and expires_at), order (one-time; carries order_id, never expires), or admin.

3. Check access from your module

Read the user's active entitlements and match on key. The endpoint returns only active, non-expired rows, so presence of the key is the whole check:

curl https://billing.inite.ai/v1/entitlements/user-123 -H "x-api-key: <service.apiKey>"
[{ "key": "pro", "status": "active", "source": "subscription",
   "value": { "subscription_id": "sub1", "product_code": "pro" },
   "expiresAt": "2026-08-01T00:00:00Z" }]
const ents = await billing.get(`/v1/entitlements/${userId}`) // service key
const hasPro = ents.some((e) => e.key === 'pro')
if (!hasPro) return deny()

End users check their own with GET /v1/entitlements/me (key from their JWT — never trust a userId from the client).

4. Prefer events over polling

Rather than calling the API on every request, cache access in your module and keep it fresh from the webhook stream:

EventDo
billing.entitlement.grantedmark the user as having key (until expires_at, if present)
billing.entitlement.revokedremove access to key
billing.subscription.ended / .cancelledthe related entitlements are revoked — expect a revoked event too

5. Revocation is automatic

Entitlements are revoked for you on refund (the order-scoped ones) and on subscription end/expiry (the subscription-scoped ones, swept hourly at period end past grace). Each revocation emits billing.entitlement.revoked. You don't revoke manually — you react to the event.

Next