Skip to main content
Webhooks notify your systems in real time when something happens in your account — a sale completes, a subscription renews, a payment fails, a refund is issued, and more. When you register an endpoint you choose exactly which of the events below to receive.

Delivery format

Every delivery is an HTTP POST with a JSON body and these headers: Every payload also carries a webhookId field identifying the endpoint the event was delivered to. Because the signature is computed over the exact bytes that include webhookId, always verify against the raw request body before parsing — see Verifying the signature below and Authentication & security.
Each event is delivered with a single attempt and a 5-second timeout. Acknowledge with a 2xx immediately and do the real work asynchronously, and reconcile periodically via the API (for example GET /payments) in case a delivery is missed. Every attempt — success or failure — is recorded in your account’s webhook delivery log.

Test vs live deliveries

A webhook endpoint is created in whichever mode your account is currently in. A test-mode endpoint only receives events generated by test-mode sales (pk_test_…); a live-mode endpoint only receives live events. Register the endpoint in the same mode as the traffic you want to observe.

Event catalog

You can subscribe to any of the following when you create or update a webhook.
Three additional events — identity.verification_session.verified, identity.verification_session.processing, and identity.verification_session.requires_input — exist in the subscribable event list for creator KYC / identity verification, but they are reserved for internal use and are not currently delivered to webhook endpoints. Do not build on them.

Order events

order.completed

Fires when a one-time product payment succeeds (all completed sales). Delivered alongside customer.created.
order.id is the Pocketsflow order/sale id; paymentIntentId is the underlying processor payment id (pay_…). variantId and vatNumber are null when not applicable. metadata echoes back any webhookMetadata you attached at checkout, and a top-level clientReferenceId echoes the clientReferenceId/externalId you supplied (or null). On Stripe-originated sales amount and amountBeforeTax are expressed in the smallest currency unit (cents); if you need guaranteed precision, look the order up via GET /orders/{id}. Free orders (a 100% discount or a free product) deliver a slightly different shape: amount is 0, order carries no paymentIntentId/planId, and the tax block uses amount/rate keys instead of taxAmount/taxRate — don’t assume every field above is always present.

order.refunded

Fires when a charge is refunded (fully or partially). It replays the same metadata (and top-level clientReferenceId) the original order.completed carried, so you can match the refund to your own record.

Customer & review events

customer.created

Fires on every completed sale (delivered next to order.completed) and whenever you create a customer via the API. On a sale, the payload identifies the new customer plus the related product and order:
When created directly via POST /customers, the payload is the full customer record (_id, userId, buyerEmail, country, productId, testMode, …) plus webhookId.

review.created

Fires when a buyer submits a product review. The payload is the full review record:

Product events

product.created, product.updated, and product.deleted fire from the product endpoints. Each payload is the full product document plus webhookId:

Subscription events

Pocketsflow distinguishes the initial subscription payment from renewals:
  • The first charge that activates a membership fans out payment_intent.succeeded.
  • Every charge — the first one and every renewal — fans out invoice.created when it is opened, then invoice.payment_succeeded or invoice.payment_failed when it settles. billingReason (initial or renewal) tells the two apart.
Both also record a subscription payment you can read back from GET /payments?type=subscription (with billingReason of initial or renewal).
customer.subscription.created fires only on activation — exactly once, and it is never paired with customer.subscription.updated. (It used to deliver both back-to-back, which made .updated useless as a “something changed” signal.) .updated now fires only on a state change after activation: past_due, recovery, cancel-scheduled, pause, resume, and cancellation. If you were keying “new subscriber” off .updated, switch to .created.
Almost every subscription event carries a paymentMethod — the card paying for the subscription — in the same shape order.completed sends:
id is the stable per-card identifier — key on it to recognise the same card across charges. Non-card payment methods send "paymentMethod": null. The two events with no charge in play — customer.subscription.deleted and customer.subscription.trial_will_end — omit the card.
Correlation fields. Every subscription event additionally carries three top-level fields for binding events to your own records:
  • subscriptionCustomerId — the stable id of this one subscription (the same value as subscription.customerId), present on every event for that subscriber including customer.subscription.deleted. It identifies a subscription, not a person — one buyer with two subscriptions has two different ids. See Field reference below. On invoice.* events fired before the subscriber record exists (the very first charge) it is null.
  • clientReferenceId — echoes back the clientReferenceId (or externalId) you supplied at checkout, null when you didn’t.
  • metadata — replays the checkout metadata you attached (via checkout-URL query params, the popup’s metadata option, or a checkout session’s metadata), captured at activation. Subscriptions created before this field existed replay {}.
The JSON samples below show "metadata": {} for brevity — with checkout metadata attached you get your own key/values back, e.g. "metadata": { "watch_id": "w_123" }.

Field reference

