Web Subscriptions
Sell subscriptions on the web through your own Stripe Billing account — web purchases grant the same Amba entitlements as mobile ones.
Your mobile subscriptions flow through RevenueCat; for web checkout,
RevenueCat itself documents Stripe Billing as the supported path — and Amba
wires that path straight into the same entitlement contract. Connect your own
Stripe Billing account, map your Stripe prices to Amba products, and a web
purchase grants exactly the entitlements a mobile purchase would: same
user_entitlements rows, same has() check, same auto-reward cascade, same
segments and webhooks.
One entitlement contract, every store:
- App Store / Play Store — validated by RevenueCat, delivered via the RevenueCat integration.
- Web — billed by your Stripe account, delivered via the
stripe_billingintegration on this page. - Anything else (gift codes, comps, migrations) — the direct grant endpoint.
The client never knows or cares which leg granted access:
Amba.entitlements.has('pro') is true either way.
Setup
1. Declare the product and entitlement
Define the entitlement your app gates on and the product that unlocks it, with
the Stripe price id (price_…) as the product's web store reference:
The same Amba product resolves on whichever store the purchase came from —
that's the point. (A Stripe product id prod_… works in the web slot
too; the price id is more precise when one product has several prices.)
2. Connect the integration
In your Stripe dashboard, create a webhook endpoint (Developers → Webhooks) pointing at:
Subscribe it to these events:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failed
Then store the endpoint's signing secret (whsec_…) on the integration:
The response echoes the exact webhook_url to register. Every delivery is
verified against the signing secret (signature + timestamp) before anything is
processed. secret_api_key is optional and only used for the connection test
— Amba never charges, refunds, or mutates anything in your Stripe account.
3. Attach the user at checkout
A web purchase must land on the right app user. Set the Amba user id in the subscription's metadata when you create the Checkout Session — Stripe then carries it on every lifecycle event for that subscription:
The metadata contract:
| Key | Matches |
|---|---|
amba_user_id | the Amba user id (Amba.users.me().id) |
amba_external_id | your own stable id (app_users.external_id, set at login) |
Set one of the two. When metadata is absent on a later event, Amba still
resolves a known subscriber by their billing customer id (recorded on the
first matched event), and falls back to external_id = <customer id> for apps
that store it there. An event that resolves to no user is acknowledged but not
applied — re-send it from the Stripe dashboard after fixing the metadata.
How events map
| Stripe event | Effect |
|---|---|
invoice.paid (subscription create) | Grants the mapped entitlements, fires the auto-reward cascade once |
invoice.paid (renewal cycle) | Refreshes the rows, extends expiry, fires renewal rewards once |
customer.subscription.created / updated (active) | Grants/refreshes access immediately — no duplicate reward |
customer.subscription.updated (cancel at period end) | Auto-renew off — access kept until period end, exactly like mobile |
customer.subscription.deleted / status canceled/unpaid | Revokes (is_active = false) |
invoice.payment_failed / status past_due | Billing issue — access kept while the payment retries; revoke follows the status change |
Ingest is idempotent and order-safe, exactly like the mobile leg: events are deduplicated by their immutable id, an older event can never overwrite newer entitlement state, and the reward cascade fires at most once per billing period — a replayed webhook can't double-pay.
Entitlement resolution always follows your product → entitlement map (the one from step 1), so web and mobile purchases of the same product unlock the same set — and granting a new entitlement to a product later applies to both stores at once.
Reading entitlements
Nothing changes. GET /client/entitlements (the SDK's has() /
list()) returns web-granted rows with store: "web" and the same
expiry-aware is_active derivation as every other row. Canonical
entitlement.granted / subscription.started / subscription.renewed events
flow into segments, event-rule rewards, and your outbound webhooks the same
way mobile events do.
Restore on the web is automatic. There's no receipt on the device to restore from — the
subscription lives in your billing account and every lifecycle change lands server-side, so a user
who reinstalls or signs in on a new device reads their current entitlements immediately. The SDK's
restore() remains the mobile-store affordance.