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

# Outbound Webhooks

> Receive signed, versioned Crave commerce events with durable delivery evidence.

Crave sends merchant events to server-side HTTPS endpoints configured in **Business Manager →
Developers → Webhooks**. Sandbox and production are separate deployments: create and verify an
endpoint in each environment that you use.

## Delivery contract

Every request is an HTTP `POST` whose exact UTF-8 body is a versioned event envelope:

```json theme={null}
{
  "id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
  "type": "order.created",
  "version": "2026-08-27",
  "deployment": "sandbox",
  "organizationId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
  "occurredAt": "2026-08-26T12:00:00.000Z",
  "test": false,
  "data": {
    "amount": "42.50",
    "cartId": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
    "currency": "USD",
    "locationId": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
    "orderId": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
    "status": "pending"
  }
}
```

Crave includes these headers:

| Header | Meaning |
| - | - |
| `Crave-Webhook-Id` | Immutable event ID; use it as your deduplication key. |
| `Crave-Webhook-Timestamp` | Unix seconds used in the signature. |
| `Crave-Webhook-Signature` | One or more comma-separated `v1=` HMAC signatures. |
| `Crave-Webhook-Event` | Event type. |
| `Crave-Webhook-Version` | Payload schema version. |
| `Crave-Webhook-Test` | `true` only for an explicit dashboard test event. |

## Verify signatures

Store the endpoint secret only in your server-side secret manager. Verify the signature against the
**raw request body before JSON parsing**. The signed input is
`<timestamp>.<exact raw body>`, using HMAC-SHA256 and unpadded base64url output.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyCraveWebhook({
  rawBody,
  signatureHeader,
  timestampHeader,
  secret,
}: {
  rawBody: Buffer;
  signatureHeader: string;
  timestampHeader: string;
  secret: string;
}) {
  const timestamp = Number(timestampHeader);
  const now = Math.floor(Date.now() / 1000);
  if (!Number.isSafeInteger(timestamp) || Math.abs(now - timestamp) > 300) {
    return false;
  }

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("base64url");

  return signatureHeader.split(",").some((candidate) => {
    const [version, encoded] = candidate.trim().split("=", 2);
    if (version !== "v1" || !encoded) return false;
    const left = Buffer.from(encoded);
    const right = Buffer.from(expected);
    return left.length === right.length && timingSafeEqual(left, right);
  });
}
```

Reject timestamps outside the 300-second tolerance. During a bounded secret-rotation overlap,
Crave signs with both the current and previous secret, so a receiver deploying either valid secret
continues to verify requests. After the new secret is deployed, remove the previous secret from the
receiver.

## Event catalog

The `2026-08-27` registry defines:

* `order.created`
* `order.status_changed`
* `order.cancelled`
* `order.refunded`
* `settlement.updated`
* `payment.succeeded`
* `payment.failed`
* `catalog.published`
* `catalog.availability_changed`
* `location.updated`
* `customer.verified`
* `catering.event.updated`

`catering.event.updated` carries only `eventId`, `locationId`, the six-digit display
`reference`, `change`, and `status`. It never carries customer contact details, quote
lines or prices, dietary/access notes, or customer-link credentials. Quote negotiation
emits `quote_sent`, restaurant `declined`, and `expired`; the shared contract also
reserves `accepted` and `payment_received` for the customer acceptance/payment lane.
The display reference is not an authentication credential. Duplicate deliveries retain
the same event ID.

`settlement.updated` carries the order, fulfillment, settlement, refund, and reconciliation
revisions side by side under `revisions`, alongside the `orderId` and the `cartId` the order was
submitted from. Each revision advances independently, so reconcile the concern you care about
against its own revision rather than inferring one from another. A pay-at-store order
emits it while the settlement is still `due`, again as tender evidence arrives, and again for
post-close recovery.

Order lifecycle states are carried by `order.status_changed`; there are no separate
`order.confirmed`, `order.ready`, or `order.completed` event types. `cart.abandoned` is not part of
the outbound catalog. High-frequency shared-cart updates use the assisted-ordering realtime
contract, not merchant webhooks.

Order, payment, and settlement envelopes carry `cartId` so an assisted-ordering consumer can
reconcile the exact canonical cart without scanning active sessions. It is `null` only when the order originated
outside a Crave storefront cart, such as a provider-native POS order; consumers must not infer a
cart in that case.

For a cart-correlated event, use the signed `locationId` and `cartId` only as lookup coordinates,
then read the canonical machine projection from
`GET /api/v1/locations/:locationId/carts/:cartId/commerce-projection`. This server-to-server
route requires an API key with `orders:read` and access to that location. It remains available after
the guest cart capability expires, also covers `payment.failed` events that do not carry an order ID,
and returns only the correlated cart ID, checkout attempt state, settlement state, reconciliation
state, and order status:

The generated [Integration OpenAPI specification](/docs/generated/integration.openapi.json)
is also served by each API deployment at `/integration-swagger.json`. Its base is
`/api/v1`, **not** `/api/v1/storefront`. Keep the organization API key on your
server; a diner capability or merchant session is not a replacement. Private API
errors use `success`, `message`, and an optional `code`/`data`, not the Storefront
error envelope. Treat the projection as a reconciliation read, never permission
to retry an uncertain checkout.

Baseline middleware can reject before the handler: `429` returns `{status,
message}` and a limiter-store outage returns `503` with `{status, code, message}`
and `Retry-After: 1`. These responses do not carry `success`; the generated
specification documents them separately from private handler errors.

```json theme={null}
{
  "cartId": "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
  "checkout": {
    "attemptId": "ffffffff-ffff-4fff-8fff-ffffffffffff",
    "collectionMode": "ONLINE",
    "status": "SUCCEEDED"
  },
  "payment": { "status": "PAID", "reconciliationStatus": "CURRENT" },
  "order": {
    "id": "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
    "status": "ACCEPTED"
  }
}
```

Treat the webhook body as a hint. Confirm that the response `cartId` matches the event before
updating local projections. The route is `private, no-store`; it never returns customer data,
line items, raw provider evidence, or reusable cart credentials.

> [!NOTE]
>
> Every event in the registry is emitted except `catalog.availability_changed`
> and `location.updated`, which remain registry-reserved. `order.created` is
> emitted in the same transaction that submits a storefront cart as an order,
> `payment.succeeded` when that order's online payment is captured, and
> `settlement.updated` for every settlement transition in every deployment.
> Subscribing to a reserved type is safe, but do not build a flow that assumes
> an event whose producer has not shipped.

## Acknowledge and deduplicate

1. Preserve the raw body and verify the timestamp and signature.
2. Atomically record `Crave-Webhook-Id` in your database.
3. If that ID already exists, return `2xx` without repeating the business action.
4. Queue your own processing and return `2xx` promptly.

Delivery is **at least once**. Requests for different events may arrive out of order, and a timeout
can occur after your server commits. Use the event ID for deduplication and use the Crave REST API
to reconcile current resource state when the final outcome is ambiguous.

## Retries and terminal failures

Crave retries network errors, `408`, `409`, `425`, `429`, and `5xx` responses. Each automatic cycle
allows up to 12 attempts, starts with a 30-second exponential-backoff window, applies full jitter,
and caps the window at 6 hours. A valid `Retry-After` is honored up to 1 hour. Connections and
responses each have a 10-second timeout. Other `4xx` responses are terminal.

Business Manager records each attempt's status, duration, safe error code, allowlisted response
headers, and at most 4 KB of response text. An organization administrator or authorized Crave
support operator can manually redeliver a delivered or terminal request. Manual redelivery keeps
the original event ID and exact body, records an audit reason, and opens one fresh bounded attempt
cycle; it never creates a new business event.

## Endpoint security

* Endpoints must use public HTTPS on port 443.
* Credentials in URLs, fragments, redirects, private/loopback/link-local destinations, and empty DNS
  answers are rejected.
* DNS is revalidated immediately before every connection, and the connection is pinned to the
  validated address set to prevent DNS rebinding.
* Signing secrets are encrypted at rest and shown only when an endpoint is created or rotated.
* Disabling an endpoint terminalizes its queued deliveries; re-enable it before requesting a
  redelivery.

See the delivery timeline in Business Manager when troubleshooting. Crave staff use an audited
cross-organization health view for queue age, failing endpoints, and terminal deliveries.

## Validating payloads

Crave may add fields to an event payload within a contract version. If your handler validates with a
strict schema — one that rejects unknown keys — an added field makes your endpoint answer `400`, and
a `400` is terminal: the delivery is dropped rather than retried, and no alert fires on our side
because the request completed normally.

Accept unknown fields, or track the registry and deploy your schema change before ours lands.

If you correlate order and settlement events, [Assisted Ordering](/docs/guides/assisted-ordering.md)
documents which identifier each event is correlated by.


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