One Sume webhook route for job and run events, 204 on the rest

Route job.* and *.run.terminal events from one verified webhook handler and answer 204 for any unknown event, so a new type cannot trigger retries.

4 min readSume
All posts

Use one route for both Sume webhook surfaces, verify the signature once, switch on event, and answer 204 for any event name you do not know. A 500 on a new event type makes Sume retry the same delivery, up to 10 attempts 30 seconds apart, for a payload you will never handle. The signature scheme is identical for job webhooks and run webhooks, so one verifier covers both.

The two event families

Job webhooks send terminal events only, with no progress or partial deliveries. Canceled and skipped runs send no run webhook.

Webhook events, from the docs read 2026-10-08
FamilyEventsDedupe on
Generation jobjob.completed, job.failed, job.canceledjob_id
Runformat.run.terminal, action.run.terminal, agent.run.terminalrun id or request_id

The handler

This uses verifyWebhook from @sume-com/sdk 0.2.0, which is async, returns false instead of throwing, and compares in constant time. It reads the raw body first, refuses an empty secret at load time, and keeps the saveOnce stub as a place for a unique-key insert.

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 empty");
const seen = new Set(); // swap for a unique-key insert
const saveOnce = async (key) => void seen.add(key);

export async function POST(request) {
  const body = await request.text(); // raw bytes first
  const ok = await verifyWebhook({ body, headers: request.headers, secret });
  if (!ok) return new Response("bad signature", { status: 401 });
  const event = JSON.parse(body);
  switch (event.event) {
    case "job.completed":
    case "job.failed":
    case "job.canceled":
      await saveOnce(`job:${event.job_id}`);
      break;
    case "format.run.terminal":
    case "action.run.terminal":
    case "agent.run.terminal":
      await saveOnce(`run:${event.run_id ?? event.request_id}`);
      break;
    default:
      break; // a new event type is not an error
  }
  return new Response(null, { status: 204 });
}

Why 204 is the right default

Add a log line in the default branch with the event name. It shows you new event types the first day they appear, without making them an incident.

  • Sume does not follow redirects and counts a non-2xx as a failed attempt, so an error response retries.
  • Retries arrive with the same job or request id, so dedupe on that id before side effects.
  • Return fast. Do slow work after the response, because each attempt times out after 10 seconds.
  • A bad signature gets 401 and nothing else. Do not reveal why it failed.

Testing the default branch

Send yourself a signed body with an invented event name, such as job.paused, and check that the handler returns 204 and writes nothing. Sume also offers POST /v1/webhooks/test-deliveries, which sends a dummy webhook.test event to your URL. That event is a ready-made unknown event for your default branch, so a passing test delivery shows the whole route works: signature, parse, switch and 204.

Do the same for the opposite case. Send a body with a bad signature and check for 401. Both outcomes belong in your tests before the first paid job.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume