Amba

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_billing integration 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:

amba_entitlements_define({ project_id: "proj_xxx", key: "pro" })

amba_products_create({
  project_id: "proj_xxx",
  product_id: "pro_monthly",
  grants_entitlement_id: "pro",
  store_product_refs: {
    app_store: "com.yourapp.pro.monthly",
    play_store: "pro_monthly",
    web: "price_1AbCdEfGhIjKlMnO"
  }
})

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:

https://api.amba.dev/webhooks/stripe-billing?project_id=proj_xxx

Subscribe it to these events:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.paid
  • invoice.payment_failed

Then store the endpoint's signing secret (whsec_…) on the integration:

amba_integrations_configure({
  project_id: "proj_xxx",
  provider: "stripe_billing",
  config: {
    webhook_secret: "whsec_…",
    secret_api_key: "sk_live_…"   // optional — enables amba_integrations_test
  }
})

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:

// your server — Stripe Checkout
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: 'price_1AbCdEfGhIjKlMnO', quantity: 1 }],
  subscription_data: {
    metadata: {
      amba_user_id: ambaUserId, // Amba.users.me().id, passed to your server
    },
  },
  success_url: 'https://yourapp.com/success',
  cancel_url: 'https://yourapp.com/cancel',
});

The metadata contract:

KeyMatches
amba_user_idthe Amba user id (Amba.users.me().id)
amba_external_idyour 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 eventEffect
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/unpaidRevokes (is_active = false)
invoice.payment_failed / status past_dueBilling 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.

On this page