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

# Webhooks

> Receive real-time notifications for events

## Overview

Webhooks notify your server when events happen in Shoppex. Use them to:

* Fulfill orders automatically
* Update your database
* Send custom notifications
* Integrate with external services

<Info>
  This page covers normal Shoppex event webhooks like `order:paid` and `subscription:created`, including the secrets created in **Settings -> Webhooks**.
  If you are implementing a `DYNAMIC` product with `dynamic_webhook`, use [Dynamic Product Delivery](/api-reference/webhooks/dynamic-delivery).
</Info>

<Info>
  You can register webhooks through the dashboard or manage webhook subscriptions through the Developer API.
  The event payload is the same either way.
</Info>

## Setting Up Webhooks

<Steps>
  <Step title="Create Endpoint">
    Create an HTTP endpoint on your server that accepts POST requests:

    ```typescript theme={"system"}
    app.post('/webhooks/shoppex', (req, res) => {
      const event = req.body;
      // Handle event
      res.status(200).send('OK');
    });
    ```
  </Step>

  <Step title="Register in Dashboard">
    Go to **Settings → Webhooks → Add Endpoint** and enter your URL.
  </Step>

  <Step title="Select Events">
    Choose explicit event names, for example:

    * `order:paid`
    * `order:cancelled`
    * `subscription:created`

    If you use the Dev API, fetch the full allowlist from `GET /dev/v1/webhooks/events`.
  </Step>
</Steps>

## Webhook Payload

All webhooks follow this structure:

```json theme={"system"}
{
  "event": "order:paid",
  "data": {
    "uniqid": "abc123def456",
    "type": "PRODUCT",
    "status": "COMPLETED",
    "gateway": "STRIPE",
    "total": 29.99,
    "total_display": 29.99,
    "currency": "USD",
    "exchange_rate": 1,
    "crypto_exchange_rate": 0,
    "crypto_gateway": null,
    "apm_method": "CARD",
    "customer_email": "customer@example.com",
    // ... more event-specific fields
  },
  "created_at": 1705314650
}
```

<Note>
  Dashboard test deliveries use the same top-level envelope as live deliveries: `event`, `data`, and `created_at`.
  The values inside `data` are synthetic, but the signature covers the full raw JSON body exactly the same way as a real webhook.
</Note>

<Note>
  Top-level webhook `created_at` is a Unix timestamp.
  Nested timestamps inside `data` can be ISO 8601 strings.
</Note>

<Note>
  One invoice can produce more than one payment attempt over time.
  For example, a customer can retry a payment or switch gateways.
  Treat the Shoppex invoice ID and event type as the durable business signal, not a single provider session.
</Note>

<Note>
  Order webhooks can also include payment-context fields like `exchange_rate`, `crypto_exchange_rate`, `crypto_gateway`, and `apm_method`.
  These help with reconciliation and analytics without requiring a follow-up invoice fetch in common cases.
</Note>

## Available Events

### Common Order Events

| Event                     | Description                                  |
| ------------------------- | -------------------------------------------- |
| `order:paid`              | Order/invoice paid successfully              |
| `order:cancelled`         | Order cancelled or expired                   |
| `order:paid:product`      | Order paid (includes full product data)      |
| `order:cancelled:product` | Order cancelled (includes full product data) |

<Note>
  If a paid checkout reduces product stock, handle `order:paid` or `order:paid:product`.
  Shoppex does not send `product:stock` as a second event for every purchase-driven stock decrement.
  Use `product:stock` for direct catalog stock updates, for example when stock is edited through the dashboard or API.
</Note>

### Common Subscription Events

| Event                    | Description              |
| ------------------------ | ------------------------ |
| `subscription:created`   | New subscription started |
| `subscription:cancelled` | Subscription cancelled   |

<Info>
  Shoppex supports more event names than the short guide tables above.
  Examples: `order:created`, `order:updated`, `order:partial`, `order:disputed`, `subscription:trial:started`, `subscription:updated`, `subscription:renewed`, `subscription:upcoming`, plus product/query/feedback/affiliate events.
  Use [Webhook Events Reference](/api-reference/webhooks/events) or `GET /dev/v1/webhooks/events` for the full current list.
</Info>

## Verifying Webhooks

Always verify webhook signatures to ensure authenticity. New integrations should verify `X-Shoppex-Signature-V2`.
Shoppex signs `${deliveryId}.${timestamp}.${rawBody}` using **HMAC-SHA256**:

