Webhooks
Subscribe to events and receive signed HTTP POSTs at your own endpoints when things happen in your workspace. Manage endpoints in the dashboard under Settings → Developer / API → Webhooks.
Event catalog
| Event | When |
|---|---|
message.created | A message is sent or received (any channel) |
message.updated | A message is updated (e.g. delivery/read status) |
conversation.created | A new conversation is opened |
conversation.status_changed | A conversation is resolved/closed/snoozed/reopened |
contact.created | A contact is created |
contact.updated | A contact is updated |
Subscribe to specific events or to all events ("*").
Payload
Every delivery has this envelope (the data object varies by event type):
{
"id": "evt_0b9c...",
"type": "message.created",
"api_version": "2026-06-01",
"created_at": "2026-06-24T10:00:00Z",
"app_id": "66f1...",
"data": { "...": "the resource object" }
}
Headers
| Header | Description |
|---|---|
X-Relasi-Webhook-Id | Unique event id — use it to dedupe. |
X-Relasi-Timestamp | RFC3339 send time — reject if too old (replay guard). |
X-Relasi-Signature | v1=<base64 HMAC-SHA256> over "<id>.<unix_ts>.<body>". |
X-Relasi-Event | The event type, for quick routing. |
Verifying the signature
Always verify before trusting a payload. The signed content is
"<X-Relasi-Webhook-Id>.<unix_timestamp>.<raw_body>", keyed with the endpoint's
signing secret (shown in the dashboard). Use a constant-time comparison and
reject deliveries whose timestamp is more than ~5 minutes from now.
import crypto from "node:crypto";
// rawBody must be the exact bytes received (do not re-serialize).
function verifyRelasiWebhook(req, signingSecret) {
const id = req.headers["x-relasi-webhook-id"];
const tsHeader = req.headers["x-relasi-timestamp"];
const signature = req.headers["x-relasi-signature"]; // "v1=..."
const rawBody = req.rawBody;
// 1) Replay guard (5 min)
const ts = Math.floor(new Date(tsHeader).getTime() / 1000);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
// 2) Recompute signature
const signed = `${id}.${ts}.${rawBody}`;
const expected =
"v1=" +
crypto.createHmac("sha256", signingSecret).update(signed).digest("base64");
// 3) Constant-time compare
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Retries & reliability
- Non-2xx responses (or timeouts over 10s) are retried with exponential backoff:
~1m, 5m, 30m, 2h, 5h, 10h — up to 8 attempts, then marked
exhausted. - An endpoint that fails repeatedly is auto-disabled; re-enable it in the dashboard (which clears its failure count).
- Inspect every attempt under Details → Recent deliveries, and use Redeliver to retry a specific event.
- Endpoints should be idempotent: the same event may be delivered more than
once. Dedupe on
X-Relasi-Webhook-Id.