> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pocketsflow.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook events

> Every event Pocketsflow can deliver to your webhook endpoints — when each fires, its payload, and how to verify the signature.

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:

| Header                    | Description                                                                                      |
| ------------------------- | ------------------------------------------------------------------------------------------------ |
| `X-Pocketsflow-Event`     | The event type (for example `order.completed`). Route on this.                                   |
| `X-Pocketsflow-Signature` | Hex HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret.              |
| `X-Pocketsflow-Timestamp` | Unix epoch milliseconds when the event was sent (informational — **not** part of the signature). |
| `Content-Type`            | Always `application/json`.                                                                       |

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](#verifying-the-signature) below and
[Authentication & security](/api-webhooks/authentication-and-security).

<Note>
  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`](/api-reference/introduction#payments)) in case a delivery is
  missed. Every attempt — success or failure — is recorded in your account's
  webhook delivery log.
</Note>

## 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.

| Event                                  | Category      | When it fires                                                                                                      |
| -------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------ |
| `order.completed`                      | Orders        | A payment succeeded and a one-time order is complete.                                                              |
| `order.refunded`                       | Orders        | An order was refunded (fully or partially).                                                                        |
| `customer.created`                     | Customers     | A new customer record was created — on every completed sale and when you create a customer via the API.            |
| `review.created`                       | Reviews       | A buyer left a product review.                                                                                     |
| `product.created`                      | Products      | A product was created.                                                                                             |
| `product.updated`                      | Products      | A product was modified.                                                                                            |
| `product.deleted`                      | Products      | A product was deleted.                                                                                             |
| `payment_intent.succeeded`             | Subscriptions | The **initial** subscription payment succeeded and the membership activated.                                       |
| `payment_intent.payment_failed`        | Subscriptions | A subscription charge failed. Delivered alongside `invoice.payment_failed`.                                        |
| `invoice.created`                      | Subscriptions | A subscription charge was opened, before it settles.                                                               |
| `invoice.upcoming`                     | Subscriptions | A renewal charge is due in 3 days — a heads-up before the card is hit.                                             |
| `invoice.payment_succeeded`            | Subscriptions | A subscription charge succeeded — first charge **or** renewal (`billingReason` says which).                        |
| `invoice.payment_failed`               | Subscriptions | A subscription payment failed (e.g. card declined) — the subscriber moves to `past_due`.                           |
| `customer.subscription.created`        | Subscriptions | A subscription activated. Fires **once**, on activation.                                                           |
| `customer.subscription.updated`        | Subscriptions | A subscription changed **after** activation (`past_due`, recovery, cancel-scheduled, pause, resume, cancellation). |
| `customer.subscription.pause`          | Subscriptions | Payment collection stopped. The buyer **keeps access**.                                                            |
| `customer.subscription.resumed`        | Subscriptions | A pause was lifted, or a scheduled cancel was reversed.                                                            |
| `customer.subscription.trial_will_end` | Subscriptions | A trial is about to end.                                                                                           |
| `customer.subscription.deleted`        | Subscriptions | A subscription/membership was cancelled or ended.                                                                  |

<Note>
  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.
</Note>

## Order events

### `order.completed`

Fires when a one-time product payment succeeds (all completed sales).
Delivered alongside [`customer.created`](#customer-created).

```json theme={null}
{
  "product": {
    "id": "6949918ee8d99b0e628103e0",
    "variantId": null
  },
  "order": {
    "id": "6a43ce05fae6c4f0cb5a2193",
    "paymentIntentId": "pay_Qjy4o3JnPfhHfW",
    "planId": "plan_MFtAtixcYzsMu"
  },
  "currency": "usd",
  "customer": {
    "email": "buyer@example.com",
    "firstName": "Jane",
    "lastName": "Buyer"
  },
  "tax": {
    "taxAmount": 0,
    "taxRate": 0,
    "ToCountry": "NL",
    "ToZip": "2573 NH",
    "ToState": "ZH",
    "percentage": 0,
    "vatNumber": null
  },
  "amount": 1000,
  "amountBeforeTax": 1000,
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6a43ca9f7e229e3568f9c9bb"
}
```

<Note>
  `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}`](/api-reference/introduction#orders). 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.
</Note>

### `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.

```json theme={null}
{
  "product": { "id": "6949918ee8d99b0e628103e0", "variantId": null },
  "order": { "id": "6a43ce05fae6c4f0cb5a2193" },
  "currency": "usd",
  "customer": { "email": "buyer@example.com" },
  "tax": {
    "amount": 0,
    "rate": 0,
    "ToCountry": "NL",
    "ToZip": "2573 NH",
    "ToState": "ZH",
    "percentage": 0,
    "vatNumber": null
  },
  "amount": 1000,
  "amountBeforeTax": 1000,
  "webhookId": "6a43ca9f7e229e3568f9c9bb"
}
```