Most subscription events share the same core blocks. Here’s exactly what each field means so you know what to store and what to key on:
string
The subscription offer id — the recurring product the buyer signed up to. This is shared by every buyer on that offer; it is not unique to one subscriber.
string | null
The subscriber id — one specific buyer’s single subscription to the offer above (the id of the underlying SubscriptionCustomer record). Same value as the top-level subscriptionCustomerId. null only on the very first invoice.* charge, before the subscriber record has been created.
string | null
The top-level, flattened copy of subscription.customerId — provided so you don’t have to reach into the nested subscription block. Present on every event for that subscriber, including customer.subscription.deleted.
object
The subscriber’s current state — { id, status, active, cancelAtPeriodEnd, paused }. Sent on customer.subscription.created / .updated only. Read this to see what the subscription now looks like rather than inferring it from the event name.
object
The buyer — { email, firstName, lastName }. customer.email is the stable identity of the person; use it to recognise the same buyer across different subscriptions (see the warning below).
string
The subscription’s current lifecycle status: active, trialing, past_due, paused, canceled, …
string
On invoice.* events only: initial for the first charge that activates the membership, renewal for every recurring charge after it.
object | null
The card paying for the subscription — { id, type, brand, last4, expMonth, expYear } — or null when no card is on file or the method isn’t a card. id (payt_…) is the stable per-card identifier; key on it to recognise the same card across charges.
string | null
Echoes back the clientReferenceId / externalId you supplied at checkout, or null when you didn’t.
object
Your checkout metadata, replayed verbatim (e.g. { "watch_id": "w_123" }). {} when none was attached.
string
On customer.subscription.created / .updated: the buyer’s self-service billing portal link — safe to email them.
string
Identifies the endpoint this event was delivered to. Included in the signed body — verify the signature before parsing.
subscriptionCustomerId is per-subscription, not per-customer. It is the id of one buyer’s single subscription — stable for that subscription’s whole lifetime (created → renewals → pause/resumeddeleted), but not shared across subscriptions. The same person subscribing to two offers — or re-subscribing after cancelling — gets a different subscriptionCustomerId each time. To recognise the same person across subscriptions, key on customer.email, not subscriptionCustomerId.

payment_intent.succeeded (initial subscription payment)

subscription.id is the subscription offer id; subscription.customerId is the subscriber (SubscriptionCustomer) id.

invoice.created

Fires when a subscription charge is opened, before it settles — the terminal outcome follows as invoice.payment_succeeded or invoice.payment_failed. On the very first charge the subscriber record does not exist yet, so subscription.customerId is null.

invoice.upcoming

A heads-up 3 days before the renewal charge, so you can warn the buyer (or nudge them to fix an expiring card) before the card is hit. Only subscriptions that will actually be charged produce it — cancelled, cancel-at-period-end, and paused subscriptions do not.

invoice.payment_succeeded (first charge and renewals)

Fires on the first charge and every renewal — billingReason disambiguates. On the first charge subscription.customerId can be null.
The initial charge — note billingReason and the null customerId:

invoice.payment_failed

Fires when a subscription charge fails; the subscriber’s status becomes past_due. Use it to trigger dunning emails or pause access. payment_intent.payment_failed and customer.subscription.updated are delivered alongside it.

payment_intent.payment_failed

Delivered alongside invoice.payment_failed on every failed charge. Same information, plus the card that was declined. Subscribe to whichever of the two suits your integration — subscribing to both means two deliveries per failure.

customer.subscription.deleted (cancellation)

Fires when a membership is deactivated/cancelled. The subscriber’s status becomes canceled and active becomes false. Revoke access here.

customer.subscription.created

Fires once, when the subscription activates — never paired with customer.subscription.updated. This is the “new subscriber” signal: grant access here.
subscriptionCustomer is the current state of the subscriber: status, whether they’re active, whether a cancel is scheduled (cancelAtPeriodEnd), and whether payment collection is paused. portalUrl is the buyer’s self-service billing portal — safe to email them.

customer.subscription.updated

Same shape as created. Fires on a state change after activation: past_due, recovery, cancel-scheduled, pause, resume, and cancellation. It never fires on creation. Read subscriptionCustomer / status to see what the subscription now looks like, rather than inferring it from the event name.
On a failed payment the same event appends failureMessage instead of cancelAtPeriodEnd:

customer.subscription.pause

Fires when payment collection is stopped. The buyer keeps access — only the charging stops, until the subscription is resumed. That’s why accessRetained is true and the subscriber stays active. Do not revoke access on this event; revoke on customer.subscription.deleted.

customer.subscription.resumed

Fires when a pause is lifted (collection restarts) or a scheduled cancel is reversed — either way, the subscription will keep billing.

customer.subscription.trial_will_end

Fires when a trial is about to end and the first real charge is coming.

Verifying the signature

The signature is HMAC-SHA256(secret, rawBody), hex-encoded, where rawBody is the exact JSON body Pocketsflow sent (it already includes webhookId). Never re-serialize the parsed object before verifying — hash the raw bytes.
Compare the result to the X-Pocketsflow-Signature header using a constant-time comparison, and reject the request if they don’t match.