Entitlements
Track which users have access to which features. Amba is the entitlements layer — wire any subscription source (or none) into it.
An entitlement is Amba's abstraction for "does this user have access to a feature." It's a row in user_entitlements keyed by (app_user_id, entitlement_id) that your app reads at runtime to gate paid content.
Amba is your single entitlement source. Grant entitlements directly from your server (Stripe, Paddle, manual ops, gift codes, comp grants, anything), wire in an optional integration like RevenueCat or Superwall, or mix the two. Every entitlement ends up in the same table and is read through the same client API regardless of where it came from.
Read Amba — don't OR-merge with a purchase-provider SDK on the client. When you wire in a
purchase provider, its webhooks sync into user_entitlements automatically, so GET /client/entitlements (and the SDK's entitlements.has(name) / list()) already reflect every
purchase, renewal, cancellation, and expiry. Gate your paywall on Amba alone. Combining Amba's
answer with a separate provider-SDK check client-side is the common footgun: the two can disagree
mid-sync, double your network calls, and force you to reconcile two sources of truth. There is one
source of truth, and it's Amba.
Data model
user_entitlements:
| Column | Purpose |
|---|---|
app_user_id | The user who owns the entitlement |
entitlement_id | Logical identifier (e.g. "premium") |
product_id | Store product identifier |
is_active | Whether the entitlement is currently valid |
store | "app_store" / "play_store" / "web" (or null) |
period_type | "trial" / "intro" / "normal" / ... |
purchase_date | First purchase timestamp |
expiration_date | When the current period ends (NULL for lifetime) |
The columns are whitelisted for segment targeting — see Segment operators.
Grant entitlements from your server
This is the universal path — works for any source.
The handler upserts on (app_user_id, entitlement_id), so a second call for the same pair refreshes the row in place. To revoke, pass is_active: false. To clear a column (e.g. extend a finite subscription to lifetime), pass that field as null.
Use this directly from:
- Web checkout (Paddle or anything custom) — your backend receives the payment event, upserts the entitlement. If you bill the web through Stripe Billing, you don't need this endpoint at all — the built-in Web Subscriptions integration ingests the lifecycle events directly.
- Manual admin tooling — support comps a month, refund a chargeback, grant a gift code.
- Migrations from legacy systems — backfill entitlements once at import time.
- Your own server that already validates store receipts — if your backend receives App Store Server Notifications or Google Play RTDN and validates the receipts itself, post the resulting access decision to this endpoint. Amba does not receive or validate store receipts for you — there is no direct App Store / Play notification endpoint; your server makes the access decision and writes it here.
- Your own paywall / subscription service — anything that knows whether a user should have access can write to this endpoint.
See the full reference: POST /admin/projects/:projectId/users/:userId/entitlements.
Optional: sync from RevenueCat
If you use RevenueCat for mobile subscriptions, Amba accepts its webhook and maps the events into the same user_entitlements table. Skip this section if you don't use RevenueCat.
| Provider event | Effect |
|---|---|
INITIAL_PURCHASE | Upsert active entitlement row |
RENEWAL | Refresh the row, extend expiration_date |
CANCELLATION | Auto-renew turned off — access is kept until expiration_date, not revoked now |
EXPIRATION | is_active = false (the period actually lapsed) |
REFUND | is_active = false (access pulled immediately) |
Each event writes straight into user_entitlements, so the next GET /client/entitlements already reflects it — there's nothing to merge on the client.
Ingest is idempotent and order-safe: provider webhooks are delivered at-least-once and can arrive out of order, so Amba deduplicates each event and refuses to let an older event overwrite newer entitlement state. A redelivered or late event never silently revokes an active subscriber.
Reading is expiry-aware: GET /client/entitlements derives is_active as "marked active AND not past expiration_date", so a subscription whose period has lapsed never reads as active even before its expiry event lands.
Setup: RevenueCat integration.
Optional: sync paywall events
If you use a paywall provider such as Superwall, Amba records its paywall events (shown, dismissed, purchased, ...) as engagement events named paywall_<event> so streaks, XP rules, and segments can target paywall behavior alongside first-party events. Skip if you don't use a paywall provider.
Setup: Superwall integration.
Define products & offerings
Beyond raw entitlement grants, you can declare a product catalog Amba owns — so a purchase resolves to a canonical entitlement no matter which store it came from, and your paywall renders from one source of truth.
- Entitlement — the thing you gate on (
has("pro")). Define it once. - Product — something sellable. Carries
store_product_refsmapping each store to its own id ({ "app_store": "com.app.pro.yearly", "play_store": "pro_yearly", "web": "price_123" }) andgrants_entitlement_id— the entitlement it unlocks. - Offering — the named set of packages your paywall renders. Each package surfaces one product at a position.
Configure all of it agentically (MCP: amba_entitlements_define, amba_products_create, amba_entitlements_map_product, amba_offerings_create) or via the admin API under /admin/projects/:projectId/subscriptions/*.
Once products map to entitlements, any grant carrying a product_id — a store webhook OR a server-side grant — unlocks the mapped entitlement automatically. The mapping lives in Amba, so every purchase path — RevenueCat on mobile, Stripe Billing on the web, a server-side grant — resolves to the same entitlement set.
Read the offering from the app
The response is provider-neutral: render the packages, let the user buy through the platform store, then gate features on has().
Restore purchases
App Store review (guideline 3.1.1) requires a visible Restore Purchases action for any app selling subscriptions. Wire a button to:
restore() asks Amba's server to re-fetch the user's owned entitlements from the configured subscription service and re-apply them, then returns the active set. It is fail-closed: a transport failure rejects and leaves the user's access exactly as it was — never a silent unlock or revoke. A user with no prior purchases succeeds with restored_count: 0.
Reward bundles on entitlement grant
When a user gains an entitlement, Amba emits provider-neutral entitlement.granted and subscription.started / subscription.renewed engagement events. Bind a currency grant rule (or any event-driven rule) to those event names to automatically reward new subscribers — grant currency/XP, move them to a segment, fire a push — without writing any glue. The cascade is idempotent: a redelivered webhook or repeat grant fires the reward at most once per billing period.
Checking entitlements in the SDK
Both methods hit GET /client/entitlements:
UserEntitlement shape:
Example: gate a premium feature
Targeting by entitlement
Entitlement fields can drive segment rules, which in turn drive push campaigns and remote-config overrides:
See Segment operators for the full list of supported entitlement fields.
Local caching
The SDK does not cache entitlements client-side beyond the fetch lifetime. For performance-sensitive gates, fetch once on launch (after Amba.configure() resolves) and keep the result in React state / a context.
Do not persist entitlement state to local storage and trust it later — subscription state can change server-side (cancellations, billing issues). Always confirm against the server before granting access to paid content.
Routes reference
| Method | Path | Description |
|---|---|---|
GET | /client/entitlements | List the user's entitlements. ?active_only=true returns only currently-active (unexpired) grants. |
POST | /client/entitlements/restore | Restore Purchases — re-sync the user's owned entitlements from the subscription service. |
GET | /client/offerings | The offerings + packages your paywall renders. ?offering_id=… for one. |
POST | /admin/projects/:projectId/users/:userId/entitlements | Server-side grant / refresh. Universal path — works for any subscription source. |
POST/GET | /admin/projects/:projectId/subscriptions/* | Declare entitlements, products, the product→entitlement map, and offerings. |
POST | /webhooks/revenuecat | Optional. Accepts RevenueCat subscription events. |
POST | /webhooks/superwall | Optional. Accepts Superwall paywall events. |