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

# Webhooks

> Receive real-time notifications for order lifecycle events.

Crave sends webhook events to your server when key actions occur — such as an order being placed, confirmed, or completed. Use webhooks to trigger fulfillment workflows, send notifications, or sync data with external systems.

## How webhooks work

<Steps>
  <Step title="Configure a webhook URL">
    Register your endpoint in the Crave Dashboard under **Settings > Webhooks**.
  </Step>

  <Step title="Crave sends a POST request">
    When an event occurs, Crave sends a JSON payload to your URL.
  </Step>

  <Step title="Your server processes the event">
    Parse the payload, verify the signature, and execute your business logic.
  </Step>

  <Step title="Return a 200 response">
    Acknowledge receipt within 10 seconds. Crave retries on failure.
  </Step>
</Steps>

## Event types

| Event             | Trigger                                           |
| ----------------- | ------------------------------------------------- |
| `order.created`   | Payment succeeds and order is created             |
| `order.confirmed` | Restaurant confirms the order                     |
| `order.ready`     | Order is ready for pickup or delivery             |
| `order.completed` | Order is fulfilled                                |
| `order.cancelled` | Order is cancelled by restaurant or customer      |
| `cart.abandoned`  | Cart has been inactive for the configured timeout |

## Payload format

Every webhook delivery sends a `POST` request with a JSON body:

```json theme={null}
{
  "id": "evt_abc123",
  "type": "order.created",
  "createdAt": "2025-03-15T18:30:00Z",
  "data": {
    "orderId": "ord_456",
    "cartId": "cart_789",
    "locationId": "loc_123",
    "customerName": "Alex Johnson",
    "customerEmail": "alex@example.com",
    "total": "27.98",
    "currency": "usd",
    "fulfillmentMethod": "delivery",
    "items": [
      {
        "name": "Margherita Pizza",
        "quantity": 2,
        "total": "25.98"
      }
    ]
  }
}
```

## Verify webhook signatures

Every webhook includes a `X-Crave-Signature` header containing an HMAC-SHA256 signature. Verify it to ensure the request came from Crave.

```ts filename="app/api/webhooks/route.ts" theme={null}
import { createHmac } from 'crypto';

const WEBHOOK_SECRET = process.env.CRAVEUP_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('X-Crave-Signature');

  const expected = createHmac('sha256', WEBHOOK_SECRET)
    .update(body)
    .digest('hex');

  if (signature !== expected) {
    return new Response('Invalid signature', { status: 401 });
  }

  const event = JSON.parse(body);

  switch (event.type) {
    case 'order.created':
      await handleOrderCreated(event.data);
      break;
    case 'order.completed':
      await handleOrderCompleted(event.data);
      break;
  }

  return new Response('OK', { status: 200 });
}
```

<Warning>
  Always verify the signature before processing the event. Without verification, an attacker could send fake events to your endpoint.
</Warning>

## Retry policy

If your endpoint returns a non-2xx status or times out (10 seconds), Crave retries with exponential backoff:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes  |
| 3rd retry | 15 minutes |
| 4th retry | 1 hour     |
| 5th retry | 4 hours    |

After 5 failed attempts, the event is marked as failed. You can manually retry failed events from the Dashboard.

## Idempotency

Webhook deliveries may arrive more than once. Use the `id` field to deduplicate:

```ts theme={null}
async function handleOrderCreated(data: OrderData) {
  // Check if we already processed this event
  const existing = await db.processedEvents.findUnique({
    where: { eventId: data.orderId },
  });

  if (existing) return; // already processed

  await db.processedEvents.create({
    data: { eventId: data.orderId },
  });

  // Process the order...
}
```

## Common use cases

**Send an order confirmation email:**

```ts theme={null}
case 'order.created':
  await sendEmail({
    to: event.data.customerEmail,
    subject: `Order confirmed — ${event.data.locationName}`,
    body: `Your order of ${event.data.items.length} items is being prepared.`,
  });
  break;
```

**Sync with a POS system:**

```ts theme={null}
case 'order.created':
  await posClient.createOrder({
    externalId: event.data.orderId,
    items: event.data.items,
    total: event.data.total,
  });
  break;
```

**Track abandoned carts:**

```ts theme={null}
case 'cart.abandoned':
  await analytics.track('cart_abandoned', {
    cartId: event.data.cartId,
    total: event.data.total,
    itemCount: event.data.items.length,
  });
  break;
```

## Test webhooks locally

Use a tunnel service to expose your local server during development:

```bash theme={null}
# Using ngrok
ngrok http 3000

# Then set the webhook URL in the Dashboard to:
# https://abc123.ngrok.io/api/webhooks
```

<Tip>
  The Dashboard shows a delivery log for each webhook endpoint. Use it to inspect payloads and debug failed deliveries.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Testing" icon="flask-conical" href="/guides/testing">
    Test your webhook handlers with mock events.
  </Card>

  <Card title="Deployment" icon="rocket" href="/guides/deployment">
    Deploy your storefront and configure production webhooks.
  </Card>
</CardGroup>
