Webhooks

Receive notifications when renders complete and files finish processing.

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

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:

json
{
  "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

Always verify the signature before processing webhook payloads.

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

js
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

Editframe enables pipelines for each organization after you tell us about your footage. Pipeline events are sent only to organizations with pipelines enabled.

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.started tells you that it started. When no worker slot is free, a step can wait and then send pipeline_step.started again for the same attempt.
  • pipeline_step.waiting_review announces a new review task with its review_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. After pipeline_group.reopened, the group sends pipeline_group.completed again 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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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.