Billing webhooks
An HTTP POST to your server the moment money moves — signed, and shaped so the subscription handler you already run keeps working.
Fictura can notify your backend the moment money moves — a purchase, a renewal, a cancellation — by sending an HTTP POST to a URL you choose. You almost certainly handle one already, just in the other direction: RevenueCat calls your endpoint when a store purchase lands. This is the same pattern with a different caller.
Events
type | data.event_name | Fires when |
|---|---|---|
billing.purchase | subscription_started | A user pays for a subscription with no trial. |
billing.trial_start | trial_started | A free trial begins. price is 0. |
billing.conversion | subscription_renewed | A trial becomes its first paid period. |
billing.renewal | subscription_renewed | A paid period renews. |
billing.cancellation | subscription_cancelled | Auto-renew is switched off. |
billing.expiration | subscription_expired | Access actually ends. |
billing.billing_issue | billing_issue | A payment failed; the card is being retried. |
billing.other | varies — e.g. subscription_uncancelled | Anything else — for example, auto-renew switched back on. |
Cancellation is not expiration
A billing.cancellation means the user turned auto-renew off — they keep access until expires_at. Only billing.expiration ends access. Handlers that treat a cancel as "no longer subscribed" lock out customers who paid through the end of the month.
The payload
{
"type": "billing.purchase",
"app_id": "6b8506b9-2f01-499b-a95a-e64ee67b2d3a",
"sent_at": "2026-08-10T18:22:41.000Z",
"data": {
"event_name": "subscription_started",
"app_user_id": "8f14e45f-ceea-467f-a9d2-5f9e4b1c2d3e",
"product_id": "price_1TmKgSDnJtk7pxNB…",
"price": 9.99,
"currency": "USD",
"store": "STRIPE",
"country": "US",
"expires_at": "2026-09-10T18:22:41.000Z",
"period_type": "NORMAL",
"environment": "PRODUCTION",
"master_user_id": "0f1e2d3c-4b5a-6978-8695-a4b3c2d1e0f9"
}
}Your own id for the user — the same app_user_id your RevenueCat webhook reports. Key your database writes on this.
The plan purchased — a Stripe price id for web purchases, a store product id otherwise.
Gross amount, in the currency's major unit. 0 for trials.
Where the money moved: STRIPE, RC_BILLING, APP_STORE, PLAY_STORE.
When access lapses. Write this to your table — it's how expiry stays enforceable.
NORMAL, TRIAL, or INTRO.
PRODUCTION or SANDBOX. Skip sandbox events in production handlers.
Fictura's internal cross-device id. Useful for support lookups; not your database key.
Verify the signature
If you set a signing secret when connecting the webhook, every delivery carries an X-Growth-Signature header: the HMAC-SHA256 of the raw request body, hex-encoded, keyed with your secret. Verify it before trusting the payload — without this check, anyone who learns your endpoint URL can mint fake purchases. Code for Node and Deno is on Signature verification.
Set it up
Dashboard → Setup → Connections → Outgoing Webhooks.
Paste your endpoint URL.
Set a signing secret.
Any long random string. Optional, strongly recommended — it's the only thing standing between your endpoint and spoofed purchases.
Connecting sends a signed test delivery.
A "type": "test" payload hits the URL; answer with a 2xx and you're live.
Delivery semantics
- One attempt, eight-second timeout. There is no automatic retry today. Make your handler fast: accept, respond 200, process after.
- Make writes idempotent. Upsert on your user id rather than inserting, so a duplicated event can never double-write.
- Webhooks notify; they don't decide. Access should be derivable from your table's
expires_at— a handler that misses one delivery must degrade to "expires on time", never to "premium forever". - Or skip the table entirely and ask
GET /api/v1/entitlements/:user_id— both answers derive from the same events.
These are Fictura → you
This page covers deliveries from Fictura to your server. The other direction — Stripe telling Fictura a payment landed — needs no setup at all: saving the Web Checkout card registers our endpoint on your Stripe account automatically.