Bun.serve webhook receiver for Sume: raw body, verify, dedupe
A 24-line Bun.serve receiver for Sume job webhooks: read the raw body first, verify sume-v1, dedupe on job_id, answer 204 before the work. Tested on Bun 1.4.

A Sume webhook receiver in Bun is a Bun.serve fetch handler that does four things in order: read await req.text() before any parsing, verify the sume-v1 signature over timestamp.rawbody, skip ids it has already seen, and return 204 quickly. The 24-line handler below does exactly that, refuses to start without a signing secret, and answers 401 to a bad signature.
It uses verifyWebhook from @sume-com/sdk (0.2.0), which is async and also accepts the two-signature header Sume sends for 24 hours after a secret rotation. Read verifyWebhook returns false during a Sume secret rotation if you hand-roll your own check.
Facts the handler relies on
| Fact | Value |
|---|---|
| Signed string | timestamp, a dot, then the raw body |
| Headers | x-sume-webhook-timestamp and x-sume-webhook-signature |
| Replay window | 5 minutes is the suggested tolerance |
| Attempts | Up to 10, 30 seconds apart, 10 second timeout each |
| Dedupe key | job_id (request_id on run webhooks) |
| Job events | job.completed, job.failed, job.canceled |
The receiver
Start it with SUME_COM_WEBHOOK_SIGNING_SECRET set to the secret from the dashboard Webhooks tab, then expose it over public HTTPS; Sume rejects localhost and non-HTTPS webhook URLs.
import { verifyWebhook } from "@sume-com/sdk";
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is not set");
const seen = new Set<string>(); // swap for a database unique index in production
Bun.serve({
port: 8787,
async fetch(req) {
if (req.method !== "POST") return new Response("method not allowed", { status: 405 });
const body = await req.text(); // raw bytes first, parse after verifying
if (!(await verifyWebhook({ body, headers: req.headers, secret }))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(body);
const id = event.job_id ?? event.request_id;
if (!id) return new Response(null, { status: 204 }); // unknown shape: ack, do not retry-storm
if (!seen.has(id)) {
seen.add(id);
queueMicrotask(() => console.log("handle", event.event, id)); // work after the 2xx
}
return new Response(null, { status: 204 });
},
});Why each line is there
Each line maps to a documented delivery rule, and each one fails in a recognizable way when it is missing: a parsed body gives a 401 on every genuine delivery, a missing id check gives double side effects, and slow work gives timeouts and repeat attempts.
req.text()first: re-serialized JSON does not match the signed bytes.- Unknown shapes get a 204. An event type you have not seen should not become a 500 and a retry storm.
- Work after the response: the delivery times out at 10 seconds and each failure burns one of 10 attempts.
- The
Setis a stand-in. It forgets on restart, so use a database unique constraint on the id.
Limits
I tested it on Bun 1.4.0 with @sume-com/sdk 0.2.0 by sending signed requests from a script: a valid delivery, the same delivery again, and one with a corrupted signature returned 204, 204, 401, and the handler ran once. That is a local test, not a delivery from Sume. The 204 only means you accepted the event; if your queue write fails after the response, nothing retries it, so persist the event before answering when loss matters, and keep status polling as the backup the docs recommend.
Sources
Related posts
More in Integrations
- Claude Code 2.1.286 prompt count '2 of 5': parallel Sume calls
Claude Code 2.1.286 shows a count like 2 of 5 on stacked permission prompts, oldest first. Five parallel Sume calls make five prompts; script_run is one.
- Claude Code 2.1.287 large MCP results: Sume's include_results answer
Claude Code 2.1.287 fixed paging of large MCP results saved as JSON. For Sume job waves, use jobs_wait include_results and read omitted ids once.
- Claude Code ENABLE_CLAUDEAI_MCP_SERVERS=false and your Sume entry
Claude Code can load claude.ai connectors next to your own servers. Set ENABLE_CLAUDEAI_MCP_SERVERS=false to run only the Sume entry you configured.
- Claude Code mcp add --client-id: Sume's OAuth needs no client ID
claude mcp add has --client-id and --client-secret for pre-registered OAuth apps. Sume's hosted MCP quickstart uses neither: add the URL, then login.
Written by Sume