Webhooks.
Subscribe to inference events. We deliver signed JSON over HTTPS, retry with exponential backoff, and give you tools to inspect every delivery attempt.
§ 06.1Register an endpoint¶
Create a webhook endpoint via POST /v1/webhook-endpoints (scope webhooks: write).
curl -X POST https://api.demo.deeprelay.ai/v1/webhook-endpoints \
-H "Authorization: Bearer deeprelay_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"url": "https://hooks.acme.com/deeprelay",
"event_types": [
"video.completed",
"video.failed"
]
}'{
"id": "5f1b8a9c-2d3e-4a7b-9c1d-8e0f2a3b4c5d",
"url": "https://hooks.acme.com/deeprelay",
"secret": "f9c1a3...64hex...", // ← store this; never shown again
"event_types": ["video.completed", "video.failed"],
"enabled": true,
"created_at": "2026-05-08T17:00:00Z"
}Manage endpoints with GET /v1/webhook-endpoints (list) and DELETE /v1/webhook-endpoints/{id}. See the reference for the full CRUD surface.
§ 06.2Event types¶
v1 emits two video generation events. Subscribe to whichever subset you need; pass them in event_types at creation. There is no all shortcut: subscriptions are explicit, so an endpoint never silently starts receiving event types added later.
| Type | Fires when |
|---|---|
video.completed | A video generation job reached a terminal success state. The rendered result is ready to fetch and the job has been metered. |
video.failed | A video generation job reached a terminal failure state. Failed jobs are not billed. |
§ 06.3Delivery format¶
We POST a single JSON event to your URL, from the user agent Deeprelay-Webhooks/1. Each delivery carries two headers you care about:
Deeprelay-Signature: HMAC-SHA256 over<timestamp>.<body>. See verification below.Deeprelay-Event-Id: a stable ID per event. Use it to deduplicate when retries arrive after you've already processed the original.
POST https://hooks.acme.com/deeprelay
Deeprelay-Signature: t=1746732102,v1=5f1b8a9c0d1e2f3a4b5c6d7e8f9a0b1c...
Deeprelay-Event-Id: dc062a1e-cd94-4196-8c28-2e244d927f3f
User-Agent: Deeprelay-Webhooks/1
Content-Type: application/json
{
"id": "dc062a1e-cd94-4196-8c28-2e244d927f3f",
"type": "video.completed",
"created_at": "2026-05-08T17:01:30Z",
"data": {
"video": {
"id": "41ca1150-7121-47ce-9e44-66b63f6a7e47",
"object": "video",
"model": "deeprelay/wan-2.2-t2v",
"status": "completed"
}
}
}Respond with any 2xx status (we recommend 204 No Content) within 10 seconds. Anything else, or a timeout, triggers a retry (see below).
§ 06.4Verify signatures¶
Always verify the Deeprelay-Signature header before trusting the payload. The format is two comma-separated parts:
Deeprelay-Signature: t=<unix_seconds>,v1=<hex(hmac_sha256(secret, "<t>.<body>"))>The HMAC input is the literal string <timestamp>.<raw body bytes>. Make sure your framework gives you the unparsed body, not the JSON-decoded object.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.DEEPRELAY_WEBHOOK_SECRET; // stored at creation
// Capture the raw body so we can HMAC it byte-for-byte.
app.use(express.raw({ type: "application/json" }));
app.post("/deeprelay", (req, res) => {
const header = req.header("Deeprelay-Signature") ?? "";
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const t = Number(parts.t);
const v1 = parts.v1;
// 1. Replay protection: timestamp must be within ±5 minutes.
if (Math.abs(Date.now() / 1000 - t) > 300) {
return res.status(400).send("stale signature");
}
// 2. Recompute and compare in constant time.
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
if (
!crypto.timingSafeEqual(
Buffer.from(v1, "hex"),
Buffer.from(expected, "hex"),
)
) {
return res.status(400).send("bad signature");
}
// 3. Process the event.
const event = JSON.parse(req.body.toString());
console.log(event.type, event.data);
res.status(204).end();
});§ 06.5Retry policy¶
We retry any non-2xx response or timeout with exponential backoff: 5 attempts spaced 30s, 5m, 30m, 2h, 12h, so a delivery that never succeeds is given up on about 2.5 hours after the first try.
After the final attempt fails, the delivery is marked dead and stops retrying.
Because a dead delivery is not retried, treat repeated failures as something to fix on your side: return a 2xx as soon as you have durably accepted the event, and do the slow work afterwards rather than inside the 30-second per-attempt timeout.