Deno KV: dedupe Sume webhooks with atomic check on a null versionstamp

Deno.serve and Deno KV: verify the Sume HMAC with crypto.subtle.verify, then claim job_id with kv.atomic().check({ versionstamp: null }) so retries commit once.

5 min readSume
All posts

Claim the job with kv.atomic().check({ key, versionstamp: null }).set(key, event).commit(). The check passes only if the key does not exist, so the first delivery of a job_id commits and every retry gets ok: false. Verify the signature first with crypto.subtle.verify, which compares the digest in constant time, and answer 204 to both the new delivery and the duplicate.

Sume's webhook docs say delivery is at-least-once, with up to 10 attempts, and that job_id is the idempotency key. A KV key built from that id turns the rule into one atomic operation, with no table or lock.

The server

Run it with deno run --unstable-kv -A server.ts on Deno versions where KV is still flagged. The signed string is <timestamp>.<raw_body>, and the header holds sume-v1=<hex> entries.

const secret = Deno.env.get("SUME_COM_WEBHOOK_SIGNING_SECRET") ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is empty");
const enc = new TextEncoder();
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
const kv = await Deno.openKv();
const unhex = (h: string) => Uint8Array.from(h.match(/../g) ?? [], (b) => parseInt(b, 16));

async function valid(req: Request, raw: string): Promise<boolean> {
  const ts = Number(req.headers.get("x-sume-webhook-timestamp"));
  if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
  for (const e of (req.headers.get("x-sume-webhook-signature") ?? "").split(",")) {
    const hex = e.trim().replace(/^sume-v1=/, "");
    if (!/^[0-9a-f]{64}$/.test(hex)) continue;
    if (await crypto.subtle.verify("HMAC", key, unhex(hex), enc.encode(`${ts}.${raw}`))) return true;
  }
  return false;
}

Deno.serve({ port: 8080 }, async (req) => {
  const raw = await req.text();
  if (req.method !== "POST" || !(await valid(req, raw))) return new Response(null, { status: 401 });
  const event = JSON.parse(raw);
  if (event.job_id) {
    await kv.atomic().check({ key: ["sume_job", event.job_id], versionstamp: null })
      .set(["sume_job", event.job_id], { event: event.event, at: Date.now() }).commit();
  }
  return new Response(null, { status: 204 });
});

Outcomes of the atomic claim

The result of commit() tells you which delivery you saw, but the answer to Sume is the same for both.

Deno KV claim outcomes for a Sume job webhook (Sume docs, read 2026-10-04)
Deliverycommit().okAnswerEffect
First for this job_idtrue204Record written once
Retry after a lost 204false204No second write
Redeliver from the APIfalse204Same job_id, so ignored
webhook.test, no job_idNot called204Nothing stored
Bad signatureNot called401Nothing stored

Caveats

  • Write the work you want done once (an upload, an email) as a record in the same store, and let a worker act on it. A commit that succeeds before your downstream call leaves the claim without the work if the process dies.
  • crypto.subtle.verify needs the 32 raw bytes of the digest, so the sample decodes the hex and skips anything that is not 64 hex characters.
  • Top-level await is fine in Deno modules, which is why the sample has no main() wrapper.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume