Sume webhook signature fails? Verify the raw body before JSON.parse
Sume signs HMAC-SHA256 over timestamp.raw_body. Re-serialized JSON changes the bytes and the check fails. Read the raw body first; a Node verifier is included.

A Sume webhook signature fails to verify most often because the body was parsed and re-serialized before checking. Sume signs the exact raw bytes with HMAC-SHA256 over <timestamp>.<raw_body>. Whitespace or key order changes break the match. Read the raw text first, verify, and only then parse.
What Sume signs
The signed string is the timestamp, a dot, and the raw JSON body. Two headers come with each delivery: x-sume-webhook-timestamp and x-sume-webhook-signature, whose value looks like sume-v1=<hex>. A third header, x-sume-webhook-secret-fingerprint, names the secret that signed it.
A Node verifier
This is the docs verifier trimmed to the check. It rejects stale timestamps (default tolerance 300 seconds) and compares in constant time. The secret is read from SUME_COM_WEBHOOK_SIGNING_SECRET and the function refuses an empty one.
import crypto from 'node:crypto';
export function verify(rawBody, ts, header, secret, tol = 300) {
if (!secret) throw new Error('missing signing secret');
const t = Number(ts);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > tol) return false;
const want = Buffer.from('sume-v1=' + crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex'));
return header.split(',').some((e) => {
const got = Buffer.from(e.trim());
return got.length === want.length &&
crypto.timingSafeEqual(got, want);
});
}Framework traps
Express with express.json() consumes the stream and hands you an object. Use a raw body parser on the webhook route only, or read request.text() in a fetch-style handler. Any middleware that logs and re-stringifies the body has the same effect.
When it still fails
Compare fingerprints. The dashboard Webhooks tab shows a fingerprint next to the secret, and each delivery carries the same value. If they differ, you hold the wrong secret. If they match, suspect the raw body or the clock.
Fix the check, then return a fast 2xx. Sume retries non-2xx responses, so a failing verifier also causes repeated deliveries.
Test the verifier with Send test
Before real traffic, post a signed dummy event from /dashboard/webhooks or POST /v1/webhooks/test-deliveries. It carries a webhook.test event that your handler should verify, acknowledge and ignore. If it passes here with the raw body, it will pass for job events too.
Also confirm that your proxy or CDN does not rewrite the body, for example by compressing or normalizing JSON. The signature covers the bytes that Sume sent, so the bytes your code reads must match them exactly.
Related posts
More in Developers
- Sume rejects localhost webhook_url: develop locally with polling
Sume webhook URLs must be public HTTPS. localhost, private-network and non-HTTPS URLs are rejected. Develop with polling and Send test, then switch the URL on.
- Swift: submit a Kling 3.0 job and save the MP4 to disk
One Swift file: POST /v1/videos for kling-3, poll the polling_url, follow the content redirect and write out.mp4. A 5 s clip without audio costs $0.70.
- Swift URLSession: call Sume's image API and branch on 200 or 202
Use URLSession.data(for:) with async/await, cast the response to HTTPURLResponse, and read statusCode before you touch data[0].url on Sume's /v1/images.
- Which Sume audio calls can sync-wait 30 s? A map vs 150 ms claims
Detach, timeline audio, ingest, music and TTS: which can return in a 30 s sync wait, which are async jobs, and what a 150 ms end-to-end claim leaves out.
Written by Sume