## Integrating Relay Webhooks

Each API key can have a single webhook endpoint configured. In order to configure a webhook you’ll need the following:

| **Field** | **Description** | **Example** |
| --- | --- | --- |
| API key | Any requests tied to this API key will post events to the webhook endpoint | `your-api-key` |
| Endpoint | An HTTPS endpoint to POST to when the status of a request changes | `https://my.server/receive-relay-event` |
| Custom Headers | Any custom headers you wish to receive | `{"Authorization": "Bearer 0x123", "X-Hookdeck-Source-Id": "src_123"}` |

Once you have this information reach out to Relay to configure your API key with a webhook endpoint.

## Webhook Events

Relay Webhooks specifically stream transaction statuses for both cross and same-chain status, though the stages differ slightly.

| **status** | **indication** |
| --- | --- |
| waiting | waiting for origin chain deposit |
| depositing | origin deposit confirmed via /execute API, fill pending |
| pending | origin chain deposit confirmed, fill pending submission |
| submitted | fill submitted on destination chain |
| success | fill succeeded |
| failure | fill failed |
| refund | refunded |

### Example Payload

```
{
  "event": "request.status.updated",
  "timestamp": 1774993296140,
  "data": {
    "status": "refund",
    "inTxHashes": [
      "0x..."
    ],
    "txHashes": [],
    "updatedAt": 1774993296121,
    "originChainId": 8453,
    "destinationChainId": 42161,
    "depositAddress": {
      "address": "0x..",
      "depositAddressType": "open", // open / strict
      "depositor": "0x..", //nullable
      "depositTxHash": "0x..", //nullable — origin-chain tx hash of the depositor's transfer into the deposit address
    }, //nullable
    "requestId": "0x...",
    "referrer": "your-referrer", //nullable — echoes the referrer supplied on the originating quote request
    "details": null,
    "failReason": "DOUBLE_SPEND",
    "refundFailReason": "N/A"
  }
}
```

The `referrer` field echoes the value supplied on the originating quote request, or `null` if no referrer was provided. Use it to attribute webhook events back to the request source (campaign, partner, or integration surface) without an extra lookup. The `details`, `failReason`, and `refundFailReason` fields mirror the same fields returned by [`GET /intents/status/v3`](https://docs.relay.link/references/api/get-intents-status-v3):

| **Field** | **Description** |
| --- | --- |
| `details` | Additional status context (e.g. decoded revert data on failures). `null` when no extra detail is available. |
| `failReason` | Reason a fill failed. Returns `"N/A"` on non-failure statuses. On `failure`, `fallback`, and `refund` statuses, returns the recorded reason or `"UNKNOWN"` when no specific reason is available. See [`/intents/status/v3`](https://docs.relay.link/references/api/get-intents-status-v3) for the enum. |
| `refundFailReason` | Reason a refund leg failed. Defaults to `"N/A"` when no refund-leg failure is recorded. See [`/intents/status/v3`](https://docs.relay.link/references/api/get-intents-status-v3) for the enum. |

By default we post all status updates. If you’re only interested in certain statuses you can filter on the `status` property in your endpoint.

## Verification

Each webhook request includes two headers for verification:

| **Header** | **Description** |
| --- | --- |
| `X-Signature-Timestamp` | Unix timestamp of when the webhook was sent |
| `X-Signature-SHA256` | HMAC-SHA256 signature of the request |

To verify a webhook is authentic, compute the HMAC-SHA256 hash of `${timestamp}.${body}` using your API key as the secret, and compare it to the `X-Signature-SHA256` header value.

Example Verification

```
import crypto from 'crypto';

function verifyWebhook(req, apiKey) {
  const timestamp = req.headers['x-signature-timestamp'];
  const signature = req.headers['x-signature-sha256'];
  const body = JSON.stringify(req.body);

const expected = crypto
    .createHmac('sha256', apiKey)
    .update(`${timestamp}.${body}`)
    .digest('hex');

const signatureBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expected, 'hex');

return (
    signatureBuffer.length === expectedBuffer.length &&
    crypto.timingSafeEqual(signatureBuffer, expectedBuffer)
  );
}
```

Requests that fail verification should be rejected with a non-2xx status code.

## Webhook Delivery & Reliability

Relay’s built-in webhook service includes up to 10 retries with exponential backoff. For most integrations, this is sufficient. For production deployments requiring guaranteed delivery, routing, fan-out, or detailed observability, we recommend using a webhook gateway like [Hookdeck](https://hookdeck.com/). A gateway sits between Relay and your server, giving you:

- **Guaranteed delivery** with configurable retry policies
- **Routing & fan-out** to multiple destinations from a single source
- **Event inspection & replay** for debugging
- **Rate limiting** to match your server’s capacity

To set this up, use your Hookdeck ingestion URL as the Relay webhook `endpoint` and route events to your server from there. See [Hookdeck’s getting started guide](https://hookdeck.com/docs) for setup instructions.
