Sume webhook receiver: return 204 for an unknown event, not a 500

Route Sume webhook events through a handler table, answer 204 for any event you do not know, and keep the webhook.test event out of your job table. Node sample.

4 min readSume
All posts

Answer any Sume webhook event that your code does not recognize with a 204, after the signature check passes. The SDK webhook docs show exactly this, with a comment that an unknown event is "not a 500". If a new event type shows up and your receiver replies 500, Sume treats it as a failed delivery and retries it, which wastes your attempts and fills your logs with an error that is not an error.

The simplest shape is a table that maps event names to functions. Anything not in the table falls through to a 204, and adding a new event later is one line. The sample below runs on current Node LTS lines (24.21 is the newest LTS listed on the Node release page today) using only node:http and node:crypto.

The events your table needs

Sume sends terminal job events only. There are no progress or partial deliveries. The test button and the test-delivery endpoint send a separate, dummy webhook.test event that has no job_id, so it must not land in a table keyed by job. The webhook docs list the shapes below.

Sume webhook events and the receiver response (read 2026-10-08)
EventMeaningReceiver response
job.completedJob done, public result available204 after storing artifacts
job.failedJob failed with a public error; status ERROR204 after recording the error
job.canceledJob reached canceled; status ERROR204 after marking canceled
webhook.testDummy signed event, no job_id204, store nothing
Any other eventNot documented yet204, log the name

Steps

  • Verify the signature on the raw body before parsing JSON. Reject with 401 on a mismatch, and refuse to start when the secret is empty.
  • Look the event name up in the handler table. If it is missing, do nothing and still return 204.
  • Dedupe on job_id. Sume retries until it gets a 2xx, up to 10 attempts at a fixed spacing of 30 seconds by default, so the same event can arrive more than once.
  • Store durably before you answer. The 10 second per-attempt timeout is part of the retry budget.
  • Keep polling the status route as a backup for events you never received.

Receiver

The in-memory Set is only for the demo; use a database row keyed by job_id in production. The sample also skips the timestamp window check to stay under the line limit, so add the five-minute check from the docs before you deploy it.

import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is empty");
const seen = new Set();
const handlers = {
  "job.completed": (e) => console.log("store artifacts", e.payload.artifacts.length),
  "job.failed": (e) => console.log("record failure", e.error?.code),
  "job.canceled": (e) => console.log("mark canceled", e.job_id),
  "webhook.test": () => console.log("test delivery, no job"),
};
function valid(raw, h) {
  const ts = h["x-sume-webhook-timestamp"] ?? "";
  const want = createHmac("sha256", secret).update(`${ts}.${raw}`).digest();
  return String(h["x-sume-webhook-signature"]).split(",").some((p) => {
    const got = Buffer.from(p.trim().replace("sume-v1=", ""), "hex");
    return got.length === want.length && timingSafeEqual(got, want);
  });
}
export const server = http.createServer(async (req, res) => {
  let raw = "";
  for await (const c of req) raw += c;
  if (!valid(raw, req.headers)) return res.writeHead(401).end();
  const event = JSON.parse(raw);
  const fn = handlers[event.event]; // unknown event: still a 2xx, never a 500
  if (fn && !(event.job_id && seen.has(event.job_id))) fn(event);
  if (event.job_id) seen.add(event.job_id);
  res.writeHead(204).end();
});

What Sume does not do

Sume does not promise that the set of event names will never grow, and it does not send progress events. It also does not pause retries because your handler returned a 500 for an event type you did not expect. After the automatic attempts are used, a failed delivery can be re-sent with POST /v1/jobs/{job_id}/webhook/redeliver, which does not count against the automatic ten.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume