Deno.serve webhook receiver for Sume with a KV dedupe check
A short Deno.serve receiver for Sume webhooks: verify sume-v1 over the raw body, dedupe on job_id with an atomic KV write, and acknowledge with 204.

In Deno, receive a Sume webhook with Deno.serve, read await req.text(), verify the signature, and dedupe on job_id with an atomic Deno KV write so a retried delivery is handled once. The short receiver below does that, and it needs --unstable-kv on Deno 2.9.7, where calling Deno.openKv without the flag fails with "Deno.openKv is not a function".
The signature check is the 21-line verifier from verifyWebhook returns false during a Sume secret rotation, saved as verify.mjs. It is WebCrypto only, so Deno needs no Node compatibility layer.
Why dedupe on job_id
Redeliver does not use up the automatic attempts, so a manual redeliver after an outage is another copy of an event you may already have stored. The id is the key.
| Behavior | What it means for the receiver |
|---|---|
| Up to 10 attempts | The same event can arrive more than once |
| Redeliver endpoint | A fresh timestamp and signature for the same job |
| Test sends | webhook.test has no job_id, so route it away from your handler |
| Run webhooks | Same signature scheme, run_id instead of job_id |
The receiver
Run it with SUME_COM_WEBHOOK_SIGNING_SECRET set: deno run -A --unstable-kv receiver.ts. The in-memory KV forgets on restart; open a path or use a hosted database for real traffic.
import { verifySume } from "./verify.mjs";
const secret = Deno.env.get("SUME_COM_WEBHOOK_SIGNING_SECRET") ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is not set");
const kv = await Deno.openKv(":memory:"); // use a persistent KV or database in production
Deno.serve({ port: 8788 }, async (req) => {
if (req.method !== "POST") return new Response("method not allowed", { status: 405 });
const body = await req.text(); // raw body, before JSON.parse
if (!(await verifySume({ body, headers: req.headers, secret }))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(body);
const id: string | undefined = event.job_id ?? event.run_id;
if (!id) return new Response(null, { status: 204 });
const first = await kv.atomic().check({ key: ["seen", id], versionstamp: null })
.set(["seen", id], Date.now()).commit();
if (first.ok) queueMicrotask(() => console.log("handle", event.event, id));
return new Response(null, { status: 204 });
});Notes
The receiver is deliberately small. Test results, from a script that signs requests with a known secret, are in the next section; the notes here are the parts that are easy to get wrong when you adapt it to a framework or a hosting platform that wraps requests.
check({ versionstamp: null })makes the write succeed only if the key does not exist, so two concurrent deliveries cannot both win.- The handler logs after the response is built; replace the
queueMicrotaskcall with a durable queue write. - The secret check at the top stops a deployment that forgot the variable from accepting everything.
Limits
On Deno 2.9.7 a valid delivery returned 204, a repeat returned 204 with no second handling, and a corrupted signature returned 401. Those are local requests, not live Sume deliveries. The flag requirement is specific to the version I ran; check your Deno release notes. A dedupe write that succeeds before your real work fails will drop that event, so write the dedupe key and the work item in one transaction if you can.
Sources
Related posts
More in Integrations
- Fastify 5 raw body for a Sume webhook: parseAs string
Fastify parses JSON before your handler, which breaks HMAC checks. Use parseAs string, then verify sume-v1 and dedupe. Tested on Fastify 5.12.5.
- Flowise Custom MCP: connect Sume over Streamable HTTP
Add Sume to a Flowise Agent node as a Custom MCP tool: a url, an Authorization header from a $vars variable, then refresh Available Actions.
- Instagram Reels API: 1920 px width cap and 25 Mbps video bitrate
Meta's Reel spec caps width at 1920 px and bitrate at 25 Mbps VBR, with 23-60 FPS and 300 MB. Probe width, fps and size with Sume; bitrate is an average.
- Instagram Reels API aspect ratio: 0.01:1 to 10:1, 9:16 advised
Meta's Reels API accepts any aspect ratio from 0.01:1 to 10:1 and only recommends 9:16. See what each ratio rule says and check a clip with video inspect.
Written by Sume