Delivery guide
Receive events reliably
Validate every webhook, acknowledge it quickly, and process repeated deliveries without creating duplicate work.
Receive a webhook
Relayline sends the event as a JSON POST request. Your endpoint must use HTTPS and return a 2xx response within 10 seconds.
{
"id": "evt_01J6Y5T4M8N2K7P9Q3R1",
"type": "order.created",
"delivery_id": "dlv_01J6Y3G8V9K2M4P7Q1R5",
"created_at": "2026-09-04T14:32:18Z",
"data": {
"order_id": "ord_7M2L9",
"total": 4800,
"currency": "USD"
}
}| Header | Description |
|---|---|
Content-Type | application/json |
Relayline-Signature | Signing timestamp and HMAC signature in the form t=timestamp,v1=signature. |
Relayline-Delivery-Id | The delivery identifier used to trace the request. |
Relayline-Attempt | The attempt number, beginning at 1. |
Use the event id as an idempotency key in your own application. A retried delivery keeps the same event and delivery identifiers.
Verify the signature
Each request includes a Unix timestamp in seconds and a hexadecimal HMAC-SHA256 signature in Relayline-Signature. The signed message is the timestamp, a period, and the original request-body bytes. Pass those bytes as a Buffer to the verifier below, using the server-side webhook signing secret. Parse and validate JSON only after verification.
import { createHmac, timingSafeEqual } from 'node:crypto';
/** Verify raw bytes before parsing JSON. A timestamp check does not replace deduplication. */
export function verifyRelaylineSignature(rawBody, header, secret, now = Date.now()) {
if (!Buffer.isBuffer(rawBody) || typeof header !== 'string' || typeof secret !== 'string' || !secret) return false;
if (!Number.isFinite(now)) return false;
const match = /^t=(0|[1-9]\d*),v1=([a-f0-9]{64})$/i.exec(header);
if (!match) return false;
const timestamp = Number(match[1]);
if (!Number.isSafeInteger(timestamp) || Math.abs(Math.floor(now / 1000) - timestamp) > 300) return false;
const expected = createHmac('sha256', secret).update(`${match[1]}.`).update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}
Acknowledge before processing
Return a successful response after validating and storing the event. Perform slower work asynchronously so the request does not exceed the 10-second timeout.
Return 204 No Content as soon as the event is safely queued in your application.
Retry schedule
Relayline retries when the destination times out, cannot be reached, or returns a final HTTP status outside 200–299.
| Attempt | Approximate delay after previous failure | Elapsed time |
|---|---|---|
| 1 | Immediate | 0 minutes |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 30 minutes | 36 minutes |
| 5 | 2 hours | Approximately 2 hours 36 minutes |
Elapsed times are approximate and exclude time spent making requests and waiting for timeouts.
After the fifth unsuccessful attempt, the delivery changes to failed. Retrieve the delivery to inspect its final status.
For a complete example, follow one delivery through acceptance, a destination failure, and recovery.
Production handler checklist
- Verify the signature before trusting the body.
- Reject malformed requests with a
4xxresponse. - Store incoming events durably with atomic deduplication, and make worker side effects idempotent.
- Queue expensive work before acknowledging the request.
- Log the delivery ID so support investigations can trace an event.