About webhooks
A guide and code pattern for receiving Editframe webhook notifications in server applications. It covers signature verification, durable event acceptance, duplicate handling, and render, file, and pipeline event payloads.
Key features
- 01Configure HTTPS webhook URLs and event topics
- 02Verify HMAC-SHA256 signatures on raw bodies
- 03Persist accepted events before responding
- 04Handle duplicate and out-of-order deliveries
- 05Document render, file, and pipeline topics
Use cases
- 01Receiving Editframe render status updates
- 02Processing file lifecycle notifications
- 03Tracking pipeline runs, steps, and reviews
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ editframe/ skills --skill webhooksAfter installing, type/webhooksin Claude Code to confirm it loaded.
Details
From the project READMEWebhooks
Use webhooks when a server needs notifications independent of an active client connection.
For a live progress display or a one-off render, use getRenderProgress or getFileProcessingProgress. These functions consume event streams.
No SDK function registers a webhook. Configure one on an API key, through the dashboard (API → API Keys, or editframe.com/resource/api_keys). Set a Webhook URL (this must use HTTPS). Select which Webhook Events (topics) to receive. When you create or update the key, the dashboard generates a Webhook Secret, used to sign deliveries. Copy this secret and store it alongside the API key.
Handling a Webhook
Verify the signature and validate the payload before acceptance. Persist the event before you return success.
The example accepts an application-defined acceptEvent function. It must commit to a durable inbox or queue before its promise resolves.
import express from "express";
import crypto from "node:crypto";
type WebhookEvent = { topic: string; data: Record<string, unknown> };
type AcceptEvent = (event: WebhookEvent, fingerprint: string) => Promise<void>;
export function createWebhookRouter(secret: string, acceptEvent: AcceptEvent) {
if (!secret) throw new Error("Webhook secret is required");
const router = express.Router();
router.post("/webhooks/editframe", express.raw({ type: "*/*" }), async (req, res) => {
const signature = req.get("x-webhook-signature");
const rawBody = req.body;
if (!signature || !/^[a-f0-9]{64}$/i.test(signature)) {
return res.status(401).send("Invalid signature");
}
if (!Buffer.isBuffer(rawBody)) {
return res.status(400).send("Raw body required");
}
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest();
if (!crypto.timingSafeEqual(Buffer.from(signature, "hex"), expected)) {
return res.status(401).send("Invalid signature");
}
let payload: WebhookEvent;
try {
const value = JSON.parse(rawBody.toString("utf-8"));
if (!value || typeof value.topic !== "string" || !value.data ||
typeof value.data !== "object" || Array.isArray(value.data)) {
return res.status(400).send("Invalid payload");
}
payload = value;
} catch {
return res.status(400).send("Invalid JSON");
}
const fingerprint = crypto.createHash("sha256").update(rawBody).digest("hex");
try {
await acceptEvent(payload, fingerprint);
return res.status(200).send("OK");
} catch {
return res.status(500).send("Event not accepted");
}
});
return router;
}
Mount this router before middleware that parses request bodies, such as express.json(). That middleware would consume the signed bytes.
Keep durable acceptance brief; the delivery HTTP timeout is 30 seconds. Let a worker process the stored event afterward.
Return success for an already accepted duplicate. Record storage failures in your application logs without exposing the secret or signed body.
Every request carries X-Webhook-Signature: the hex-encoded HMAC-SHA256 of the raw JSON body. Re-serialized JSON can produce a different signature.
Payload
{ topic: string, data: {...} }
Render topics: render.created, render.pending, render.rendering, render.completed, render.failed
data includes id, status, created_at, completed_at, failed_at, width, height, fps, byte_size, duration_ms, md5, metadata, expires_at (null = permanent), download_url (populated once complete), error (populated on failure).
File topics: file.created, file.uploading, file.processing, file.ready, file.failed, file.updated
data includes id, type (video/image/caption), status, filename, byte_size, md5, mime_type, width, height, expires_at. Editframe sends file.updated for a file status change that doesn't match one of the other file topics.
Pipeline topics
Editframe enables pipelines for each organization. Editframe sends pipeline topics only to organizations with pipelines enabled. To get pipelines, tell Editframe about your footage at https://editframe.com/factory#contact.
| Topic | Editframe sends it 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, pipeline_run.filtered, pipeline_run.cancelled | A run ends with that status. | Run |
pipeline_step.started | A step starts. | Step |
pipeline_step.progress | The watermark_ms of a segment step increases. At most one event for each step every 10 seconds. | Step |
pipeline_step.waiting_external | An endpoint answered 202, or a redact_render step started its render. The step waits for the callback or the render. | Step |
pipeline_step.waiting_review | A review step opens a review task. | Step |
pipeline_step.retrying | A step will start a new attempt, after a retryable failure or a retry of the run. | Step |
pipeline_step.completed, pipeline_step.failed, pipeline_step.skipped, pipeline_step.cancelled | A step succeeds, fails, skips or is cancelled. | Step |
pipeline_review.decided | A person or an API key approves or rejects a review. | Review |
pipeline_review.expired | A review expires without a decision. | Review |
pipeline_group.created | A group is created, open or closed. | Group |
pipeline_group.closed | An open group is closed, and some of its runs did not finish. | Group |
pipeline_group.completed | All runs of a closed group finished. | Group |
pipeline_group.reopened | A retried run makes a completed group closed again. | Group |
file.receiving | A streamed upload starts. | File |
file.sealed | A streamed upload is sealed. This topic replaces file.processing for streamed uploads. | File |
- Run
data:id,pipeline_id,pipeline_version,pipeline_digest,group_id,file_id,external_id,metadata,status,sequence,created_at,completed_at,error. - Step
data:id,pipeline_run_id,run_metadata,step_key,namespace,type,status,attempt,sequence,started_at,completed_at,watermark_ms,review_task_id,error. - Review
data:id,pipeline_run_id,run_metadata,step_run_id,step_key,attempt,status(approved,rejectedorexpired),decision({ decision, decided_by: { type, id }, reason }, ornullafter expiry),sequence,created_at,expires_at,decided_at. The event does not contain the corrections of an approval. Get the review withgetPipelineReviewto read them. - Group
data:id,pipeline_id,pipeline_version,external_id,metadata,status,completion_count,runs(total,succeeded,failed,filtered,cancelled),created_at,closed_at,completed_at. metadataandrun_metadatacontain themetadataof the run or group when it was created.- A run ends
filteredonly when filters or reviews stop all branches. A run with a stopped branch and a successful final step sendspipeline_run.completed. Each skipped step sendspipeline_step.skipped. - File
datahas the same fields as the other file topics. errorisnullor{ message, code }.
Obey these rules:
- Select each pipeline topic on the API key. Editframe sends no topic that the key does not list.
- Create runs and groups with that API key. Editframe sends run, step and review topics only to the API key of the run, and group topics only to the API key that created the group.
- The API key of a run is the key that created it. For a run that a person starts in the dashboard, it is the key that the person selects under Webhooks. If the person selects no key, the run sends no webhooks.
- Use
sequenceto order the run, step and review events of one run. Deliveries can arrive out of order. - Expect
pipeline_step.startedagain for the sameattempt. A step that waits for a free worker slot sends it again when it starts. - Expect no event when a step is created or becomes ready, and no review event when a review task opens.
pipeline_step.waiting_reviewannounces the task. - Expect only
pipeline_group.completedwhen you close a group whose runs all finished. Afterpipeline_group.reopened, expectpipeline_group.completedagain.
For an organization with pipelines enabled, the API key form in the dashboard lists the pipeline topics under Pipeline events. A save of the form keeps the topics that the form does not list. The page of a run in the dashboard lists the webhook events of the run and its steps, with each delivery attempt.
Legacy topics
Editframe still emits these topics, tied to the deprecated per-type file APIs. Do not build a new integration against them.
image_file.created, isobmff_file.created, isobmff_track.created, unprocessed_file.created.
Delivery and duplicate handling
Each event arrives as one HTTP POST. Deliveries can repeat; do not assume ordering or unlimited retries.
data.id identifies a resource, not a delivery. Several lifecycle events can share that ID, and repeated updates can share a topic.
Use the body fingerprint to recognize identical deliveries within the endpoint's configured API-key scope. Store it atomically with the accepted event.
Identical bodies cannot distinguish separate events with identical content. Design business operations around resource state or an application-owned transition key.
Do not use a resource ID or topic-and-ID pair as a permanent notification deduplication key.
Monitor failed deliveries and reconcile important resource state through the API. A delivery acknowledgement does not prove business processing succeeded.
Testing
Change the Webhook URL of an API key in the dashboard. Editframe then sends one test event to the new URL:
{ topic: "webhook.test", data: { id: "<API key ID>", org_id: "<organization ID>" } }
The event carries X-Webhook-Signature, from the key's webhook secret. Accept the webhook.test topic in your handler.
To test a render or file topic, create a real render or upload a small file.
For local development, tunnel your dev server, for example with ngrok http 3000. Point the API key's Webhook URL at the tunnel URL while you test.
Send Feedback
Send feedback to Editframe about problems and about improvements:
- If Editframe or this skill causes a problem, find a workaround and continue the task. Then report the problem and the workaround.
- If you find an improvement that can help other tasks too, not only your current task, send a suggestion. For example, suggest a clearer instruction, a missing example, a better default, or a new feature.
Send one report for each different problem or suggestion. If the report fails, continue the task.
curl -sS https://editframe.com/api/v1/feedback \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $EDITFRAME_API_KEY" \
--data-binary @- <<'JSON'
{
"category": "bug",
"skill": "webhooks",
"summary": "One line that names the problem or the suggestion",
"details": "For a problem: steps, expected result, actual result, and errors. For a suggestion: the change and how it helps.",
"workaround": "The workaround that you used. Omit this field for a suggestion."
}
JSON
Set category to bug, documentation, feature_request, performance, or other. The API key is optional. A report without a key is anonymous. Do not put secrets, API keys, or personal data in a report. See the editframe-api skill for all fields, limits, and responses.