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.

6 min readSume
All posts

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.

From Sume's Run webhooks and Cloudflare's Context, Queues limits, JavaScript APIs, and Delivery guarantees pages, read 2026-09-27.
LimitValue
Sume delivery attempt10 s per attempt, up to 10 attempts
Sume inline receiptUp 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 message128 KB, where 1 KB is 1000 bytes
sendBatch()100 messages, or 256 KB in total
Queue consumer invocation15 minutes of wall time
Queue deliveryAt 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_retries defaults to 3.
  • Name a dead_letter_queue for the consumer. Without one, messages that keep failing are eventually discarded. The run itself is not lost: result_url still 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 with queue = "sume-events" and binding = "SUME_EVENTS", and a [[queues.consumers]] entry for the same queue.
  • Store SUME_COM_WEBHOOK_SIGNING_SECRET and SUME_API_KEY as Worker secrets with wrangler secret put. Secrets are encrypted text values, read from env like 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

Related posts

More in Integrations

All Integrations posts

Written by Sume