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

# Webhooks Quickstart

> Receive order lifecycle events and verify HMAC signatures.

Liquidramp sends outbound HTTP POST requests to your configured URL when order lifecycle events occur. Payloads use the summary order shape wrapped in an event envelope.

## Configure in the portal

Set your webhook URL and subscribed events in the [partner portal](https://dashboard.liquidramp.com).

## Event types

| Event                  | When it fires                         |
| ---------------------- | ------------------------------------- |
| `order.created`        | Order created, awaiting pay-in        |
| `order.assigned`       | Liquidity assigned to the order       |
| `order.asset_locked`   | Crypto locked in escrow               |
| `order.fiat_recieved`  | Fiat pay-in confirmed                 |
| `order.asset_released` | Crypto released to recipient          |
| `order.asset_refunded` | Crypto refunded on-chain              |
| `order.fiat_sent`      | Fiat payout initiated                 |
| `order.settled`        | Order fully settled                   |
| `order.cancelled`      | Order cancelled                       |
| `order.expired`        | Order expired (pay-in window elapsed) |

See [Callbacks](/concepts/callbacks).

## Payload shape

```json theme={null}
{
  "event": "order.created",
  "data": {
    "reference": "ORD-20260627-ABC123",
    "merchant_reference": null,
    "customer_reference": "cust-001",
    "type": "onramp",
    "from": { "currency": "NGN", "amount": 50000 },
    "to": { "currency": "USDT", "network": "BSC", "amount": 32.15 },
    "pricing": { "base_currency": "NGN", "quote_currency": "USDT", "exchange_rate": 1555.2 },
    "fee": { "currency": "USDT", "protocol_fee": 0.3, "partner_fee": 0, "total": 0.5 },
    "metadata": null,
    "amount_paid": 0,
    "amount_settled": 0,
    "amount_refunded": 0,
    "percentage_settled": 0,
    "status": "awaiting_user_payment",
    "created_at": "2026-06-27T12:00:00.000Z",
    "updated_at": "2026-06-27T12:00:00.000Z"
  }
}
```

Webhook `data` matches the list-order summary — it does not include `payin` or `payout`. Fetch [`GET /orders/:reference`](/api-reference/get-order) for full detail when needed.

```mermaid theme={null}
sequenceDiagram
    participant API as Liquidramp
    participant App as Your Server

    API->>App: POST your webhook URL
    Note over API,App: liquidramp-signature + liquidramp-timestamp
    App->>App: Verify HMAC with enc_* key
    App-->>API: 2xx (ack)
```

## Verify HMAC signatures

Every delivery includes:

| Header                 | Value             |
| ---------------------- | ----------------- |
| `liquidramp-signature` | `sha256=<hex>`    |
| `liquidramp-timestamp` | Unix milliseconds |

Build the canonical string (same algorithm as inbound API signing, but path is **your** endpoint path):

```
clientId|timestamp|POST|/webhooks/liquidramp|{"event":"order.created","data":{...}}
```

| Segment     | Rule                                                      |
| ----------- | --------------------------------------------------------- |
| `clientId`  | Your partner reference                                    |
| `timestamp` | Value of `liquidramp-timestamp` header                    |
| `POST`      | Always POST                                               |
| `path`      | URL pathname + query string of your webhook URL (no host) |
| body        | Raw POST body string                                      |

HMAC-SHA256 the canonical string with your `enc_*` encryption key. Compare to `liquidramp-signature` using a timing-safe comparison.

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

function verifyWebhook({ rawBody, signature, timestamp, clientId, encryptionKey, path }) {
  const canonical = [clientId, timestamp, "POST", path, rawBody].join("|");
  const expected = "sha256=" + crypto.createHmac("sha256", encryptionKey).update(canonical).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

<Warning>
  Read the raw request body before JSON parsing. Re-serializing parsed JSON can change key order and break verification.
</Warning>

Full guide: [Webhook verification](/guides/webhook-verification).

## Handler requirements

1. **Respond 2xx quickly** — process asynchronously if needed. Failed deliveries are retried with backoff.
2. **Verify signature before processing** — reject requests with invalid or stale timestamps.
3. **Handle duplicates** — use `data.reference` + `event` as an idempotency key.
4. **Fetch details on demand** — webhook payloads are summaries; call `GET /orders/:reference` (optionally `?include=payout`) for `payin` / `payout`.

## Related

* [Order lifecycle](/concepts/order-lifecycle)
* [Transaction statuses](/concepts/transaction-statuses)
