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

# Delivery and retries

> Timeouts, retries, idempotency, and what happens when your endpoint is down.

## The contract

|                    |                                      |
| ------------------ | ------------------------------------ |
| Method             | `POST`                               |
| Timeout            | **10 seconds**                       |
| Success            | Any 2xx                              |
| Retry on           | Non-2xx, timeout, connection failure |
| Delivery guarantee | At least once                        |

<Warning>
  **At least once**, not exactly once. You will occasionally receive the same event twice. Idempotency is your responsibility.
</Warning>

## Acknowledge fast

Ten seconds is the whole budget. Do the minimum synchronously and queue the rest.

```javascript theme={null}
app.post("/webhooks/cryptocheckout", express.raw({type:"application/json"}), async (req, res) => {
  if (!verify(req)) return res.status(400).send("bad signature");

  await queue.push(req.body);   // fast
  res.sendStatus(200);          // acknowledge

  // Slow work happens in the worker, not here
});
```

Anti-pattern to avoid: calling a payment provider, generating a PDF, or sending an email inline. Any of those can exceed the timeout, which turns a successful delivery into a retry and a duplicate.

## Idempotency

Every delivery carries a stable `X-Webhook-Id`, which also appears as `id` in the body. Retries of the same event reuse it.

```javascript theme={null}
const seen = await db.webhookEvents.findUnique({ where: { id: event.id } });
if (seen) return res.sendStatus(200);       // already handled

await db.$transaction([
  db.webhookEvents.create({ data: { id: event.id } }),
  db.orders.update({ where: { id: event.data.orderId }, data: { status: "paid" } }),
]);
```

<Tip>
  Record the ID and do the work in the **same transaction**. Otherwise a crash between them leaves you having recorded an event you never acted on.
</Tip>

## When your endpoint is down

Failed deliveries are retried with backoff. If you're down for a deploy, deliveries resume once you're back.

```mermaid theme={null}
flowchart LR
    A[Event] --> B[Deliver]
    B -->|2xx| C[Done]
    B -->|Fail| D[Retry with backoff]
    D --> B
    style C fill:#064e3b,stroke:#10B981,color:#fff
```

<Info>
  Money is never at risk here. Webhooks are notifications, not settlement. A payment we failed to tell you about still arrived in your pool, and still appears in **Payments** and the status API. The worst case is delayed fulfilment, not lost funds.
</Info>

## Reconciliation

Don't rely on webhooks alone. Two backstops worth building:

<Columns cols={2}>
  <Card title="Poll for stragglers" icon="rotate">
    For orders still pending after a sensible window, call the [status API](/api/payment-status).
  </Card>

  <Card title="Reconcile periodically" icon="scale-balanced">
    Compare your paid orders against the dashboard's ledger. Any gap is a webhook you missed.
  </Card>
</Columns>

## Debugging

The dashboard's **Webhooks** tab shows recent deliveries with response codes and bodies. Start there — most failures are a signature mismatch caused by a parsed body, or a timeout from inline work.

<Card title="Signature verification" icon="signature" href="/webhooks/signatures" horizontal>
  The most common source of rejected deliveries.
</Card>
