Fastify 5 raw body for a Sume webhook: parseAs string
Fastify parses JSON before your handler, which breaks HMAC checks. Use parseAs string, then verify sume-v1 and dedupe. Tested on Fastify 5.12.5.

In Fastify, a Sume webhook route fails signature checks by default because the JSON content-type parser turns the body into an object before your handler runs, and the HMAC covers the original bytes. Register your own parser for application/json with { parseAs: "string" } and the handler receives the raw text, which you verify and only then JSON.parse. The 27-line app below does that on Fastify 5.12.5.
Sume signs <timestamp>.<raw_body> with HMAC-SHA256 and sends x-sume-webhook-timestamp and x-sume-webhook-signature: sume-v1=<hex>. A re-serialized object does not match, since key order and whitespace were part of what was signed.
Facts
| Requirement | Value |
|---|---|
| Raw body | Verify before parsing |
| Replay window | 300 seconds suggested |
| Rotation | Header may carry two sume-v1 entries for 24 hours |
| Response | Any 2xx after storing; 10 attempts, 30 s apart, 10 s timeout |
| Dedupe key | job_id, or run_id on run webhooks |
The app
The verifier is the shared WebCrypto one from verifyWebhook returns false during a Sume secret rotation, saved as verify.mjs. Fastify gives header values as strings or arrays, so the code keeps only strings; a duplicated signature header is then absent, and verification fails closed.
import Fastify from "fastify";
import { verifySume } from "./verify.mjs";
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is not set");
const app = Fastify({ logger: false });
// Keep the bytes: Fastify's default JSON parser would hand you an object.
app.addContentTypeParser("application/json", { parseAs: "string" }, (req, body, done) => done(null, body));
const seen = new Set(); // use a database unique index in production
app.post("/webhooks/sume", async (req, reply) => {
const body = req.body; // raw string
const headers = new Headers(
Object.entries(req.headers).filter(([, v]) => typeof v === "string"));
if (!(await verifySume({ body, headers, secret }))) return reply.code(401).send("bad signature");
const event = JSON.parse(body);
const id = event.job_id ?? event.run_id;
if (id && !seen.has(id)) {
seen.add(id);
setImmediate(() => console.log("handle", event.event, id)); // work after the 2xx
}
return reply.code(204).send();
});
await app.listen({ port: Number(process.env.PORT ?? 8789) });Where it goes wrong
Three practical traps cost the most time in this setup:
- Registering the parser on the whole app also changes every other JSON route. Scope it with
app.registerand an encapsulated plugin if you have other JSON endpoints. - The default body limit is 1 MiB, which is plenty for a terminal job event; do not raise it to make a failing signature go away.
- A proxy that rewrites the body (compression, charset changes) breaks the signature. Compare
x-sume-webhook-secret-fingerprintwith the dashboard value first, then suspect the proxy.
Limits
On Fastify 5.12.5 and Node 22.14, signed local requests returned 204 for a first delivery, 204 for a repeat with no second handling, and 401 for a corrupted signature. These were not deliveries from Sume. The in-memory Set loses state on restart. The route logs and returns; real work belongs in a queue, and the 204 only means you took the event, so keep status polling as the backup for deliveries that never arrive.
Sources
Related posts
More in Integrations
- Flowise Custom MCP: connect Sume over Streamable HTTP
Add Sume to a Flowise Agent node as a Custom MCP tool: a url, an Authorization header from a $vars variable, then refresh Available Actions.
- Instagram Reels API: 1920 px width cap and 25 Mbps video bitrate
Meta's Reel spec caps width at 1920 px and bitrate at 25 Mbps VBR, with 23-60 FPS and 300 MB. Probe width, fps and size with Sume; bitrate is an average.
- Instagram Reels API aspect ratio: 0.01:1 to 10:1, 9:16 advised
Meta's Reels API accepts any aspect ratio from 0.01:1 to 10:1 and only recommends 9:16. See what each ratio rule says and check a clip with video inspect.
- Instagram Reels API audio: AAC, 48 kHz max, mono or stereo
Meta's Reel spec asks for AAC audio, a sample rate of 48 kHz at most, 1 or 2 channels and 128 kbps. Check those fields with a Sume video inspect probe first.
Written by Sume