## 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:

```json theme={null}
{
  "customer": {
    "id": "665f1c2a9b3e4a0012abaa11",
    "email": "buyer@example.com",
    "firstName": "Jane",
    "lastName": "Buyer",
    "country": "NL"
  },
  "product": { "id": "6949918ee8d99b0e628103e0", "variantId": null },
  "order": { "id": "6a43ce05fae6c4f0cb5a2193", "paymentIntentId": "pay_Qjy4o3JnPfhHfW" },
  "metadata": {},
  "webhookId": "6a43ca9f7e229e3568f9c9bb"
}
```

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:

```json theme={null}
{
  "_id": "665f1c2a9b3e4a0012abd0d0",
  "productId": "6949918ee8d99b0e628103e0",
  "buyerId": "665f1c2a9b3e4a0012abaa11",
  "buyerEmail": "buyer@example.com",
  "saleId": "6a43ce05fae6c4f0cb5a2193",
  "rating": 5,
  "review": "Fantastic — exactly what I needed.",
  "testMode": false,
  "webhookId": "6a43ca9f7e229e3568f9c9bb"
}
```

## Product events

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

```json theme={null}
{
  "_id": "6949918ee8d99b0e628103e0",
  "name": "Starter Kit",
  "price": 29,
  "description": "Everything to get going.",
  "published": true,
  "slug": "starter-kit",
  "userId": "auth0|68fd3a499605389e7305cdc1",
  "createdAt": "2026-05-01T12:00:00.000Z",
  "webhookId": "6a43ca9f7e229e3568f9c9bb"
}
```

## 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`](/api-reference/introduction#payments) (with
`billingReason` of `initial` or `renewal`).

<Note>
  **`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`.
</Note>

Almost every subscription event carries a `paymentMethod` — the card paying for
the subscription — in the same shape `order.completed` sends:

```json theme={null}
{
  "id": "pm_4Xn8Tz1",
  "type": "card",
  "brand": "visa",
  "last4": "4242",
  "expMonth": 11,
  "expYear": 2029
}
```

`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.

<Note>
  **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](#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" }`.
</Note>

### 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:

<ResponseField name="subscription.id" type="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.
</ResponseField>

<ResponseField name="subscription.customerId" type="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.
</ResponseField>

<ResponseField name="subscriptionCustomerId" type="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`.
</ResponseField>

<ResponseField name="subscriptionCustomer" type="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.
</ResponseField>

<ResponseField name="customer" type="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).
</ResponseField>

<ResponseField name="status" type="string">
  The subscription's current lifecycle status: `active`, `trialing`, `past_due`,
  `paused`, `canceled`, …
</ResponseField>

<ResponseField name="billingReason" type="string">
  On `invoice.*` events only: `initial` for the first charge that activates the
  membership, `renewal` for every recurring charge after it.
</ResponseField>

<ResponseField name="paymentMethod" type="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.
</ResponseField>

<ResponseField name="clientReferenceId" type="string | null">
  Echoes back the `clientReferenceId` / `externalId` you supplied at checkout, or
  `null` when you didn't.
</ResponseField>

<ResponseField name="metadata" type="object">
  Your checkout metadata, replayed verbatim (e.g. `{ "watch_id": "w_123" }`).
  `{}` when none was attached.
</ResponseField>

<ResponseField name="portalUrl" type="string">
  On `customer.subscription.created` / `.updated`: the buyer's self-service
  billing portal link — safe to email them.
</ResponseField>

<ResponseField name="webhookId" type="string">
  Identifies the endpoint this event was delivered to. Included in the signed
  body — verify the signature before parsing.
</ResponseField>

<Warning>
  **`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`/`resumed` → `deleted`), 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`.
</Warning>

### `payment_intent.succeeded` (initial subscription payment)

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

`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`.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "amount": 29.99,
  "currency": "usd",
  "paymentId": "pay_9Rt3Vc6",
  "billingReason": "renewal",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `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.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "active",
  "renewalAt": "2026-07-17T00:00:00.000Z",
  "daysUntilRenewal": 3,
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `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`.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "amount": 29.99,
  "currency": "usd",
  "paymentId": "pay_9Rt3Vc6",
  "billingReason": "renewal",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {
    "plan": "pro",
    "referrer": "newsletter"
  },
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

