Fictura
Web checkout

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

typedata.event_nameFires when
billing.purchasesubscription_startedA user pays for a subscription with no trial.
billing.trial_starttrial_startedA free trial begins. price is 0.
billing.conversionsubscription_renewedA trial becomes its first paid period.
billing.renewalsubscription_renewedA paid period renews.
billing.cancellationsubscription_cancelledAuto-renew is switched off.
billing.expirationsubscription_expiredAccess actually ends.
billing.billing_issuebilling_issueA payment failed; the card is being retried.
billing.othervaries — e.g. subscription_uncancelledAnything 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

POST — application/json
{
  "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"
  }
}
app_user_id
string · nullable

Your own id for the user — the same app_user_id your RevenueCat webhook reports. Key your database writes on this.

product_id
string · nullable

The plan purchased — a Stripe price id for web purchases, a store product id otherwise.

price
number · nullable

Gross amount, in the currency's major unit. 0 for trials.

store
string · nullable

Where the money moved: STRIPE, RC_BILLING, APP_STORE, PLAY_STORE.

expires_at
ISO 8601 · nullable

When access lapses. Write this to your table — it's how expiry stays enforceable.

period_type
string · nullable

NORMAL, TRIAL, or INTRO.

environment
string · nullable

PRODUCTION or SANDBOX. Skip sandbox events in production handlers.

master_user_id
uuid

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

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.

On this page