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

# Custom platforms

> The generic redirect + webhook pattern to integrate Pocketsflow with any store, platform, or no-code tool.

The [WooCommerce guide](/integrations/woocommerce) is one concrete example of a
general pattern that works for **any** platform — a custom storefront, a Shopify
app, a membership site, or a no-code tool. This page distills that pattern with
language-agnostic examples.

## The two calls you need

<Steps>
  <Step title="Create a checkout session, then redirect">
    Server-side, call `POST /checkout/sessions` and send the buyer to the `url`
    in the response. Put your own order/cart id in `metadata`.
  </Step>

  <Step title="Confirm via the order.completed webhook">
    When the webhook fires, verify the signature and read your id back out of
    `metadata` to settle the matching order.
  </Step>
</Steps>

## 1. Create a checkout session

<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": "65a1b2c3d4e5f6a7b8c9d0e1",
      "successUrl": "https://store.example.com/thank-you",
      "cancelUrl": "https://store.example.com/cart",
      "customerEmail": "buyer@example.com",
      "metadata": { "external_order_id": "ORDER-1043" }
    }'
  ```

  ```js 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: "65a1b2c3d4e5f6a7b8c9d0e1",
      successUrl: "https://store.example.com/thank-you",
      cancelUrl: "https://store.example.com/cart",
      customerEmail: order.email,
      metadata: { external_order_id: order.id },
    }),
  });

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

  ```php PHP theme={null}
  $response = wp_remote_post( 'https://api.pocketsflow.com/checkout/sessions', array(
      'headers' => array(
          'Authorization' => 'Bearer ' . POCKETSFLOW_API_KEY,
          'Content-Type'  => 'application/json',
      ),
      'body' => wp_json_encode( array(
          'productId'  => '65a1b2c3d4e5f6a7b8c9d0e1',
          'successUrl' => 'https://store.example.com/thank-you',
          'cancelUrl'  => 'https://store.example.com/cart',
          'metadata'   => array( 'external_order_id' => $order_id ),
      ) ),
  ) );

  $body = json_decode( wp_remote_retrieve_body( $response ), true );
  // header( 'Location: ' . $body['url'] );
  ```
</CodeGroup>

| Field               | Required | Notes                                                                                                                                                                                     |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productId`         | Yes      | A Pocketsflow product **or subscription offer** owned by your account. Pass either kind of id here.                                                                                       |
| `successUrl`        | No       | Absolute `http(s)` URL the buyer returns to after paying.                                                                                                                                 |
| `cancelUrl`         | No       | Absolute `http(s)` URL if they abandon checkout.                                                                                                                                          |
| `customerEmail`     | No       | Prefills the buyer's email on the checkout (and inside the payment form), and is attached to the resulting order.                                                                         |
| `lockEmail`         | No       | With `customerEmail`, makes the prefilled email read-only so the buyer can't change the address you keyed your records on.                                                                |
| `clientReferenceId` | No       | Your own correlation id (max 256 chars). Echoed back **top-level** as `clientReferenceId` on the resulting webhooks — including every subscription lifecycle event.                       |
| `discountCode`      | No       | A discount code to pre-apply.                                                                                                                                                             |
| `metadata`          | No       | Arbitrary key/values, echoed back on the order + webhooks. For subscriptions, the metadata is captured at activation and replayed on every `customer.subscription.*` / `invoice.*` event. |

The response is `{ "id": "cs_…", "url": "https://yourstore.pocketsflow.com/checkout?…" }`.

## 2. Verify and handle the webhook

Every webhook is an HTTP `POST` signed with HMAC-SHA256 over the raw body, keyed
with your endpoint's signing secret. Verify it before trusting the payload.

<CodeGroup>
  ```js 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");

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

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

      if (event === "order.completed" && orderId) {
        markOrderPaid(orderId); // idempotent
      }

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

  ```php PHP theme={null}
  $raw       = file_get_contents( 'php://input' );
  $signature = $_SERVER['HTTP_X_POCKETSFLOW_SIGNATURE'] ?? '';
  $expected  = hash_hmac( 'sha256', $raw, 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 );
  $orderId = $payload['metadata']['external_order_id'] ?? null;

  if ( $event === 'order.completed' && $orderId ) {
      mark_order_paid( $orderId ); // idempotent
  }

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

## Best practices

* **Verify against the raw body.** Don't re-serialize the JSON before hashing.
* **Be idempotent.** A webhook may be re-sent; processing the same event twice
  must be safe (check whether the order is already paid first).
* **Acknowledge fast.** Return `2xx` quickly and do heavy work asynchronously.
* **Reconcile.** Treat the webhook as the source of truth; if one is missed, use
  `GET /orders` and match on your `metadata` id.
* **Keep secrets server-side.** API keys and signing secrets must never reach the
  browser.

## A note on Shopify and marketplace apps

The same redirect + webhook pattern works for a custom Shopify integration
today. A deeper, no-code "native app" experience (one-click connect, automatic
product sync) for WooCommerce, Shopify, and others is planned — see the
[integrations overview](/integrations/overview) for what's available now.

## Related topics

* [WooCommerce guide](/integrations/woocommerce)
* [API reference](/api-reference/introduction)
* [Webhook events](/api-webhooks/events)
* [Consuming webhooks](/api-webhooks/consuming-webhooks)
