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

# WooCommerce

> Sell through your WooCommerce store and settle payments with Pocketsflow using the API and webhooks.

This guide shows how to connect a **WooCommerce** store to Pocketsflow so that
customers pay through Pocketsflow's hosted checkout while their order is tracked
in WooCommerce.

It uses only public Pocketsflow APIs and webhooks — no plugin marketplace
listing required. (A native, no-code WooCommerce app is planned separately.)

## How it works

Pocketsflow acts as a **redirect payment gateway** for WooCommerce:

```mermaid theme={null}
sequenceDiagram
    participant B as Buyer
    participant W as WooCommerce
    participant P as Pocketsflow
    B->>W: Place order (choose "Pay with Pocketsflow")
    W->>P: POST /checkout/sessions (productId, metadata.woocommerce_order_id)
    P-->>W: { id, url }
    W-->>B: Redirect to checkout url
    B->>P: Complete payment on hosted checkout
    P-->>B: Redirect to successUrl (back to WooCommerce)
    P->>W: Webhook order.completed (signed)
    W->>W: Verify signature, mark order paid
```

The buyer's WooCommerce order id travels in `metadata` and is echoed back on the
`order.completed` webhook, which is how WooCommerce knows **which** order to mark
as paid.

## Prerequisites

* A Pocketsflow account with at least one product.
* An API key (`pk_live_…` for production, `pk_test_…` while developing). Create
  one under **Developers → API keys**.
* A WooCommerce store you can add a small custom plugin / snippet to.

<Note>
  Develop against a `pk_test_…` key first. Test-mode checkout sessions don't
  move real money, and you'll receive test-mode webhooks so you can validate the
  full flow end to end.
</Note>

## Step 1 — Map WooCommerce products to Pocketsflow products

Each WooCommerce product that should be paid through Pocketsflow needs a
corresponding Pocketsflow `productId`. Create the product in the dashboard (or
via `POST /products`) and store the id on the WooCommerce product, e.g. as a
custom field `_pocketsflow_product_id`.

```php theme={null}
// Save the mapping on the WooCommerce product (admin side)
update_post_meta( $wc_product_id, '_pocketsflow_product_id', '65a1b2c3d4e5f6a7b8c9d0e1' );
```

## Step 2 — Create a checkout session when the order is placed

Register Pocketsflow as a payment gateway and, in `process_payment()`, create a
checkout session and redirect the buyer to the returned `url`.

```php theme={null}
class WC_Gateway_Pocketsflow extends WC_Payment_Gateway {

    public function process_payment( $order_id ) {
        $order = wc_get_order( $order_id );

        // For simplicity, assume a single line item mapped to a Pocketsflow product.
        $items = $order->get_items();
        $item  = reset( $items );
        $pf_product_id = get_post_meta( $item->get_product_id(), '_pocketsflow_product_id', true );

        $response = wp_remote_post( 'https://api.pocketsflow.com/checkout/sessions', array(
            'headers' => array(
                'Authorization' => 'Bearer ' . POCKETSFLOW_API_KEY,
                'Content-Type'  => 'application/json',
            ),
            'timeout' => 20,
            'body'    => wp_json_encode( array(
                'productId'     => $pf_product_id,
                'customerEmail' => $order->get_billing_email(),
                'successUrl'    => $this->get_return_url( $order ),
                'cancelUrl'     => wc_get_checkout_url(),
                // Echoed back on the webhook so we can find this order later:
                'metadata'      => array(
                    'woocommerce_order_id' => (string) $order_id,
                ),
            ) ),
        ) );

        if ( is_wp_error( $response ) ) {
            wc_add_notice( 'Payment error: unable to start checkout.', 'error' );
            return array( 'result' => 'failure' );
        }

        $body = json_decode( wp_remote_retrieve_body( $response ), true );

        // Mark the WC order as pending until the webhook confirms payment.
        $order->update_status( 'pending', 'Awaiting Pocketsflow payment.' );

        return array(
            'result'   => 'success',
            'redirect' => $body['url'], // hosted Pocketsflow checkout
        );
    }
}
```

The `successUrl` is where the buyer lands after paying — WooCommerce's standard
"order received" page (`$this->get_return_url( $order )`). The `cancelUrl` sends
them back to the cart if they abandon checkout.

## Step 3 — Register a webhook

Tell Pocketsflow where to send events. You can do this once from the dashboard
(**Developers → Webhooks**) or via the API:

