deeprelayDocs
UPDATED 2026.05.11READ 9 MINSUGGEST AN EDIT →
CH·06API

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"
    ]
  }'
201 CREATED
{
  "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.

TypeFires when
video.completedA video generation job reached a terminal success state. The rendered result is ready to fetch and the job has been metered.
video.failedA 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.
DELIVERY
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:

HEADER
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.