Webhook signature mismatch: Sume fingerprint vs OpenRouter t=,v1=

A video webhook that fails verification has three usual causes: parsed body, wrong secret, wrong header format. Sume adds a secret fingerprint header.

4 min readSume
All posts

When a video webhook fails verification, check the body, then the secret, then the header format. Sume helps with the second one: each delivery carries x-sume-webhook-secret-fingerprint, which you compare with the fingerprint shown beside your secret in the dashboard. You never paste the secret itself into a ticket. A client written against OpenRouter needs the third check, because the two services format the signature differently.

Two signature formats

The OpenRouter video guide, read today, says the signature header is X-OpenRouter-Signature: t=<timestamp>,v1=<hash>, an HMAC-SHA256 over the raw body. It also sends an idempotency header, X-OpenRouter-Idempotency-Key, built from the job id and status.

Sume signs <timestamp>.<raw_body>, sends the timestamp in x-sume-webhook-timestamp, and puts sume-v1=<hex> in x-sume-webhook-signature. The video docs say Sume sends its own job envelope, not the OpenRouter video.generation.* events.

Signature formats, read 2026-10-08
ItemOpenRouterSume
Signature headerX-OpenRouter-Signaturex-sume-webhook-signature
Formatt=<timestamp>,v1=<hash>sume-v1=<hex>
Signed dataRaw body<timestamp>.<raw_body>
TimestampInside the headerx-sume-webhook-timestamp
Event namesvideo.generation.*job.completed, job.failed, job.canceled

Three checks in order

Do these in order; the first one that fails is the cause.

  • Raw body. Any JSON parse and re-serialize changes the bytes. In Next.js use await request.text().
  • Secret. Compare the fingerprint header with the dashboard value. A rotation changes the fingerprint to the new secret at once.
  • Header format. A Sume header can carry two entries during the 24 hour rotation window, sume-v1=<new>,sume-v1=<old>. Accept the delivery when any entry matches.

What a failed check should return

Return 401 for a bad signature. Sume retries up to 10 times, so a wrong secret shows up as a run of retries, not one loss. The status value on the delivery row moves from retrying to exhausted; fix the secret, then redeliver the real event.

import { verifyWebhook } from "@sume-com/sdk";

export async function check(req: Request, secret: string) {
  if (!secret) return new Response("not configured", { status: 500 });
  const body = await req.text();
  const ok = await verifyWebhook({ body, headers: req.headers, secret });
  if (ok) return null;
  const fp = req.headers.get("x-sume-webhook-secret-fingerprint");
  console.warn("sume signature failed, fingerprint", fp);
  return new Response("bad signature", { status: 401 });
}

Reading the fingerprint

The fingerprint is a short identifier of the secret, not the secret. The dashboard shows it next to the value, and each delivery carries it in x-sume-webhook-secret-fingerprint. It also appears on the delivery receipt as signing_secret_fingerprint. If the two differ, your receiver holds a different secret from the one Sume signed with, and no code fix helps.

After a rotation the header names the new secret from the first moment, and also during the 24 hour window. It tells you which secret to move to. It does not list the secrets Sume still accepts.

Porting from the OpenRouter format

If you wrote a verifier for the t=<timestamp>,v1=<hash> header, you split the header, build the signed string and compare. For Sume you read the timestamp from its own header and build <timestamp>.<raw_body> with a literal dot. Then you compare to each sume-v1= entry in a constant-time check. The SDK helper does this, enforces a 300 second replay window first, and returns false for a malformed header instead of throwing.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume