Rotate the Sume webhook secret without dropping events

After POST /v1/webhooks/signing-secret/rotate, Sume signs with both secrets for 24 hours. How verifyWebhook handles the two-entry header, and the deploy order.

3 min readSume
All posts

Rotate the Sume signing secret in this order: upgrade your receiver to a verifier that accepts multi-signature headers, then rotate, then deploy the new secret within 24 hours. For that day Sume signs every delivery with both secrets and sends x-sume-webhook-signature: sume-v1=<new>,sume-v1=<old>, so a receiver holding either secret verifies.

A hand-written verifier that compares the whole header for equality fails every delivery during the window. verifyWebhook in @sume-com/sdk 0.2.0 already handles the two-entry header, per the verifying webhooks page.

A receiver with the SDK

The guard on the secret is worth keeping; a missing environment variable should stop the process, not weaken the check.

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");

async function saveTerminalJob(jobId: string, event: unknown) {
  console.log("store", jobId, event); // replace with a durable, deduped write
}

export async function POST(request: Request) {
  const body = await request.text(); // raw, before any JSON.parse
  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 saveTerminalJob(event.job_id, event); // dedupe on job_id
      break;
    default:
      break; // unknown event type: still 2xx, no retry storm
  }
  return new Response(null, { status: 204 });
}

Rotation timeline

Sume docs, read 2026-10-08
StepWhat Sume doesWhat you do
BeforeOne sume-v1 entryUpgrade the receiver
RotateNew secret first, old second in the headerCopy the new secret
Next 24 hoursTwo entries on every deliveryDeploy the new secret on your schedule
AfterOld secret rejectedNothing; check previous_valid_until on the API response

Edge cases

  • Rotating twice inside one window retires the oldest secret at once, which is how you stop a real leak.
  • x-sume-webhook-secret-fingerprint names the new secret from the moment you rotate; it does not say which secrets are still accepted.
  • verifyWebhook returns false rather than throwing, so a bad delivery is one branch, not a try/catch.

Deploy order, concretely

First ship the receiver that accepts multi-entry headers; with the SDK that means version 0.2.0. Then rotate in the dashboard or with POST /v1/webhooks/signing-secret/rotate. Then update the secret in your secret store and redeploy. Rotating needs an API key with account:write; reading the secret needs account:read. Finally, confirm that deliveries verify with the new secret and let the 24-hour window close. The fingerprint header is your check at each step: it should match the fingerprint beside the new secret in the dashboard.

Keep the same receiver for run webhooks. Format, Action and Agent runs use the identical sume-v1 scheme and the same secret, so routing on event is enough to share one endpoint between runs and jobs.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume