Cloudflare Worker that verifies a Sume video webhook (verifyWebhook)
A 20-line Cloudflare Worker for Sume job webhooks: verifyWebhook from @sume-com/sdk on WebCrypto, empty-secret guard, 204 fast ack. Why the check is async.

On Cloudflare Workers, verify a Sume webhook by passing the raw request text, the request headers and your signing secret to verifyWebhook from @sume-com/sdk. It runs on WebCrypto, not node:crypto, so it imports cleanly in a Worker, and it returns true only when the sume-v1 signature matches and the timestamp is within the replay window.
Why a Worker is a good webhook edge
Video jobs finish on their own schedule. A 30-second Seedance 2.5 render or a 4K Gemini Omni Flash 1.1 clip can take minutes, so the callback arrives when nobody is watching. A Worker is always warm at the edge, answers in milliseconds, and can hand the event to a queue, which keeps you inside Sume's 10-second timeout for each delivery attempt.
The SDK's verifier was written for exactly this runtime. Its source notes that it uses WebCrypto so the published package stays importable from Workers, Deno, Bun and bundlers that refuse node: specifiers. That is also why the function is async, and why you must await it.
What the helper checks
You do not have to hand-roll the HMAC. The helper does the work, and it never throws on a malformed delivery. A missing header, a bad timestamp and a wrong signature all become false, so you branch on one value.
| Input | Result | Note |
|---|---|---|
| Matching sume-v1 signature, fresh timestamp | true | Constant-time comparison |
| Timestamp older than 300 s | false | Default tolerance, set toleranceSeconds to change it |
| Missing or garbage header | false | Never throws |
| Empty secret | false | Refused before any HMAC work |
| Two signatures during rotation | true if either matches | Newest first, comma-separated |
The Worker
Install the package in your Worker project with npm install @sume-com/sdk, store the secret with wrangler secret put SUME_COM_WEBHOOK_SIGNING_SECRET, and deploy. The explicit empty-secret check is belt and braces, since it turns a missing binding into a loud 500 instead of a silent stream of 401s.
If you also want the Worker to poll, add a scheduled handler that reads GET /v1/videos/{id} for ids still pending after your own deadline. Cloudflare cron triggers make that a few lines, and it closes the gap for deliveries that exhaust their 10 attempts.
import { verifyWebhook } from "@sume-com/sdk";
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("no", { status: 405 });
if (!env.SUME_COM_WEBHOOK_SIGNING_SECRET) {
return new Response("secret not configured", { status: 500 });
}
const body = await request.text(); // raw, before any JSON.parse
const ok = await verifyWebhook({
body,
headers: request.headers,
secret: env.SUME_COM_WEBHOOK_SIGNING_SECRET,
});
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(body);
if (event.event === "job.completed") {
await env.JOBS.send({ jobId: event.job_id }); // a Queues binding
} else if (event.event === "job.failed" || event.event === "job.canceled") {
await env.JOBS.send({ jobId: event.job_id, terminal: event.event });
}
return new Response(null, { status: 204 });
},
};
Details that bite
Read the body with request.text() before anything else. Sume signs the raw bytes, so a parsed and re-serialized body will never verify. Do not call request.json() first, even in a middleware.
Send only the job id downstream. Fetch the artifact later through GET /v1/videos/{id}/content, with your API key held in a separate secret. The webhook is a doorbell and the poll route is the source of truth, so if the queue consumer sees an id it does not know, it can read the job directly.
Dedupe on job_id in the consumer. Sume retries non-2xx responses up to 10 attempts, and a redeliver call re-posts the real terminal event with a fresh timestamp, so repeats are normal.
Why this matters this season
Real-time avatar systems are moving the other direction. Tavus describes Griffin as a full-duplex video-to-video model, and says Griffin-Lite is available only to select trusted testers as a research preview. You cannot call that from a Worker today. Rendered video jobs, which Sume ships now, are the opposite shape: submit, leave, and let a signed callback bring you back, and the Worker above is the whole receiver.
- Keep the Worker thin. Verify, enqueue, acknowledge.
- Use the dashboard Send test action to check the secret before a real render.
- Keep a poll fallback for events that never arrive.
Sources
Related posts
More in Developers
- compose_duration_clamped_to_source: the banner clip came out shorter
Compose clamps video.duration to the source file and warns compose_duration_clamped_to_source. The job still succeeds; read the result length first.
- Conversational video edits on Omni: a three-turn chain on Sume
Edit a clip in turns on Sume: each Omni video_to_video call takes the last result as video_url. Three turns on a 10 s clip cost $3.75 at 720p. curl chain.
- createSumeClient custom fetch: log ratelimit-remaining as you go
Pass your own fetch to createSumeClient to warn when ratelimit-remaining runs low, without wrapping every SDK call. Works on Node 18+, Bun, Deno, Workers.
- C# HttpClient and Sume images: keep the key off the download call
Reuse one HttpClient, but set the bearer header per request, not as a default header, or it also rides along when you download the Sume image URL.
Written by Sume