Granting Access with Webhooks
Your backend grants access when it receives the webhook — never on the strength of a URL parameter. This page lists the events that matter for a mobile entitlement system. The full catalog lives in Overview & event types.
Which events to listen to
One-time payment
| Event | Action |
|---|---|
payment.authorized | Grant access |
payment.failed / payment.canceled | Clear the "pending" state |
payment.partially_refunded / payment.fully_refunded | Revoke |
dispute.opened / dispute.lost | Revoke |
Subscription
| Event | Action |
|---|---|
subscription.active | Grant access |
subscription.trial_started | Grant access (see warning below) |
subscription.past_due | Up to you (grace period) |
subscription.pending_cancellation | Do nothing — access runs until cancelAt |
subscription.canceled | Revoke, honouring cancelAt |
See Subscription Statuses for the underlying state machine.
Session lifecycle
Handy, since these events are indexed on the sessionId you stored when creating the session.
| Event | Content |
|---|---|
checkout_session.completed | The session is finalized, paymentId available |
checkout_session.failed / checkout_session.canceled | Session closed without payment |
Two traps
Never usepayment.settledto grant access.
payment.settledcorresponds to the internalPAYMENT_SUCCESSstatus — the funds have landed in the merchant's account. That can take several days. The signal of a successful payment on the buyer's side ispayment.authorized(statusCHECKOUT_SUCCESS). See Understanding Payment Statuses.
The free trial path and the waitlist path emit nopayment.*event at all.There is no
Paymentin the database: the session is anchored on a payment-method setup (customerPaymentMethodRequestId) instead. If you only listen topayment.*, you will never see your free trials go by. Listen tosubscription.trial_started— and, if you want the card-collection signal itself, topayment_setup.completed. See Trial Periods, Waitlist, and Free Trial & Card Setup.
Delivery format
Each request contains a data array, potentially batched:
{
"data": [
{
"eventType": "payment.authorized",
"payload": {
"id": "pay_GzOkEaYOOFHHRKYQcMVaj",
"status": "CHECKOUT_SUCCESS",
"metadatas": { "appUserId": "usr_123", "platform": "ios" }
}
}
]
}Verify the Svix signature on the raw body, before any deserialization — see Verifying signatures. Respond 2xx in under 15 seconds and process asynchronously, as described in Webhook Best Practices.
Idempotency and ordering
Webhooks can be replayed and arrive out of order. Two rules:
- Make your processing idempotent. Granting the same access twice must be a no-op. Use the
svix-idheader as the delivery key. - Never infer state from a sequence of events. If you receive
subscription.canceledthensubscription.active, do not conclude that the subscription is active — reconcile through the API, as described in Refreshing Entitlements.
Example handler
app.post('/webhooks/inflow', async (req, res) => {
const wh = new Webhook(process.env.INFLOW_WEBHOOK_SECRET);
let body;
try {
body = wh.verify(req.rawBody, req.headers); // RAW body
} catch {
return res.status(400).end();
}
res.status(200).end(); // acknowledge first
await queue.push(body.data); // process afterwards
});
async function handle(event) {
const appUserId = event.payload.metadatas?.appUserId;
if (!appUserId) return;
switch (event.eventType) {
case 'payment.authorized':
case 'subscription.active':
case 'subscription.trial_started':
await grantAccess(appUserId, event.payload); // idempotent
break;
case 'payment.fully_refunded':
case 'dispute.lost':
await revokeAccess(appUserId);
break;
case 'subscription.canceled':
await scheduleRevocation(appUserId, event.payload.cancelAt);
break;
case 'subscription.pending_cancellation':
break; // access continues until the end of the period
}
}Marketplaces (Connect) receive the
connect.*mirrors of these event types. Route ondata[].eventType— see Overview & event types.
Next steps
- Returning to the App — what happens on the buyer's side while your backend processes the event.
- Refreshing Entitlements — the fallback when a webhook is late or never arrives.
- Managing webhooks — list deliveries, replay an event, retrieve the secret.
Updated 1 day ago