Delivery format
Every delivery is an HTTPPOST 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:
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.createdwhen it is opened, theninvoice.payment_succeededorinvoice.payment_failedwhen it settles.billingReason(initialorrenewal) tells the two apart.
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.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 assubscription.customerId), present on every event for that subscriber includingcustomer.subscription.deleted. It identifies a subscription, not a person — one buyer with two subscriptions has two different ids. See Field reference below. Oninvoice.*events fired before the subscriber record exists (the very first charge) it isnull.clientReferenceId— echoes back theclientReferenceId(orexternalId) you supplied at checkout,nullwhen you didn’t.metadata— replays the checkout metadata you attached (via checkout-URL query params, the popup’smetadataoption, or a checkout session’smetadata), captured at activation. Subscriptions created before this field existed replay{}.
"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.
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.
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.
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 isHMAC-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.
X-Pocketsflow-Signature header using a constant-time
comparison, and reject the request if they don’t match.