mcpField
W
webhooks

webhooks

github.com/editframeDevelopment19125 installsUpdated 2w ago
Repository

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

  1. 01Configure HTTPS webhook URLs and event topics
  2. 02Verify HMAC-SHA256 signatures on raw bodies
  3. 03Persist accepted events before responding
  4. 04Handle duplicate and out-of-order deliveries
  5. 05Document render, file, and pipeline topics

Use cases

  1. 01Receiving Editframe render status updates
  2. 02Processing file lifecycle notifications
  3. 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 webhooks

After installing, type/webhooksin Claude Code to confirm it loaded.

Details

From the project README

Webhooks

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.

TopicEditframe sends it whendata
pipeline_run.createdA run is created (pending).Run
pipeline_run.startedA run starts its steps (running).Run
pipeline_run.retriedA failed or cancelled run is retried.Run
pipeline_run.completedA run succeeded.Run
pipeline_run.failed, pipeline_run.filtered, pipeline_run.cancelledA run ends with that status.Run
pipeline_step.startedA step starts.Step
pipeline_step.progressThe watermark_ms of a segment step increases. At most one event for each step every 10 seconds.Step
pipeline_step.waiting_externalAn endpoint answered 202, or a redact_render step started its render. The step waits for the callback or the render.Step
pipeline_step.waiting_reviewA review step opens a review task.Step
pipeline_step.retryingA 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.cancelledA step succeeds, fails, skips or is cancelled.Step
pipeline_review.decidedA person or an API key approves or rejects a review.Review
pipeline_review.expiredA review expires without a decision.Review
pipeline_group.createdA group is created, open or closed.Group
pipeline_group.closedAn open group is closed, and some of its runs did not finish.Group
pipeline_group.completedAll runs of a closed group finished.Group
pipeline_group.reopenedA retried run makes a completed group closed again.Group
file.receivingA streamed upload starts.File
file.sealedA 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, rejected or expired), decision ({ decision, decided_by: { type, id }, reason }, or null after expiry), sequence, created_at, expires_at, decided_at. The event does not contain the corrections of an approval. Get the review with getPipelineReview to 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.
  • metadata and run_metadata contain the metadata of the run or group when it was created.
  • A run ends filtered only when filters or reviews stop all branches. A run with a stopped branch and a successful final step sends pipeline_run.completed. Each skipped step sends pipeline_step.skipped.
  • File data has the same fields as the other file topics.
  • error is null or { 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 sequence to order the run, step and review events of one run. Deliveries can arrive out of order.
  • Expect pipeline_step.started again for the same attempt. 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_review announces the task.
  • Expect only pipeline_group.completed when you close a group whose runs all finished. After pipeline_group.reopened, expect pipeline_group.completed again.

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.

Comments

Markdown supported