Webhooks
Configure HTTPS endpoints in the Developer console to receive signed run lifecycle events.
Developers receiving terminal run notifications.
Configure HTTPS endpoints in the Developer console to receive signed run lifecycle events. Webhooks complement polling: use them when you need push notification instead of a poll loop.
Subscribable events#
run.completed: dispatched when a run succeeds.run.failed: dispatched when a run fails.run.cancelled: dispatched when a run is cancelled.
Verification#
Each delivery includes edgaze-signature: t=<unix>,v1=<hmac> and edgaze-webhook-id. The signed payload is {timestamp}.{raw_json_body} using HMAC-SHA256 with your endpoint secret. Reject signatures older than five minutes to prevent replay.
Verify the signature against the exact bytes received, before parsing or re-serializing the JSON:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyEdgazeWebhook(rawBody, signatureHeader, secret) {
const fields = Object.fromEntries(
signatureHeader.split(",").map((field) => {
const separator = field.indexOf("=");
return [field.slice(0, separator).trim(), field.slice(separator + 1).trim()];
}),
);
const timestamp = Number(fields.t);
if (!Number.isFinite(timestamp) || !fields.v1) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = Buffer.from(
createHmac("sha256", secret).update(`${timestamp}.${rawBody}`, "utf8").digest("hex"),
"hex",
);
const actual = Buffer.from(fields.v1, "hex");
return actual.length === expected.length && timingSafeEqual(actual, expected);
}In a Next.js Route Handler, obtain rawBody with await request.text() and read the header with request.headers.get("edgaze-signature"). Return a non-2xx response when the header is missing or verification fails.
Delivery guarantees#
- Every subscribed endpoint gets its own queued delivery. The first attempt is made as soon as the run settles.
- A failed attempt is retried automatically: up to six attempts per delivery, spaced roughly 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours apart, with jitter.
- Retried failures are: any 5xx,
408,425,429, a connection error, and a timeout (an attempt gets five seconds to respond). - Not retried: any other 4xx, and a redirect. Those mean the endpoint rejected or is misconfigured, so the delivery is marked failed immediately.
- When a receiver returns
Retry-After, a longer wait is honored. A shorter one is ignored. - Delivery is at-least-once, so the same event can arrive more than once. Deduplicate on
edgaze-delivery-id. - Endpoints are called in parallel, and retries are interleaved with new events. Ordering is not guaranteed.
- Webhook failures never block run completion: your handler must be idempotent.
- After 15 consecutive deliveries that exhaust every attempt, the endpoint is paused and stops receiving events. The Developer console shows why and re-enables it in one click.
- Use test delivery and delivery logs in the Developer console to debug signature or payload issues. A test delivery is a single attempt and is never retried.
Retries make a missed notification unlikely, not impossible: an endpoint that stays down for more than about nine hours exhausts the delivery. If missing a terminal run would cause a correctness or billing problem on your side, reconcile on a schedule with List Runs filtered by status, and use the run id to deduplicate against what you already processed.
A run cancelled through Cancel Run dispatches run.cancelled the same way, and is never charged.
Delivery headers#
| Header | Meaning |
|---|---|
edgaze-signature | t=<unix>,v1=<hmac> over {timestamp}.{raw body}. |
edgaze-webhook-id | The endpoint this delivery was sent to. Use it to select the right secret. |
edgaze-delivery-id | Stable id for this delivery. Every retry repeats it, so it is your deduplication key. |
edgaze-attempt | 1-indexed attempt number. Anything above 1 means an earlier attempt did not succeed. |
edgaze-event | The event type, so you can route before parsing the body. |
Retries send byte-identical bodies, so the signature verifies the same way on every attempt. Only the timestamp and signature change, because each attempt is signed when it is sent.
Store the delivery id you have already processed and return 200 without reprocessing when you see it again:
export async function POST(request) {
const deliveryId = request.headers.get("edgaze-delivery-id");
if (deliveryId && (await alreadyProcessed(deliveryId))) {
return new Response(null, { status: 200 });
}
// Verify the signature, persist the payload keyed by deliveryId, then acknowledge.
return new Response(null, { status: 200 });
}Payload reference#
{
"type": "run.completed",
"runId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "completed",
"reason": null,
"runUrl": "https://api.edgaze.ai/v1/runs/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"occurredAt": "2026-07-08T10:00:42.000Z"
}Was this useful?
Your response helps us improve the documentation.