```bash theme={null}
curl -X POST https://api.pocketsflow.com/webhooks \
  -H "Authorization: Bearer pk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://store.example.com/?wc-api=pocketsflow_webhook",
    "events": ["order.completed", "order.refunded"]
  }'
```

The response includes a **signing secret** (shown once). Store it securely — you
need it to verify incoming webhooks.

## Step 4 — Handle the webhook and mark the order paid

Pocketsflow signs every webhook so you can confirm it really came from us. Each
delivery includes these headers:

| Header                    | Description                                                                |
| ------------------------- | -------------------------------------------------------------------------- |
| `X-Pocketsflow-Event`     | The event type, e.g. `order.completed`.                                    |
| `X-Pocketsflow-Signature` | HMAC-SHA256 of the raw request body, keyed with your signing secret (hex). |
| `X-Pocketsflow-Timestamp` | When the event was sent (ms since epoch).                                  |

Verify the signature against the **raw** body, then use the
`metadata.woocommerce_order_id` to settle the right order:

```php theme={null}
add_action( 'woocommerce_api_pocketsflow_webhook', function () {
    $raw       = file_get_contents( 'php://input' );
    $signature = $_SERVER['HTTP_X_POCKETSFLOW_SIGNATURE'] ?? '';
    $event     = $_SERVER['HTTP_X_POCKETSFLOW_EVENT'] ?? '';

    // 1. Verify the signature (timing-safe).
    $expected = hash_hmac( 'sha256', $raw, POCKETSFLOW_WEBHOOK_SECRET );
    if ( ! hash_equals( $expected, $signature ) ) {
        status_header( 400 );
        exit( 'Invalid signature' );
    }

    $payload = json_decode( $raw, true );
    $wc_order_id = $payload['metadata']['woocommerce_order_id'] ?? null;
    $order = $wc_order_id ? wc_get_order( $wc_order_id ) : null;

    if ( ! $order ) {
        status_header( 200 ); // Nothing to do — acknowledge so we don't retry.
        exit( 'OK' );
    }

    // 2. Idempotency — don't double-process.
    if ( $order->is_paid() ) {
        status_header( 200 );
        exit( 'Already processed' );
    }

    // 3. React based on the event type.
    if ( $event === 'order.completed' ) {
        $order->payment_complete( $payload['order']['id'] ?? '' );
        $order->add_order_note( 'Paid via Pocketsflow.' );
    } elseif ( $event === 'order.refunded' ) {
        $order->update_status( 'refunded', 'Refunded in Pocketsflow.' );
    }

    status_header( 200 );
    exit( 'OK' );
} );
```

<Warning>
  Always verify against the **raw, unparsed** request body. Re-serializing the
  JSON (e.g. `json_encode(json_decode($raw))`) can reorder keys and break the
  signature check.
</Warning>

## Step 5 — Test the full flow

1. Switch your gateway to use a `pk_test_…` key.
2. Place a test order in WooCommerce and complete the hosted checkout.
3. Confirm the buyer is redirected back to the WooCommerce "order received" page.
4. Confirm the `order.completed` webhook arrives and the order flips to
   **Processing / Completed**.

You can also resend a test event from **Developers → Webhooks → ⋯ → Send test**
to exercise your handler without placing an order.

## Handling edge cases

<AccordionGroup>
  <Accordion title="Buyer abandons checkout">
    They're returned to your `cancelUrl` and the WooCommerce order stays
    `pending`. WooCommerce's "hold stock" / cancel-unpaid-orders settings will
    clean these up.
  </Accordion>

  <Accordion title="Webhook missed or delayed">
    Webhooks are the source of truth for payment. If one is missed, reconcile by
    listing recent orders via `GET /orders` and matching on
    `metadata.woocommerce_order_id`. Keep your handler idempotent so a re-send is
    safe.
  </Accordion>

  <Accordion title="Multiple line items">
    The example assumes one mapped product per order. For carts with several
    items, create a checkout session per mapped line item, or model the cart as a
    single Pocketsflow product. Mixed carts are covered as a follow-up in the
    integration spec.
  </Accordion>
</AccordionGroup>

## Reference

* [Checkout session API](https://api.pocketsflow.com/docs) — `POST /checkout/sessions`
* [Webhook events](/api-webhooks/events)
* [Authentication & security](/api-webhooks/authentication-and-security)
* [Custom platforms](/integrations/custom-platforms) — the same pattern for any store
