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.

5 min readSume
All posts

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

All Developers posts

Written by Sume