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

# Webhooks

> Receive signed events when orders, products, reviews and affiliate records change.

Webhooks send an HTTP `POST` to your server when something happens in your store, so you don't have to poll.

Create endpoints in **Developers › Webhooks** or with [`POST /webhooks`](#manage-endpoints-with-the-api).

## Events

| Event | When |
| :- | :- |
| `order.created` | An order is created (storefront, API or embed). |
| `order.completed` | An order is paid and delivered. Includes the deliveries. |
| `order.partially_paid` | Part of a crypto payment arrived. |
| `order.manual_review` | A manual payment waits for your review. |
| `order.expired` | An order wasn't paid in time. |
| `order.cancelled` | An unpaid order was cancelled. |
| `order.refunded` | A refund was recorded. Sent once per refunded amount. |
| `product.out_of_stock` | A product ran out of stock. |
| `product.restocked` | Stock was added to a product. |
| `product.deleted` | A product was deleted. |
| `review.created` | A customer left a review. |
| `question.created` | A customer asked a question on a product page. |
| `question.answered` | Your team answered a product question (dashboard or API). `answer.first` is `true` for the first answer. |
| `customer.created` | A new customer was added to your store. |
| `withdrawal.completed` | A wallet withdrawal was sent. |
| `affiliate.partner_applied` | Someone applied to your affiliate programme. |
| `affiliate.conversion` | An attributed order completed and the commission was recorded. |
| `affiliate.payout_requested` | A partner requested a payout. |
| `affiliate.payout_sent` | You marked a payout as sent. |

`GET /events/types` returns the same list.

## Payload

```json theme={"system"}
{
  "id": "evt_9b2c4f1a7d",
  "object": "event",
  "type": "order.completed",
  "created": 1791100000,
  "livemode": true,
  "store": { "id": "0bHQn0bU2dei", "name": "Acme", "slug": "acme" },
  "data": {
    "object": {
      "object": "order",
      "id": "c35ffd-19a1b2c3d4e-8c7a20",
      "status": "completed",
      "currency": "USD",
      "total": 34.99,
      "customer": { "email": "buyer@example.com", "country": "GB" },
      "items": [{ "product_id": "a1B2c3D4e5F6", "name": "Pro License", "quantity": 1, "unit_price": 34.99, "total": 34.99 }],
      "paid_at": "2026-10-04T12:03:00.000Z"
    }
  }
}
```

<Note>
  Webhook amounts are in major units (`34.99`), unlike the API, which uses minor units (`3499`).
</Note>

## Verify the signature

Every request has these headers:

| Header | Value |
| :- | :- |
| `PapelShip-Signature` | `t=<unix seconds>,v1=<hex HMAC>` |
| `PapelShip-Event-Id` | The event ID. Use it to ignore duplicates. |
| `PapelShip-Event-Type` | The event type. |
| `PapelShip-Delivery-Attempt` | 1 for the first try. |

The signature is `HMAC-SHA256(secret, "<t>.<raw body>")` in hex. The secret (`whsec_...`) is shown when you create the endpoint. Compute it over the **raw** body, before parsing JSON, and reject old timestamps.

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

  export function verify(rawBody, header, secret, toleranceSec = 300) {
    const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    const t = Number(parts.t);
    if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
    const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
    const a = Buffer.from(expected);
    const b = Buffer.from(parts.v1 || "");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }
  ```

  ```python Python theme={"system"}
  import hmac, hashlib, time

  def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      t = int(parts.get("t", 0))
      if not t or abs(time.time() - t) > tolerance:
          return False
      expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, parts.get("v1", ""))
  ```

  ```php PHP theme={"system"}
  function verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
      parse_str(str_replace(',', '&', $header), $parts);
      $t = (int)($parts['t'] ?? 0);
      if (!$t || abs(time() - $t) > $tolerance) return false;
      $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
      return hash_equals($expected, $parts['v1'] ?? '');
  }
  ```
</CodeGroup>

## Respond and retries

* Answer with any `2xx` status within 10 seconds. Do slow work after you respond.
* Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (7 attempts over about 21 hours).
* An endpoint is turned off after 40 failures in a row with no success in 3 days. Turn it back on in the dashboard or with `PATCH /webhooks/{id}` and `enabled: true`.
* The same event can arrive more than once. Use `PapelShip-Event-Id` to process it once.

## Manage endpoints with the API

```bash theme={"system"}
curl -X POST https://app.papelship.com/api/v1/webhooks \
  -H "x-api-key: pk_live_your_key" \
  -H "x-store-hash: your_store_id" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/webhooks/papelship", "events": ["order.completed", "order.refunded"] }'
```

The response contains the signing `secret`. It is shown only once; store it in your server's environment.

| Endpoint | Use |
| :- | :- |
| `GET /webhooks` | List endpoints with 7-day delivery stats. |
| `PATCH /webhooks/{id}` | Change the URL, events or `enabled`. |
| `POST /webhooks/{id}/test` | Send a sample event. |
| `GET /events/deliveries` | Delivery log with status and response codes. |
| `POST /events/deliveries/{id}/resend` | Send a delivery again. |

Rolling the secret stays in the dashboard.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.