---
name: editframe-webhooks
description: "Webhook notifications for render completion and file processing events. Configure an endpoint, verify HMAC signatures, and handle real-time status payloads."
license: MIT
metadata:
  author: editframe
  version: "3.0"
---


# Webhooks

Use webhooks to receive real-time HTTP POST notifications when a render completes or a file finishes processing. Use them instead of polling `getRenderProgress`/`getFileProcessingProgress`.

No SDK function registers a webhook. Configure one on an API key, through the dashboard (Settings → 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

```typescript
import express from "express";
import crypto from "node:crypto";

const app = express();

// Use express.raw(), not express.json(). Signature verification needs the
// exact raw bytes. Re-serializing parsed JSON can reorder keys or change
// whitespace, which changes the hash and breaks verification.
app.post("/webhooks/editframe", express.raw({ type: "*/*" }), (req, res) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const rawBody = req.body as Buffer;

  const expected = crypto
    .createHmac("sha256", process.env.EDITFRAME_WEBHOOK_SECRET!)
    .update(rawBody)
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"))) {
    return res.status(401).send("Invalid signature");
  }

  res.status(200).send("OK"); // respond fast — Editframe times out at 30s
  const payload = JSON.parse(rawBody.toString("utf-8"));
  processWebhookEvent(payload).catch(console.error); // do real work after responding
});
```

Every request carries an `X-Webhook-Signature` header: `HMAC-SHA256(webhook_secret, raw_json_body)`, hex-encoded. Verify with `crypto.timingSafeEqual`, not `===`.

## Payload

```typescript
{ 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.

### 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

- Each event arrives as one HTTP POST, with a JSON body.
- Editframe retries on a fixed 10-second interval, up to 3 attempts total, with a 30-second timeout per attempt. After the final failed attempt, it marks the event failed. This is visible in the dashboard's delivery log. Editframe does not retry or replay the event further.
- Editframe may deliver an event more than once. Key any side effect off `data.id`, or off the topic-and-id pair, to stay idempotent.
- Always hash the **raw** request body for signature verification. Parsing the body, then calling `JSON.stringify` again, can reorder keys or change whitespace. This produces a different hash than the one Editframe signed.

## Testing

```bash
npx editframe webhook -t render.completed   # sends a real test event to the URL configured on your API key
```

See the `editframe-api` skill's CLI section for more detail. There is no `--webhookURL` flag. The target URL always comes from the key's dashboard configuration. The dashboard's API key detail page has an equivalent "Test Webhook" button.

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.
