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

# Webhooks

> Subscribe to events, verify each signed delivery, and fetch the latest resource when a link is present.

Webhooks call your endpoint when something changes, so you do not have to poll constantly. Each delivery is a small, signed event. Fetch the full resource from the API when the event includes a link.

## Event types

| Event                          | Fires when                                                                                                |
| ------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `order.created`                | A new order is placed                                                                                     |
| `order.updated`                | An order changes                                                                                          |
| `order.assigned`               | An order is assigned to a member of staff                                                                 |
| `order.status_changed`         | An order moves to a new status                                                                            |
| `order.payment_status_changed` | An order's payment status changes, including refunds and voids. Fetch the order and read `payment_status` |

The set is append-only; new event types may be added over time.

## Event shape

Events are deliberately thin and carry no personal data. They tell you what changed and where to read it:

```json theme={null}
{
  "id": "01JZ7Q3R8M5W2K0X4Y6B1C3D5E",
  "type": "order.status_changed",
  "occurred_at": "2026-06-27T10:15:00+01:00",
  "tenant": {
    "ref": "550e8400-e29b-41d4-a716-446655440000"
  },
  "resource_type": "order",
  "resource_id": 1042,
  "self_url": "https://your-shop.example/api/partner/v1/orders/1042"
}
```

`self_url` is present only when the resource can be resolved to a public API URL. When it is present, call it with your read token to get the full, current resource. When it is absent, use the event as a signal to reconcile with the relevant list endpoint.

## Verifying the signature

Each delivery includes a signature header:

```http theme={null}
Webhook-Signature: t=1782902400,v1=4f1e...c9
```

The value format is `t={unix},v1={hmac_sha256_hex}`. Compute HMAC-SHA256 over `{timestamp}.{rawBody}` using the subscription signing secret, then compare it with `v1` using a timing-safe comparison. Use the raw request body exactly as received, before JSON parsing. Once the signature matches, also reject deliveries whose `t` timestamp is more than five minutes old; because `t` is part of the signed payload, this stops a captured delivery being replayed later.

<CodeGroup>
  ```php PHP theme={null}
  <?php

  $secret = getenv('DF_WEBHOOK_SECRET');
  $header = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
  $rawBody = file_get_contents('php://input');

  $parts = [];
  foreach (explode(',', $header) as $part) {
      [$key, $value] = array_pad(explode('=', $part, 2), 2, null);
      if ($key !== null && $value !== null) {
          $parts[$key] = $value;
      }
  }

  $timestamp = $parts['t'] ?? null;
  $signature = $parts['v1'] ?? null;

  if (!$timestamp || !$signature) {
      http_response_code(400);
      exit;
  }

  $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

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

  // Reject stale deliveries to prevent replay of a captured request.
  if (abs(time() - (int) $timestamp) > 300) {
      http_response_code(400);
      exit;
  }

  $event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);

  http_response_code(204);
  ```

  ```javascript Node.js theme={null}
  import crypto from 'node:crypto';
  import express from 'express';

  const app = express();

  app.post(
    '/webhooks/digital-florists',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const secret = process.env.DF_WEBHOOK_SECRET;
      const header = req.header('Webhook-Signature') ?? '';
      const rawBody = req.body.toString('utf8');

      const parts = Object.fromEntries(
        header.split(',').map((part) => part.split('=', 2)),
      );

      const timestamp = parts.t;
      const signature = parts.v1;

      if (!timestamp || !signature) {
        return res.sendStatus(400);
      }

      const expected = crypto
        .createHmac('sha256', secret)
        .update(`${timestamp}.${rawBody}`, 'utf8')
        .digest('hex');

      const expectedBuffer = Buffer.from(expected, 'hex');
      const signatureBuffer = Buffer.from(signature, 'hex');

      if (
        expectedBuffer.length !== signatureBuffer.length ||
        !crypto.timingSafeEqual(expectedBuffer, signatureBuffer)
      ) {
        return res.sendStatus(400);
      }

      // Reject stale deliveries to prevent replay of a captured request.
      const toleranceSeconds = 300;
      const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));

      if (!Number.isFinite(ageSeconds) || ageSeconds > toleranceSeconds) {
        return res.sendStatus(400);
      }

      const event = JSON.parse(rawBody);

      return res.sendStatus(204);
    },
  );
  ```
</CodeGroup>

The signing secret is shown once when you create the subscription or rotate the secret. Store it securely; only the last four characters are shown afterwards.

## Idempotency and retries

Use the event `id` to deduplicate work. Delivery is at least once, so your endpoint may receive the same event more than once. Ordering is not guaranteed.

Failed deliveries are retried with increasing backoff for roughly 24 hours. Respond with a `2xx` status quickly and do heavier work asynchronously.

If an endpoint keeps rejecting deliveries or becomes unsafe to call, the subscription is disabled and `disabled_reason` is set. Fix the endpoint, then re-enable the subscription by updating it with `active: true`.

There is no self-serve replay endpoint. To recover missed events, contact the shop or support, and reconcile by polling list endpoints with `filter[updated_since]`.

## Managing subscriptions

With the webhooks scope you can create, list, update, and delete subscriptions, rotate each subscription's signing secret, and inspect delivery attempts. Each subscription has its own endpoint URL, event list, and signing secret.
