> ## 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.

# Webhooks

> The reliable way to know a payment happened. Set these up before you go live.

Webhooks are how your server learns that money arrived. They're the only signal you should fulfil orders from.

<Danger>
  Browser callbacks fire only if the customer's tab is still open. Webhooks arrive regardless. **Fulfil from webhooks.**
</Danger>

## Set up

<Steps>
  <Step title="Build an endpoint" icon="server">
    A public HTTPS URL that accepts POST and responds 2xx quickly.
  </Step>

  <Step title="Register it" icon="link">
    **Settings → Webhooks** in the dashboard. Add the URL and copy the signing secret.
  </Step>

  <Step title="Verify every delivery" icon="signature">
    Reject anything that fails. [Signatures](/webhooks/signatures).
  </Step>

  <Step title="Test it" icon="flask">
    Use **Send test event**, then make a real testnet payment.
  </Step>
</Steps>

<Warning>
  Localhost, private IP ranges, and cloud metadata addresses are rejected. Use a tunnel like ngrok for local development.
</Warning>

## The payload

Every webhook has the same envelope:

```json theme={null}
{
  "id": "payment_confirmed_a1b2c3d4",
  "event": "payment_confirmed",
  "created_at": "2026-08-09T14:32:11.204Z",
  "data": {
    "…": "event-specific fields"
  }
}
```

<ResponseField name="id" type="string">
  Stable per transition. Use it as your idempotency key.
</ResponseField>

<ResponseField name="event" type="string">
  The event type. [Full list](/webhooks/events).
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp.
</ResponseField>

<ResponseField name="data" type="object">
  Event-specific payload.
</ResponseField>

## Headers

| Header                | Purpose                                   |
| --------------------- | ----------------------------------------- |
| `X-Webhook-Signature` | `t=<timestamp>,v1=<hmac>` — verify this   |
| `X-Webhook-Id`        | Idempotency key, matches `id` in the body |
| `X-Webhook-Timestamp` | Unix seconds, same as `t`                 |
| `User-Agent`          | `CryptoCheckout-Webhook/1.0`              |

## A correct handler

```javascript theme={null}
import crypto from "crypto";

app.post("/webhooks/cryptocheckout",
  express.raw({ type: "application/json" }),   // raw body, not parsed
  async (req, res) => {
    // 1. Verify before anything else
    if (!verifySignature(req)) return res.status(400).send("bad signature");

    const event = JSON.parse(req.body);

    // 2. Idempotency — you will see duplicates
    if (await alreadyProcessed(event.id)) return res.sendStatus(200);

    // 3. Acknowledge fast, work later
    await enqueue(event);
    res.sendStatus(200);
  });
```

Three rules in that snippet, all of which matter:

<Columns cols={3}>
  <Card title="Raw body" icon="file-code">
    Parsing and re-serialising changes the bytes and breaks the signature.
  </Card>

  <Card title="Verify first" icon="shield">
    Before parsing, before touching your database.
  </Card>

  <Card title="Acknowledge fast" icon="bolt">
    We time out at 10 seconds. Do slow work in a queue.
  </Card>
</Columns>

<Card title="Signature verification in detail" icon="signature" href="/webhooks/signatures" horizontal>
  With replay protection and language examples.
</Card>
