Webhooks push event notifications to your server so you don't need to poll.
Configure a webhook
Webhooks are configured on an API key, in API → API Keys in the dashboard. Set the key's webhook URL and select the events you want to receive. Editframe sends events for work done with that API key. Work done with other API keys or in the dashboard sends no events to it. The exception is a pipeline run started in the dashboard with that key chosen under Webhooks. See Dashboard.
Supported events
| Event | Fired when |
|---|---|
render.created | A render job is accepted and queued |
render.pending | A render job is waiting to start |
render.rendering | Frame capture has begun |
render.completed | A render job finishes successfully |
render.failed | A render job fails |
file.created | A file record is created |
file.uploading | File upload is in progress |
file.processing | File is being processed |
file.ready | File finishes processing successfully |
file.failed | File processing fails |
file.updated | A file's status changes to an unlisted value |
unprocessed_file.created | A raw upload is received |
Pipelines have their own topics. See Pipeline events.
Payload shape
All payloads include topic and data containing the relevant resource.
File events include expires_at (null for permanent uploads, or an ISO 8601 datetime when the file is scheduled to expire). See Files — retention.
Render events also include expires_at (null for permanent renders, or an ISO 8601 datetime when the render is scheduled for deletion). See Renders — retention and deletion.
Example for render.completed:
{
"topic": "render.completed",
"data": {
"id": "rnd_abc123",
"status": "completed",
"width": 1920,
"height": 1080,
"fps": 30,
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:30:00Z",
"failed_at": null,
"byte_size": 15728640,
"duration_ms": 5000,
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"metadata": null,
"download_url": "https://cdn.editframe.com/renders/rnd_abc123/output.mp4",
"expires_at": null
}
}Verifying signatures
Every webhook request includes an X-Webhook-Signature header. The value is an HMAC-SHA256 of the raw request body, hex-encoded, keyed with your webhook secret (shown in the Dashboard when you create the webhook).
import crypto from "crypto";
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
// Express example
app.post("/webhooks/editframe", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.headers["x-webhook-signature"];
const valid = verifyWebhook(req.body, sig, process.env.EDITFRAME_WEBHOOK_SECRET);
if (!valid) return res.status(401).send("Invalid signature");
const payload = JSON.parse(req.body.toString());
console.log(payload.topic, payload.data?.id);
res.sendStatus(200);
});Use express.raw() (not express.json()) to preserve the exact bytes used to compute the signature.
Retry behavior
Editframe retries webhook deliveries up to 3 times, 10 seconds apart, when your endpoint responds with a non-2xx status. Return 200 promptly and process the event asynchronously if needed.
Pipeline events
| Event | Fired when | data |
|---|---|---|
pipeline_run.created | A run is created (pending) | Run |
pipeline_run.started | A run starts its steps (running) | Run |
pipeline_run.retried | A failed or cancelled run is retried | Run |
pipeline_run.completed | A run succeeded | Run |
pipeline_run.failed | A run failed | Run |
pipeline_run.filtered | A run ended filtered: filters or reviews stopped every branch | Run |
pipeline_run.cancelled | A run was cancelled | Run |
pipeline_step.started | A step starts running | Step |
pipeline_step.progress | A segment step's watermark_ms advances, at most once every 10 seconds for each step | Step |
pipeline_step.waiting_external | Your endpoint answered 202 and the step waits for its callback, or a redact_render step started its render | Step |
pipeline_step.waiting_review | A review step opened a review task | Step |
pipeline_step.retrying | A step will run another attempt, after a retryable failure or a retry of its run | Step |
pipeline_step.completed | A step succeeded | Step |
pipeline_step.failed | A step failed | Step |
pipeline_step.skipped | A step was skipped | Step |
pipeline_step.cancelled | A step was cancelled | Step |
pipeline_review.decided | A review was approved or rejected | Review |
pipeline_review.expired | A review expired without a decision | Review |
pipeline_group.created | A group was created, open or closed | Group |
pipeline_group.closed | An open group was closed, and some of its runs have not finished | Group |
pipeline_group.completed | Every run of a closed group finished | Group |
pipeline_group.reopened | A retried run made a completed group closed again | Group |
file.receiving | A streamed upload was created and is receiving fragments | File |
file.sealed | A streamed upload was sealed. It replaces file.processing for streamed uploads. | File |
Editframe sends run, step and review events to the API key of the run, group events to the API key that created the group, and file events to the API key that created the file. The API key of a run is the key that created it, or the key chosen when the run was started in the dashboard. Each topic must be selected on the key. When pipelines are enabled for your organization, the API key form lists these topics under Pipeline events. Saving the form keeps any topics that it does not list.
Each status change sends at most one event:
- A step sends no event when it is created or becomes ready to run.
pipeline_step.startedtells you that it started. When no worker slot is free, a step can wait and then sendpipeline_step.startedagain for the sameattempt. pipeline_step.waiting_reviewannounces a new review task with itsreview_task_id. The review task sends an event only when it is decided or expires.- When you close a group whose runs all finished, the group sends only
pipeline_group.completed. Afterpipeline_group.reopened, the group sendspipeline_group.completedagain when the retried run finishes.
A run's page in the dashboard lists the webhook events about the run and its steps, with every delivery attempt and your endpoint's response.
Run, step and review events of one run share an increasing sequence. Deliveries can arrive out of order, so use sequence to order them. Timestamps are UTC with milliseconds. error is null or { "message", "code" }. metadata and run_metadata contain the metadata that the run or group was created with.
Run event:
{
"topic": "pipeline_run.filtered",
"data": {
"id": "3d0f6c1a-5b2e-4c7d-9f8a-1b2c3d4e5f62",
"pipeline_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"pipeline_version": 12,
"pipeline_digest": "sha256:c165d7ff2e3e8032f384efb8910f7c3fa2141ffdee4eb0f6f0fc6ea5358a9f27",
"group_id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"file_id": "7b8c9d0e-1f2a-4b3c-8d4e-5f6a7b8c9d10",
"external_id": "truck-0921/2026-10-01",
"metadata": { "customer": "acme" },
"status": "filtered",
"sequence": 14,
"created_at": "2026-10-02T03:00:00.000Z",
"completed_at": "2026-10-02T03:20:05.000Z",
"error": null
}
}Step event. watermark_ms is how far through the media a segment step's output is final, and review_task_id is set for review steps:
{
"topic": "pipeline_step.progress",
"data": {
"id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"pipeline_run_id": "3d0f6c1a-5b2e-4c7d-9f8a-1b2c3d4e5f60",
"run_metadata": { "customer": "acme" },
"step_key": "detector",
"namespace": "detector",
"type": "endpoint",
"status": "running",
"attempt": 1,
"sequence": 11,
"started_at": "2026-10-02T03:00:04.000Z",
"completed_at": null,
"watermark_ms": 1440000,
"review_task_id": null,
"error": null
}
}Review event. decision is null for an expired review. The event does not include the corrections of an approval; get the review with getPipelineReview to read them:
{
"topic": "pipeline_review.decided",
"data": {
"id": "8f9a0b1c-2d3e-4f5a-8b6c-7d8e9f0a1b2c",
"pipeline_run_id": "3d0f6c1a-5b2e-4c7d-9f8a-1b2c3d4e5f60",
"run_metadata": { "customer": "acme" },
"step_run_id": "d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f6a",
"step_key": "face_review",
"attempt": 1,
"status": "rejected",
"decision": {
"decision": "rejected",
"decided_by": { "type": "user", "id": "0a1b2c3d-4e5f-4a6b-8c7d-8e9f0a1b2c3d" },
"reason": "Not dashcam footage"
},
"sequence": 9,
"created_at": "2026-10-02T03:04:10.000Z",
"expires_at": "2026-10-03T03:04:10.000Z",
"decided_at": "2026-10-02T03:12:44.000Z"
}
}Group event. closed_at is set once the group is closed, completed_at while it is completed, and completion_count is the number of times that it completed:
{
"topic": "pipeline_group.completed",
"data": {
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"pipeline_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"pipeline_version": 12,
"external_id": "nightly-2026-10-01",
"metadata": { "night": "2026-10-01" },
"status": "completed",
"completion_count": 1,
"runs": { "total": 4000, "succeeded": 3921, "failed": 3, "filtered": 76, "cancelled": 0 },
"created_at": "2026-10-02T03:00:00.000Z",
"closed_at": "2026-10-02T03:00:00.000Z",
"completed_at": "2026-10-02T05:47:31.000Z"
}
}File events have the same data as the other file events: id, type, status (receiving or processing), filename, byte_size, md5, mime_type, width, height and expires_at.