The initial charge — note `billingReason` and the `null` `customerId`:

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": null
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "amount": 29.99,
  "currency": "usd",
  "paymentId": "pay_1Ab2Cd3",
  "billingReason": "initial",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `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`](#payment_intent-payment_failed) and
[`customer.subscription.updated`](#customer-subscription-updated) are delivered
alongside it.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "amount": 29.99,
  "currency": "usd",
  "paymentId": "pay_8Zq5Hn2",
  "failureMessage": "Insufficient funds.",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `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.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "amount": 29.99,
  "currency": "usd",
  "paymentId": "pay_8Zq5Hn2",
  "failureMessage": "Insufficient funds.",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `customer.subscription.deleted` (cancellation)

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

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "canceledAt": "2026-07-14T09:31:04.512Z",
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `customer.subscription.created`

Fires **once**, when the subscription activates — never paired with
`customer.subscription.updated`. This is the "new subscriber" signal: grant
access here.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "subscriptionCustomer": {
    "id": "665f1a2b3c4d5e6f7a8b9c0d",
    "status": "active",
    "active": true,
    "cancelAtPeriodEnd": false,
    "paused": false
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "active",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "portalUrl": "https://app.pocketsflow.com/portal/sub_7Kq2mDx9/665f1a2b3c4d5e6f7a8b9c0d",
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

<Note>
  `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.
</Note>

### `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.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "subscriptionCustomer": {
    "id": "665f1a2b3c4d5e6f7a8b9c0d",
    "status": "active",
    "active": true,
    "cancelAtPeriodEnd": true,
    "paused": false
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "active",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "portalUrl": "https://app.pocketsflow.com/portal/sub_7Kq2mDx9/665f1a2b3c4d5e6f7a8b9c0d",
  "metadata": {},
  "cancelAtPeriodEnd": true,
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

On a failed payment the same event appends `failureMessage` instead of
`cancelAtPeriodEnd`:

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "subscriptionCustomer": {
    "id": "665f1a2b3c4d5e6f7a8b9c0d",
    "status": "past_due",
    "active": false,
    "cancelAtPeriodEnd": false,
    "paused": false
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "past_due",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "portalUrl": "https://app.pocketsflow.com/portal/sub_7Kq2mDx9/665f1a2b3c4d5e6f7a8b9c0d",
  "metadata": {},
  "failureMessage": "Your card was declined.",
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `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`.

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "paused",
  "pausedAt": "2026-07-14T09:31:04.512Z",
  "accessRetained": true,
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `customer.subscription.resumed`

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

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "active",
  "paymentMethod": {
    "id": "pm_4Xn8Tz1",
    "type": "card",
    "brand": "visa",
    "last4": "4242",
    "expMonth": 11,
    "expYear": 2029
  },
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

### `customer.subscription.trial_will_end`

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

```json theme={null}
{
  "subscription": {
    "id": "sub_7Kq2mDx9",
    "customerId": "665f1a2b3c4d5e6f7a8b9c0d"
  },
  "customer": {
    "email": "ada@example.com",
    "firstName": "Ada",
    "lastName": "Lovelace"
  },
  "status": "trialing",
  "trialEndsAt": "2026-07-21T00:00:00.000Z",
  "metadata": {},
  "webhookId": "6612aa04bb15cc26dd37ee48"
}
```

## 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.

<CodeGroup>
  ```javascript Node.js theme={null}
  import crypto from "crypto";

  function verify(rawBody, headerSignature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(rawBody) // Buffer or exact string as received
      .digest("hex");

    const a = Buffer.from(headerSignature, "utf8");
    const b = Buffer.from(expected, "utf8");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  // Express: capture the raw body for this route
  // app.post("/webhooks/pocketsflow",
  //   express.raw({ type: "application/json" }), handler);
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def verify(raw_body: bytes, header_signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode(), raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(header_signature, expected)
  ```

  ```php PHP theme={null}
  <?php
  function verify(string $rawBody, string $headerSignature, string $secret): bool {
      $expected = hash_hmac('sha256', $rawBody, $secret);
      return hash_equals($expected, $headerSignature);
  }
  ```
</CodeGroup>

Compare the result to the `X-Pocketsflow-Signature` header using a constant-time
comparison, and reject the request if they don't match.

## Related topics

* [Authentication & security](/api-webhooks/authentication-and-security)
* [Consuming webhooks](/api-webhooks/consuming-webhooks)
* [Webhook examples](/api-webhooks/examples)
* [API reference](/api-reference/introduction)
