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

EventAction
payment.authorizedGrant access
payment.failed / payment.canceledClear the "pending" state
payment.partially_refunded / payment.fully_refundedRevoke
dispute.opened / dispute.lostRevoke

Subscription

EventAction
subscription.activeGrant access
subscription.trial_startedGrant access (see warning below)
subscription.past_dueUp to you (grace period)
subscription.pending_cancellationDo nothing — access runs until cancelAt
subscription.canceledRevoke, 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.

EventContent
checkout_session.completedThe session is finalized, paymentId available
checkout_session.failed / checkout_session.canceledSession closed without payment

Two traps

🚨

Never use payment.settled to grant access.

payment.settled corresponds to the internal PAYMENT_SUCCESS status — 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 is payment.authorized (status CHECKOUT_SUCCESS). See Understanding Payment Statuses.

🚨

The free trial path and the waitlist path emit no payment.* event at all.

There is no Payment in the database: the session is anchored on a payment-method setup (customerPaymentMethodRequestId) instead. If you only listen to payment.*, you will never see your free trials go by. Listen to subscription.trial_started — and, if you want the card-collection signal itself, to payment_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:

  1. Make your processing idempotent. Granting the same access twice must be a no-op. Use the svix-id header as the delivery key.
  2. Never infer state from a sequence of events. If you receive subscription.canceled then subscription.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 on data[].eventType — see Overview & event types.

Next steps


Did this page help you?