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.

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.
| Event | Meaning | Receiver response |
|---|---|---|
| job.completed | Job done, public result available | 204 after storing artifacts |
| job.failed | Job failed with a public error; status ERROR | 204 after recording the error |
| job.canceled | Job reached canceled; status ERROR | 204 after marking canceled |
| webhook.test | Dummy signed event, no job_id | 204, store nothing |
| Any other event | Not documented yet | 204, 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
- A Sume webhook receiver in plain Python WSGI, no framework
A 28-line wsgiref app that reads the raw body, verifies sume-v1 with a rotation-safe check, refuses an empty secret, and answers 204. Tested with curl.
- Sume webhook retries: 10 attempts, 30 s apart, a 4.5 minute window
Sume retries a job webhook up to 10 times, 30 s apart by default. That is 270 s between first and last attempt, 370 s at worst. What to do after.
- Sume webhook retries: 10 attempts, 30 s apart, 10 s timeout each
The delivery schedule for Sume job webhooks: 10 attempts, fixed 30 s spacing, 10 s timeout, about 4.5 minutes of retries, then redeliver and the status poll.
- Clock changes vs clock drift: what breaks Sume webhook checks
Sume webhook timestamps are Unix seconds, so time zone changes cannot break the 5-minute replay check; a drifting server clock can. A check to tell them apart.
Written by Sume