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

# Subscriptions API quickstart

> Sell and manage recurring subscriptions from your own code — checkout, subscriber roster, live status checks, lifecycle actions, and webhooks, with copy-paste examples in cURL, Node.js, Python, and PHP.

This page is the end-to-end integration path for subscriptions: create a
checkout for a subscription offer, list your subscribers, gate access on a
subscriber's live status, cancel/pause/resume from your backend, and react to
subscription webhooks. You can create offers through the REST API or in the
dashboard — see the [subscriptions guide](/selling/subscriptions). For the
full endpoint catalog, see the [API reference](/api-reference/introduction).

<Steps>
  <Step title="Sell">
    `POST /checkout/sessions` with your subscription offer's id, redirect the
    buyer to the returned `url`.
  </Step>

  <Step title="Gate">
    Check `GET /subscriptions/subscribers/{id}` (optionally `?live=true`)
    before serving paid content.
  </Step>

  <Step title="React">
    Verify and handle `customer.subscription.*` / `invoice.*` webhooks to keep
    your records in sync.
  </Step>
</Steps>

## 1. Authentication and base URL

Every request goes to `https://api.pocketsflow.com` with your API key in the
`Authorization` header — `pk_live_…` for live mode, `pk_test_…` for sandbox
data. Create keys in the dashboard under **Developers → API keys**, and keep
them server-side only. Details:
[Authentication & security](/api-webhooks/authentication-and-security).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.pocketsflow.com/users/me \
    -H "Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch("https://api.pocketsflow.com/users/me", {
    headers: { Authorization: `Bearer ${process.env.POCKETSFLOW_API_KEY}` },
  });
  const me = await res.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.get(
      "https://api.pocketsflow.com/users/me",
      headers={"Authorization": f"Bearer {os.environ['POCKETSFLOW_API_KEY']}"},
  )
  me = res.json()
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://api.pocketsflow.com/users/me');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer ' . getenv('POCKETSFLOW_API_KEY'),
      ],
  ]);
  $me = json_decode(curl_exec($ch), true);
  curl_close($ch);
  ```
</CodeGroup>

## Create a subscription offer

Use `POST /subscriptions` to create the recurring product before creating a
checkout session. The endpoint accepts JSON for API clients and
`multipart/form-data` when you also need to upload offer assets.

```bash cURL theme={null}
curl -X POST https://api.pocketsflow.com/subscriptions \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pro Membership",
    "price": 29,
    "frequency": "monthly",
    "trialPeriod": 0,
    "redirectBackUrl": "https://yourapp.com/billing"
  }'
```

`price` is in USD. `frequency` must be `weekly`, `monthly`, or `yearly`;
`trialPeriod` is the number of trial days. The endpoint returns the created
offer with `201 Created`.

The same operation is available through the official Node.js/TypeScript SDK:

```ts theme={null}
import { Pocketsflow } from "pocketsflow";

const pocketsflow = new Pocketsflow({
  apiKey: process.env.POCKETSFLOW_API_KEY!,
});

const offer = await pocketsflow.subscriptionOffers.create({
  name: "Pro Membership",
  price: 29,
  frequency: "monthly",
  trialPeriod: 0,
  redirectBackUrl: "https://yourapp.com/billing",
});
```

Install the SDK with `npm install pocketsflow`. See the [SDK guide](/api-reference/sdk)
for configuration, resource methods, and error handling. The SDK source's
current v1.2.0 release includes `subscriptionOffers.create`; if your installed
npm version predates that release, use the REST request above until you upgrade.

## 2. Create a checkout session for a subscription offer

`POST /checkout/sessions` accepts a **subscription offer id** in `productId`
(the same field used for one-time products — pass either kind of id). Redirect
the buyer to the `url` in the response; Pocketsflow hosts the checkout and the
recurring billing.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.pocketsflow.com/checkout/sessions \
    -H "Authorization: Bearer pk_live_xxx" \
    -H "Content-Type: application/json" \
    -d '{
      "productId": "665f1c2a9b3e4a0012ab7788",
      "successUrl": "https://yourapp.com/welcome",
      "cancelUrl": "https://yourapp.com/pricing",
      "customerEmail": "ada@example.com",
      "clientReferenceId": "user_8271",
      "metadata": { "plan": "pro" }
    }'
  ```

  ```javascript Node.js theme={null}
  const res = await fetch("https://api.pocketsflow.com/checkout/sessions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.POCKETSFLOW_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      productId: "665f1c2a9b3e4a0012ab7788", // subscription offer _id
      successUrl: "https://yourapp.com/welcome",
      cancelUrl: "https://yourapp.com/pricing",
      customerEmail: user.email,
      clientReferenceId: user.id, // echoed back on every subscription webhook
      metadata: { plan: "pro" },
    }),
  });

  const { url } = await res.json();
  // Redirect the buyer to `url`.
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.post(
      "https://api.pocketsflow.com/checkout/sessions",
      headers={"Authorization": f"Bearer {os.environ['POCKETSFLOW_API_KEY']}"},
      json={
          "productId": "665f1c2a9b3e4a0012ab7788",  # subscription offer _id
          "successUrl": "https://yourapp.com/welcome",
          "cancelUrl": "https://yourapp.com/pricing",
          "customerEmail": user.email,
          "clientReferenceId": user.id,  # echoed back on every subscription webhook
          "metadata": {"plan": "pro"},
      },
  )
  session = res.json()
  # Redirect the buyer to session["url"].
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://api.pocketsflow.com/checkout/sessions');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST           => true,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer ' . getenv('POCKETSFLOW_API_KEY'),
          'Content-Type: application/json',
      ],
      CURLOPT_POSTFIELDS => json_encode([
          'productId'         => '665f1c2a9b3e4a0012ab7788', // subscription offer _id
          'successUrl'        => 'https://yourapp.com/welcome',
          'cancelUrl'         => 'https://yourapp.com/pricing',
          'customerEmail'     => $user_email,
          'clientReferenceId' => $user_id, // echoed back on every subscription webhook
          'metadata'          => ['plan' => 'pro'],
      ]),
  ]);
  $session = json_decode(curl_exec($ch), true);
  curl_close($ch);
  // header('Location: ' . $session['url']);
  ```
</CodeGroup>

The response is `201` with:

```json theme={null}
{
  "id": "cs_3f2a9d0e1b2c4a5d6e7f8091a2b3c4d5e6f70819",
  "url": "https://yourstore.pocketsflow.com/checkout?…"
}
```

`successUrl`/`cancelUrl` must be absolute `http(s)` URLs; `clientReferenceId`
(max 256 chars) and `metadata` are captured at activation and replayed on every
`customer.subscription.*` / `invoice.*` webhook, so you can bind events back to
your own user records. The full field table is in the
[custom platforms guide](/integrations/custom-platforms).

## 3. List your subscription offers

