Cloudflare Workers webhook to a Queue for Sume video runs
Verify Sume's signed POST in a Cloudflare Worker, enqueue a small message, and answer 204 fast. Queue messages cap at 128 KB; receipts reach 1 MiB.

To receive Sume video webhooks on Cloudflare, verify each signed POST in a Worker with verifyWebhook from @sume-com/sdk, send a small message to a Cloudflare Queue, and answer 204 well inside Sume's 10-second attempt window; a consumer Worker then fetches the receipt. Enqueue ids and URLs, never the receipt itself: a Queue message is limited to 128 KB, while Sume inlines receipts up to 1 MiB.
Cloudflare facts come from its Workers Context and Secrets pages and its Queues JavaScript APIs, Limits, Delivery guarantees, and Configuration pages; Sume facts come from Run webhooks and Verifying webhooks. All were read on 2026-09-27. There is no Sume connector for Cloudflare. The SDK needs only fetch and WebCrypto and is documented to run on Cloudflare Workers; the TypeScript SDK quickstart covers installing it, and Signed webhooks for video runs covers the signature and retry contract.
Why not finish the work in ctx.waitUntil()?
For an HTTP-triggered Worker, ctx.waitUntil() extends execution for up to 30 seconds after the response is sent. That limit is shared across every waitUntil() call in the request, and promises still pending after it are canceled. Cloudflare's advice for longer work is to send messages to a Queue and process them in a separate consumer Worker, where each invocation gets up to 15 minutes of wall time.
| Limit | Value |
|---|---|
| Sume delivery attempt | 10 s per attempt, up to 10 attempts |
| Sume inline receipt | Up to 1 MiB; a larger one arrives as payload: null with error.result_url |
ctx.waitUntil() | Up to 30 s after the response is sent |
| Queue message | 128 KB, where 1 KB is 1000 bytes |
sendBatch() | 100 messages, or 256 KB in total |
| Queue consumer invocation | 15 minutes of wall time |
| Queue delivery | At least once; on rare occasions more than once |
What should the Worker put on the Queue?
Only what the consumer needs to find the result: the dedupe key and a URL. send() accepts a body only while its size is under 128 KB, and a run receipt can be far larger. Every run receipt carries its own result_url; when the receipt was too big to inline, the same pointer arrives as error.result_url. The consumer reads it with your API key and gets the full receipt, wrapped in data.
What does the Worker look like?
One Worker can be both producer and consumer: fetch() receives the webhook, and queue() receives batches from the Queue. verifyWebhook uses WebCrypto rather than node:crypto, which is what keeps it importable on Workers; it is async and returns false instead of throwing. Awaiting send() before answering means the message is on the Queue before Sume sees a 2xx.
import { verifyWebhook } from "@sume-com/sdk";
export default {
async fetch(request, env) {
const body = await request.text(); // raw, before JSON.parse
const secret = env.SUME_COM_WEBHOOK_SIGNING_SECRET;
if (!(await verifyWebhook({ body, headers: request.headers, secret }))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(body);
if (event.event !== "format.run.terminal") return new Response(null, { status: 204 });
const url = event.payload?.result_url ?? event.error?.result_url;
await env.SUME_EVENTS.send({ request_id: event.request_id, result_url: url });
return new Response(null, { status: 204 }); // well inside Sume's 10 s
},
async queue(batch, env) {
for (const message of batch.messages) {
if (await alreadyHandled(env, message.body.request_id)) continue;
const res = await fetch(message.body.result_url, {
headers: { Authorization: `Bearer ${env.SUME_API_KEY}` },
});
if (!res.ok) throw new Error(`result_url answered ${res.status}`); // retries the batch
await saveRun(env, (await res.json()).data); // the full receipt
}
},
};How do I avoid processing an event twice?
Both sides can repeat. Queues deliver at least once, and on rare occasions more than once; Sume retries a slow or failed attempt, and Redeliver re-POSTs an event on request. Cloudflare suggests a unique ID as the database key or idempotency key. Sume already sends one: request_id, the same on every retry of a run, which is why the consumer checks it before fetching.
- If
queue()throws, the whole batch counts as failed and is retried under the consumer's retry settings.max_retriesdefaults to 3. - Name a
dead_letter_queuefor the consumer. Without one, messages that keep failing are eventually discarded. The run itself is not lost:result_urlstill returns it.
How do I wire the Queue and the secrets?
Three pieces of configuration, outside the code:
- Bind the Queue in the Wrangler file: a
[[queues.producers]]entry withqueue = "sume-events"andbinding = "SUME_EVENTS", and a[[queues.consumers]]entry for the same queue. - Store
SUME_COM_WEBHOOK_SIGNING_SECRETandSUME_API_KEYas Worker secrets withwrangler secret put. Secrets are encrypted text values, read fromenvlike environment variables. - Give Sume the Worker's public HTTPS URL as
communication.webhook_url, without an explicit port: current code refuses one. Sume's webhook URL rules lists the other refusals.
Sources
- Run webhooks
- Verifying webhooks
- TypeScript SDK
- Webhooks
- Runs and results
- Cookbook
- Cloudflare Workers: Context (ctx) (read 2026-09-27)
- Cloudflare Queues: JavaScript APIs (read 2026-09-27)
- Cloudflare Queues: Limits (read 2026-09-27)
- Cloudflare Queues: Delivery guarantees (read 2026-09-27)
- Cloudflare Queues: Configuration (read 2026-09-27)
- Cloudflare Workers: Secrets (read 2026-09-27)
Related posts
More in Integrations
- CrewAI video generation with a Sume Agent Completions tool
Give a CrewAI agent a BaseTool that hands a video brief to Sume Agent Completions with a spend cap, then reads the agent.run for the finished video.
- Dify custom tool from OpenAPI: import the Sume API schema
Make a Dify custom tool from Sume's OpenAPI schema: trim it to three video operations, import it as a Swagger API tool, and keep the key secret.
- Discord bot AI video generation: defer, then edit the reply
Defer the Discord interaction within 3 seconds, submit POST /v1/videos with a callback_url, then edit the reply when Sume's job webhook arrives.
- Express raw body for webhook signatures and the 100kb limit
Mount express.raw with type application/json and a limit above 1 MiB on the Sume webhook route, then pass the raw Buffer to verifyWebhook.
Written by Sume