Express breaks your Sume webhook signature: mount express.raw first

If verifyWebhook returns false in Express, a JSON parser changed the body. Mount express.raw on the webhook route only. Working Node code with the SDK.

4 min readSume
All posts

Why does my Sume webhook signature fail in Express?

Because express.json() ran first. Sume signs the exact bytes it sent, as HMAC-SHA256 over <timestamp>.<raw_body>. A parsed and re-serialized object has different whitespace or key order, so the digest does not match even though the secret is right. The fix is to give the webhook route the raw body and parse it yourself after the check.

The SDK's verifyWebhook takes the body as a string or bytes, reads the two headers from req.headers in any case, and returns false instead of throwing when something is wrong. That makes a wrong parser order easy to miss: you get a clean 401 and no stack trace.

What each failure looks like

Check the cheap causes first. All of them return false from the verifier, so log which one you have before changing code.

Why a Sume webhook fails verification in Express (docs.sume.com, read 2026-10-06)
SymptomCauseFix
Always false, secret is rightexpress.json() ran before the routeMount express.raw on this route first
False for old deliveriesTimestamp outside 300 sFix server clock, or set toleranceSeconds
False only after a rotationOld secret still deployedDeploy the new secret; a header can carry both
False, header missingA proxy dropped x-sume-webhook-* headersForward them unchanged

Route order that works

Register the webhook route with its own raw parser before any global JSON middleware, then add express.json() for the rest of the app. The guard on the secret matters too: an unset environment variable becomes undefined, and a verifier that accepts an empty secret would sign nothing and approve everything.

import express from "express";
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");

const app = express();

app.post("/hooks/sume", express.raw({ type: "application/json" }), async (req, res) => {
  const body = req.body.toString("utf8");
  const ok = await verifyWebhook({ body, headers: req.headers, secret });
  if (!ok) return res.status(401).end();
  const event = JSON.parse(body);
  console.log(event.event, event.job_id);
  res.status(204).end();
});

app.use(express.json());
app.listen(3000);

After the check

Return a 2xx fast, because each attempt has a 10 second timeout, and do the slow work after the response. Deduplicate on job_id, since Sume may deliver the same terminal event more than once. The same route can serve job webhooks and run webhooks, because both use the sume-v1 scheme and the same signing secret.

If you cannot change the middleware order, capture the raw bytes in the JSON parser's verify callback and keep them on the request. Either way the rule is the same: the signature covers bytes, so verify bytes.

Test it with the dashboard's test delivery before you rely on a real job. A signed webhook.test event goes through the same route and the same verifier, so a green test means the parser order, the secret and the headers are all correct in the environment you will actually run.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume