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

# Webhooks

> Signed events, verification and retries.

XFaster sends an HTTPS `POST` to your endpoint when something happens to one of the store's orders. **The webhook carries the full order** (`order`), the same object as [Get an order](/restaurant/orders/get) at the moment of the event, in your key's language. One call is enough: there is nothing to fetch.

| Event | When |
| - | - |
| `order.created` | A new order is waiting. Card orders only after the payment succeeded. |
| `order.updated` | Items or notes changed (edited while being prepared, or market picking finished). Fetch and reprint. |
| `order.cancelled` | Cancelled by the customer, support or the store. |
| `order.status_changed` | Accepted (also on the tablet), ready, picked up, delivered. |

```json Payload theme={null}
{
  "event": "order.created",
  "event_id": "evt_01JA7Q4M8V3K2B1X9ZC5N6P0RT",
  "occurred_at": "2026-10-14T19:30:02+03:00",
  "order_id": 183761,
  "store_id": 64,
  "url": "https://apps.xfaster.net/api/partner/v1/orders/183761",
  "order": {
    "id": 183761,
    "number": "183761",
    "type": "delivery",
    "status": "new",
    "items": [{ "line_id": 1, "external_id": "PIZZA-MARG", "name": "מרגריטה", "quantity": 1, "unit_price": 5200, "total": 5200, "choices": [], "addons": [] }],
    "totals": { "items": 5200, "discount": 0, "coupon": 0, "packaging": 0, "delivery_fee": 1500, "tip": 0, "total": 6700, "currency": "ILS" },
    "…": "every field of Get an order"
  }
}
```

* `store_id` is the store of the order. It tells your stores apart when you use an [integrator key](/basics/authentication#integrator-keys).
* `order` is `null` only if XFaster could not build it. Then fetch it by `url`.
* Headers you gave XFaster (for example your own `Authorization`) come with every webhook. See [POS-hosted mode](/restaurant/pos-hosted).

## Verify the signature

`X-XFaster-Signature: t=<unix seconds>,v1=<hex>` where `v1 = HMAC-SHA256(webhook_secret, t + "." + raw_body)`.

<CodeGroup>
  ```js Node.js theme={null}
  import crypto from 'node:crypto';

  function isValid(rawBody, header, secret) {
    const p = Object.fromEntries(header.split(',').map((x) => x.split('=')));
    const expected = crypto.createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest('hex');
    const fresh = Math.abs(Date.now() / 1000 - Number(p.t)) < 300;
    return fresh && expected.length === p.v1?.length
      && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
  }
  ```

  ```php PHP theme={null}
  function isValid(string $rawBody, string $header, string $secret): bool {
      parse_str(str_replace(',', '&', $header), $p);
      $expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
      return abs(time() - (int) $p['t']) < 300 && hash_equals($expected, $p['v1'] ?? '');
  }
  ```

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

  def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
      p = dict(x.split('=', 1) for x in header.split(','))
      expected = hmac.new(secret.encode(), f"{p['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
      return abs(time.time() - int(p['t'])) < 300 and hmac.compare_digest(expected, p.get('v1', ''))
  ```
</CodeGroup>

## Replying and retries

* Answer any `2xx` **within 5 seconds**, then do the work.
* Otherwise XFaster retries after about **10 s, 30 s, 2 min, 5 min and 15 min** (retries run once a minute), then stops.
* Webhooks can arrive **twice or out of order**: skip an `event_id` you already handled, and ignore an `order` whose `updated_at` is older than the one you have.
* Safety net: [List orders](/restaurant/orders/list) with `updated_since` every minute.
* The store tablet still rings for every new order, so a failed webhook never loses an order.


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