`GET /subscriptions` returns your offers (the recurring products), newest
first, scoped to your account and the key's test/live mode. Use it to find the
`_id` to sell in step 2.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.pocketsflow.com/subscriptions \
    -H "Authorization: Bearer pk_live_xxx"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch("https://api.pocketsflow.com/subscriptions", {
    headers: { Authorization: `Bearer ${process.env.POCKETSFLOW_API_KEY}` },
  });
  const offers = await res.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  offers = requests.get(
      "https://api.pocketsflow.com/subscriptions",
      headers={"Authorization": f"Bearer {os.environ['POCKETSFLOW_API_KEY']}"},
  ).json()
  ```

  ```php PHP theme={null}
  $ch = curl_init('https://api.pocketsflow.com/subscriptions');
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer ' . getenv('POCKETSFLOW_API_KEY'),
      ],
  ]);
  $offers = json_decode(curl_exec($ch), true);
  curl_close($ch);
  ```
</CodeGroup>

```json theme={null}
[
  {
    "_id": "665f1c2a9b3e4a0012ab7788",
    "name": "Pro Membership",
    "subtitle": "Everything in Starter, plus…",
    "price": 29,
    "currency": "usd",
    "frequency": "monthly",
    "trialPeriod": 0,
    "published": true,
    "testMode": false,
    "createdAt": "2026-05-01T09:12:44.103Z",
    "…": "…"
  }
]
```

`frequency` is `weekly`, `monthly`, or `yearly`. A single offer is available at
`GET /subscriptions/{id}` (`404` if it doesn't exist or isn't yours).

## 4. List subscribers (filter by status)

`GET /subscriptions/subscribers` is the paginated roster of buyers across your
offers. Filters: `status`, `buyerEmail` (alias `email`), `subscriptionId`,
`page` (default `1`), `pageSize` (default `20`).

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.pocketsflow.com/subscriptions/subscribers?status=active&page=1&pageSize=20" \
    -H "Authorization: Bearer pk_live_xxx"
  ```

  ```javascript Node.js theme={null}
  const params = new URLSearchParams({ status: "active", page: "1", pageSize: "20" });
  const res = await fetch(
    `https://api.pocketsflow.com/subscriptions/subscribers?${params}`,
    { headers: { Authorization: `Bearer ${process.env.POCKETSFLOW_API_KEY}` } },
  );
  const { subscribers, pagination } = await res.json();
  ```

  ```python Python theme={null}
  import os
  import requests

  data = requests.get(
      "https://api.pocketsflow.com/subscriptions/subscribers",
      headers={"Authorization": f"Bearer {os.environ['POCKETSFLOW_API_KEY']}"},
      params={"status": "active", "page": 1, "pageSize": 20},
  ).json()
  subscribers, pagination = data["subscribers"], data["pagination"]
  ```

  ```php PHP theme={null}
  $query = http_build_query(['status' => 'active', 'page' => 1, 'pageSize' => 20]);
  $ch = curl_init('https://api.pocketsflow.com/subscriptions/subscribers?' . $query);
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer ' . getenv('POCKETSFLOW_API_KEY'),
      ],
  ]);
  $data = json_decode(curl_exec($ch), true);
  curl_close($ch);
  $subscribers = $data['subscribers'];
  ```
</CodeGroup>

```json theme={null}
{
  "subscribers": [
    {
      "_id": "665f1c2a9b3e4a0012abccdd",
      "buyerEmail": "ada@example.com",
      "status": "active",
      "active": true,
      "whopSubscriptionId": "plan_vlD50mPbsqJLp",
      "subscriptionId": "665f1c2a9b3e4a0012ab7788",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "country": "NL",
      "portalUrl": "https://app.pocketsflow.com/portal/665f1c2a9b3e4a0012ab7788/665f1c2a9b3e4a0012abccdd",
      "createdAt": "2026-06-24T10:00:00.000Z",
      "subscription": {
        "_id": "665f1c2a9b3e4a0012ab7788",
        "name": "Pro Membership",
        "price": 29,
        "frequency": "monthly"
      }
    }
  ],
  "pagination": {
    "totalCount": 137,
    "totalPages": 7,
    "currentPage": 1,
    "pageSize": 20,
    "hasMore": true
  }
}
```

`status` values include `active`, `trialing`, `past_due`, `paused`, and
`canceled` (full list in the
[API reference](/api-reference/introduction#subscriptions-and-subscribers)).
`portalUrl` is the subscriber's self-service portal — share it so buyers can
manage their own membership.

## 5. Verify one subscriber's live status (the access gate)

Before serving paid content, look the subscriber up by their `_id` (the 24-hex
Mongo id from step 4 or from webhooks — **not** a `mem_…` id here). Add
`?live=true` to also fetch the authoritative membership from the payment
processor as `whopMembership`; if that live lookup fails, `whopMembership` is
`null` and the stored `status` still applies, so the request never fails.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.pocketsflow.com/subscriptions/subscribers/665f1c2a9b3e4a0012abccdd?live=true" \
    -H "Authorization: Bearer pk_live_xxx"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    `https://api.pocketsflow.com/subscriptions/subscribers/${subscriberId}?live=true`,
    { headers: { Authorization: `Bearer ${process.env.POCKETSFLOW_API_KEY}` } },
  );
  const { status, whopMembership } = await res.json();

  // Gate: prefer the live membership when available, fall back to stored status.
  const isPaying = whopMembership
    ? whopMembership.valid === true
    : ["active", "trialing"].includes(status);
  ```

  ```python Python theme={null}
  import os
  import requests

  data = requests.get(
      f"https://api.pocketsflow.com/subscriptions/subscribers/{subscriber_id}",
      headers={"Authorization": f"Bearer {os.environ['POCKETSFLOW_API_KEY']}"},
      params={"live": "true"},
  ).json()

  # Gate: prefer the live membership when available, fall back to stored status.
  membership = data.get("whopMembership")
  is_paying = (
      membership["valid"] is True
      if membership
      else data["status"] in ("active", "trialing")
  )
  ```

  ```php PHP theme={null}
  $ch = curl_init(
      'https://api.pocketsflow.com/subscriptions/subscribers/' . $subscriber_id . '?live=true'
  );
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer ' . getenv('POCKETSFLOW_API_KEY'),
      ],
  ]);
  $data = json_decode(curl_exec($ch), true);
  curl_close($ch);

  // Gate: prefer the live membership when available, fall back to stored status.
  $is_paying = isset($data['whopMembership'])
      ? ($data['whopMembership']['valid'] ?? false) === true
      : in_array($data['status'], ['active', 'trialing'], true);
  ```
</CodeGroup>

```json theme={null}
{
  "subscriber": {
    "_id": "665f1c2a9b3e4a0012abccdd",
    "buyerEmail": "ada@example.com",
    "status": "active",
    "active": true,
    "portalUrl": "https://app.pocketsflow.com/portal/665f1c2a9b3e4a0012ab7788/665f1c2a9b3e4a0012abccdd",
    "…": "…"
  },
  "subscription": {
    "_id": "665f1c2a9b3e4a0012ab7788",
    "name": "Pro Membership",
    "price": 29,
    "frequency": "monthly",
    "…": "…"
  },
  "customer": { "_id": "665f1c2a9b3e4a0012abaa11", "buyerEmail": "ada@example.com", "…": "…" },
  "status": "active",
  "payments": [
    { "_id": "665f1c2a9b3e4a0012ab0002", "billingReason": "renewal", "amount": 29, "createdAt": "2026-06-24T10:00:00.000Z" },
    { "_id": "665f1c2a9b3e4a0012ab0001", "billingReason": "initial", "amount": 29, "createdAt": "2026-05-24T10:00:00.000Z" }
  ],
  "whopMembership": { "id": "mem_UCTD32gDLGTgb", "status": "active", "valid": true }
}
```

`payments` is the full history for this membership — the `initial` charge plus
every `renewal`. For hot paths, gate on the stored `status` (kept fresh by
webhooks) and reserve `?live=true` for the moments that matter, like restoring
account access after a failed payment.

## 6. Cancel, pause, or resume a subscriber

Three lifecycle actions, all `POST` with an empty body. `{id}` is the
subscriber's `_id` **or** their processor membership id (`mem_…`).

| Action | Path                              | Effect                                                                             |
| ------ | --------------------------------- | ---------------------------------------------------------------------------------- |
| Cancel | `POST /subscriptions/{id}/cancel` | Cancels **at period end** — the buyer keeps access until the paid period runs out. |
| Pause  | `POST /subscriptions/{id}/pause`  | Stops payment collection; the buyer **keeps access** until resumed.                |
| Resume | `POST /subscriptions/{id}/resume` | Reverses a scheduled cancel / restarts collection.                                 |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.pocketsflow.com/subscriptions/665f1c2a9b3e4a0012abccdd/cancel \
    -H "Authorization: Bearer pk_live_xxx"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    `https://api.pocketsflow.com/subscriptions/${subscriberId}/cancel`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.POCKETSFLOW_API_KEY}` },
    },
  );
  const { success, status, cancelAtPeriodEnd } = await res.json();
  // Same call shape for `/pause` and `/resume`.
  ```

  ```python Python theme={null}
  import os
  import requests

  result = requests.post(
      f"https://api.pocketsflow.com/subscriptions/{subscriber_id}/cancel",
      headers={"Authorization": f"Bearer {os.environ['POCKETSFLOW_API_KEY']}"},
  ).json()
  # Same call shape for `/pause` and `/resume`.
  ```

  ```php PHP theme={null}
  $ch = curl_init(
      'https://api.pocketsflow.com/subscriptions/' . $subscriber_id . '/cancel'
  );
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST           => true,
      CURLOPT_HTTPHEADER     => [
          'Authorization: Bearer ' . getenv('POCKETSFLOW_API_KEY'),
      ],
  ]);
  $result = json_decode(curl_exec($ch), true);
  curl_close($ch);
  // Same call shape for `/pause` and `/resume`.
  ```
</CodeGroup>

`cancel` and `resume` respond with:

```json theme={null}
{
  "success": true,
  "status": "active",
  "cancelAtPeriodEnd": true,
  "manageUrl": "https://whop.com/…"
}
```

`pause` adds a `paused` boolean to the same shape. Pause is provider-first: if
the processor refuses, you get a `502` and **no local state changes** — and a
subscriber with no membership on record returns `400`. Cancelling also fires
the `customer.subscription.updated` webhook (and `customer.subscription.deleted`
when the membership actually ends); pausing/resuming fire
`customer.subscription.pause` / `customer.subscription.resumed`.

## 7. Receive and verify subscription webhooks

Register an endpoint (dashboard, or `POST /webhooks`) and subscribe to the
subscription events. Every delivery is a `POST` with these headers:

| Header                    | Description                                                                                      |
| ------------------------- | ------------------------------------------------------------------------------------------------ |
| `X-Pocketsflow-Event`     | The event name — 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). |

Subscription-related events: `customer.subscription.created`,
`customer.subscription.updated`, `customer.subscription.deleted`,
`customer.subscription.pause`, `customer.subscription.resumed`,
`customer.subscription.trial_will_end`, `invoice.created`, `invoice.upcoming`,
`invoice.payment_succeeded`, `invoice.payment_failed`,
`payment_intent.succeeded`, and `payment_intent.payment_failed`. Every payload
carries a top-level `subscriptionCustomerId` (the subscriber `_id` from steps
4–6) and your `clientReferenceId`/`metadata` from checkout. Payload shapes:
[Webhook events](/api-webhooks/events).

Always verify the signature over the **raw body** (never re-serialize the
parsed JSON) with a constant-time comparison:

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

  const app = express();

  app.post(
    "/webhooks/pocketsflow",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const signature = req.header("X-Pocketsflow-Signature") || "";
      const expected = crypto
        .createHmac("sha256", process.env.POCKETSFLOW_WEBHOOK_SECRET)
        .update(req.body) // raw Buffer
        .digest("hex");

      const a = Buffer.from(signature);
      const b = Buffer.from(expected);
      if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
        return res.status(400).send("Invalid signature");
      }

      const event = req.header("X-Pocketsflow-Event");
      const payload = JSON.parse(req.body.toString("utf8"));

      switch (event) {
        case "customer.subscription.created":
          grantAccess(payload.subscriptionCustomerId, payload.clientReferenceId);
          break;
        case "customer.subscription.deleted":
          revokeAccess(payload.subscriptionCustomerId);
          break;
        case "invoice.payment_failed":
          flagPastDue(payload.subscriptionCustomerId);
          break;
      }
      // Handlers must be idempotent.

      res.status(200).send("OK");
    }
  );
  ```

  ```python Python (Flask) theme={null}
  import hashlib
  import hmac
  import os

  from flask import Flask, abort, request

  app = Flask(__name__)

  @app.post("/webhooks/pocketsflow")
  def pocketsflow_webhook():
      raw = request.get_data()  # raw bytes — never re-serialize
      signature = request.headers.get("X-Pocketsflow-Signature", "")
      expected = hmac.new(
          os.environ["POCKETSFLOW_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
      ).hexdigest()

      if not hmac.compare_digest(signature, expected):
          abort(400)

      event = request.headers.get("X-Pocketsflow-Event")
      payload = request.get_json()

      if event == "customer.subscription.created":
          grant_access(payload["subscriptionCustomerId"], payload.get("clientReferenceId"))
      elif event == "customer.subscription.deleted":
          revoke_access(payload["subscriptionCustomerId"])
      elif event == "invoice.payment_failed":
          flag_past_due(payload["subscriptionCustomerId"])
      # Handlers must be idempotent.

      return "OK", 200
  ```

  ```php PHP theme={null}
  $raw       = file_get_contents( 'php://input' );
  $signature = $_SERVER['HTTP_X_POCKETSFLOW_SIGNATURE'] ?? '';
  $expected  = hash_hmac( 'sha256', $raw, getenv( 'POCKETSFLOW_WEBHOOK_SECRET' ) );

  if ( ! hash_equals( $expected, $signature ) ) {
      http_response_code( 400 );
      exit( 'Invalid signature' );
  }

  $event   = $_SERVER['HTTP_X_POCKETSFLOW_EVENT'] ?? '';
  $payload = json_decode( $raw, true );

  switch ( $event ) {
      case 'customer.subscription.created':
          grant_access( $payload['subscriptionCustomerId'], $payload['clientReferenceId'] ?? null );
          break;
      case 'customer.subscription.deleted':
          revoke_access( $payload['subscriptionCustomerId'] );
          break;
      case 'invoice.payment_failed':
          flag_past_due( $payload['subscriptionCustomerId'] );
          break;
  }
  // Handlers must be idempotent.

  http_response_code( 200 );
  echo 'OK';
  ```
</CodeGroup>

<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.
  If a delivery is missed, reconcile via
  `GET /subscriptions/subscribers` (step 4).
</Note>

## Example repository

Prefer runnable code? The [subscriptions example on GitHub](https://github.com/pocketsflow/subscriptions-example)
has everything on this page as a clone-and-run project: a checkout embed (hosted
link, popup, and inline) plus a Node/Express webhook receiver that verifies the
signature and handles the full subscription lifecycle.

## Related topics

* [Subscriptions example repo](https://github.com/pocketsflow/subscriptions-example) — runnable embed + webhook code
* [Webhook events](/api-webhooks/events) — every payload, field by field
* [Authentication & security](/api-webhooks/authentication-and-security)
* [Consuming webhooks](/api-webhooks/consuming-webhooks)
* [API reference](/api-reference/introduction)
* [Subscriptions guide](/selling/subscriptions) — creating offers in the dashboard
* [Custom platforms](/integrations/custom-platforms) — the generic checkout + webhook pattern