```typescript theme={"system"}
import crypto from 'crypto';

function verifyWebhook(payload: string, signatureHeader: string, deliveryId: string, timestampHeader: string, secret: string): boolean {
  const segments = signatureHeader.split(',').map((part) => part.trim());
  const hasV1Marker = segments.includes('v1');
  const parts = Object.fromEntries(segments.filter((part) => part.includes('=')).map((part) => {
    const [key, value] = part.trim().split('=');
    return [key, value ?? ''];
  }));
  if (!deliveryId || !hasV1Marker || parts.t !== timestampHeader) return false;

  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;
  if (!/^[0-9a-f]{64}$/i.test(parts.h ?? '')) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${deliveryId}.${parts.t}.${payload}`)
    .digest('hex');

  if (parts.h.length !== expected.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(parts.h, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

app.post('/webhooks/shoppex', (req, res) => {
  const signature = req.headers['x-shoppex-signature-v2'] as string;
  const deliveryId = req.headers['x-shoppex-delivery'] as string;
  const timestamp = req.headers['x-shoppex-timestamp'] as string;
  const isValid = verifyWebhook(req.rawBody, signature, deliveryId, timestamp, WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook based on event type
  const { event, data } = req.body;

  switch (event) {
    case 'order:paid':
      // Handle successful payment
      break;
    case 'order:cancelled':
      // Handle cancellation
      break;
  }

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

Shoppex signs the full JSON request body exactly as sent on the wire.
Simple example: verify `{"event":"order:paid","data":{...},"created_at":1775764170}`, not just the inner `data` object.
Use the per-webhook secret from **Settings -> Webhooks** for that endpoint.

Per-payment callbacks created with the `webhook` field on `POST /dev/v1/payments` use the `webhook_secret` returned in that payment creation response instead.

The `v1` segment in `X-Shoppex-Signature-V2` is a version marker, not the signature.
Simple example: in `v1,t=1775764170,h=abc...`, verify the `h` value.

Do not verify `JSON.stringify(req.body)` after parsing.
Verify the raw body bytes from the incoming HTTP request instead.

### Webhook Headers

| Header                   | Description                                                   |
| ------------------------ | ------------------------------------------------------------- |
| `X-Shoppex-Event`        | The event type (e.g., `order:paid`)                           |
| `X-Shoppex-Timestamp`    | Unix timestamp in seconds used in the v2 signature            |
| `X-Shoppex-Signature-V2` | Timestamped HMAC-SHA256 signature: `v1,t=<timestamp>,h=<hex>` |
| `X-Shoppex-Signature`    | Deprecated legacy HMAC-SHA512 body-only signature             |
| `X-Shoppex-Delivery`     | Unique delivery ID for deduplication                          |

<Warning>
  `X-Shoppex-Signature` and `X-Shoppex-Unescaped-Signature` are deprecated legacy headers.
  Use `X-Shoppex-Signature-V2` for new integrations.
</Warning>

## Retry Policy

If your endpoint returns an error (non-2xx status), we retry:

| Delivery        | Delay      |
| --------------- | ---------- |
| Initial attempt | Immediate  |
| Retry 1         | 2 minutes  |
| Retry 2         | 4 minutes  |
| Retry 3         | 8 minutes  |
| Retry 4         | 16 minutes |

After the 5th failed attempt, the webhook is marked as failed. You can manually retry from the dashboard.

<Warning>
  Your endpoint must respond within **30 seconds** or the request will timeout.
</Warning>

## Best Practices

<AccordionGroup>
  <Accordion title="Return 200 quickly">
    Process webhooks asynchronously. Return 200 immediately and handle the event in a background job.
  </Accordion>

  <Accordion title="Handle duplicates">
    Webhooks may be delivered more than once. Use the delivery ID to deduplicate, and make your fulfillment logic idempotent.
  </Accordion>

  <Accordion title="Trust final Shoppex state">
    A provider can have retries, delayed confirmations, or multiple attempts for one invoice.
    Good example: mark an order fulfilled when Shoppex tells you the invoice is paid, not when you first see a provider-specific session ID.
  </Accordion>

  <Accordion title="Verify signatures">
    Always verify webhook signatures in production to prevent spoofing.
    Simple example: use the raw request body, not a parsed and re-serialized object.
  </Accordion>

  <Accordion title="Test with realistic payloads">
    Dashboard test deliveries now use the same webhook envelope and core order fields as live deliveries.
    Simple example: a test `order:paid` webhook includes fields like `gateway`, `currency`, `total_display`, and `apm_method`, not just a minimal placeholder object.
  </Accordion>
</AccordionGroup>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhook Events Reference" icon="list" href="/api-reference/webhooks/events">
    Full list of all event types and payload schemas
  </Card>

  <Card title="Dynamic Delivery" icon="truck-fast" href="/api-reference/webhooks/dynamic-delivery">
    Real-time product delivery via webhook response
  </Card>
</CardGroup>
