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
- Devin Desktop team allowlist: Sume's MCP server blocked until listed
In Devin Desktop, once an admin allowlists any MCP server, all others are blocked for the team. Add Sume's production URL to the list before members connect.
- Devin Desktop 100-tool limit: fit Sume's MCP tools with disabledTools
Cascade in Devin Desktop (formerly Windsurf) caps total tools at 100. Sume's docs list about 75 tool ids; trim others with disabledTools.
- Factory Droid MCP server: droid mcp add --type http for Sume
Add Sume's hosted MCP to Factory's droid CLI with droid mcp add --type http and an x-api-key header, why --no-oauth is needed, and where it is stored.
- 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.
Written by Sume