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.

4 min readSume
All posts

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.

Delivery behavior, read 2026-10-02 from docs.sume.com
BehaviorWhat it means for the receiver
Up to 10 attemptsThe same event can arrive more than once
Redeliver endpointA fresh timestamp and signature for the same job
Test sendswebhook.test has no job_id, so route it away from your handler
Run webhooksSame 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 queueMicrotask call 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

All Integrations posts

Written by Sume