Verify the Sume signature in the HTTP handler, not in the queue worker

A queue delay over 300 seconds makes a valid Sume signature look stale. Verify at receipt, enqueue the verified event, and use redeliver if a late check failed.

5 min readSume
All posts

Verify the sume-v1 signature in the HTTP handler that receives the webhook, before the event goes on a queue. The signature check includes a replay window (300 seconds by default), and a worker that checks it after a long queue delay will reject a delivery that was valid when it arrived.

How the window bites

Sume signs <timestamp>.<raw_body> and sends the timestamp in x-sume-webhook-timestamp. A verifier that enforces the window compares that timestamp with the clock at the moment of the check. If a job sits in your queue for six minutes, a worker that verifies at pickup sees a timestamp six minutes old and returns false, even though the bytes and the secret are correct.

The failure looks like an attack, so people respond by widening the tolerance or turning it off. Neither is needed. Move the check to the point of receipt.

Where to verify, with the effect of a queue delay of 6 minutes (360 s), per the Sume webhook docs (read 2026-10-08)
DesignCheck timeResult with a 360 s delay
verify in handler, then enqueueat receiptpasses, window not exceeded
enqueue raw body, verify in workerat pickupfails, 360 s is over 300 s
verify in worker with tolerance 0at pickuppasses, but replay protection is off

Handler shape

The handler below verifies with the SDK, then puts only the verified, parsed event on the queue. The worker never sees headers and never repeats the check. The queue message should carry job_id, which is also the idempotency key for the work.

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

export async function POST(request: Request) {
  const body = await request.text();
  const ok = await verifyWebhook({ body, headers: request.headers, secret });
  if (!ok) return new Response("bad signature", { status: 401 });

  const event = JSON.parse(body);
  await queue.send({ jobId: event.job_id, event: event.event });
  return new Response(null, { status: 204 });
}

Two common variants

Some teams verify at an API gateway or edge function and forward a trusted internal header to the app. That is fine as long as the gateway verifies the raw bytes and the app is not reachable from the internet by another route. Others verify in the handler and also store the raw body, so a later job can be debugged against the exact bytes Sume signed.

Either way, the one place that checks the timestamp should run within seconds of the delivery, not minutes.

If a late check already failed

A delivery that you dropped for a stale timestamp is not lost. POST /v1/jobs/{job_id}/webhook/redeliver (API key with jobs:write) re-sends the real terminal event with a fresh timestamp and a fresh signature, and it does not use one of the 10 automatic attempts. The job also stays readable by polling its status_url.

Keep the handler quick, since Sume allows 10 seconds per attempt. Verification and one queue write fit in that time; the video download does not. See ack fast then enqueue for the queue side.

  • Keep toleranceSeconds at its default of 300.
  • Do not set it to 0 to silence a stale-timestamp error; that turns the replay check off.
  • Check your own clock with NTP before blaming the window.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume