Hookdeck passes gzip bytes unchanged: verify Sume on raw bytes
Hookdeck now forwards binary and gzip bodies byte for byte. Read the raw bytes before parsing and verify the Sume sume-v1 signature over them.

Hookdeck's September 24 change helps a Sume receiver: the destination now gets the original bytes, and Sume's signature is computed over raw bytes. The rule on your side stays the same: read the body unparsed and verify before you touch it.
Hookdeck facts are from its changelog entry; Sume facts from Job webhooks and Verifying webhooks, read 2026-10-01. The Hookdeck page says nothing about signatures or header forwarding, so confirm in a test delivery that the x-sume-webhook-* headers arrive.
What did Hookdeck change?
The entry says binary content types and Content-Encoding: gzip or x-gzip are passed through as encoded bytes, including gzipped JSON. Destinations get the original bytes with unmodified Content-Type and Content-Length, and retries and replays send identical bytes. Transformations see body: null for these payloads, though headers, path and query can still be transformed.
| Aspect | Behavior |
|---|---|
| Body | Original bytes, including gzipped JSON |
| Content-Type, Content-Length | Unmodified |
| Retries and replays | Identical bytes |
| Transformations | body is null; headers, path, query still transformable |
Why does that matter for the signature?
Sume signs HMAC-SHA256 over <timestamp>.<raw_body> and sends sume-v1=<hex> in x-sume-webhook-signature with x-sume-webhook-timestamp. A parsed-and-reserialized body will not verify, because key order and whitespace are part of what was signed. Byte-identical retries mean a replayed delivery verifies the same as the first, inside the 300-second default replay window.
What does the receiver look like?
Read an ArrayBuffer, which verifyWebhook accepts, and refuse to run without a secret.
Return a fast 2xx after storing the event. Dedupe on job_id.
import { verifyWebhook } from "@sume-com/sdk";
export async function POST(request: Request) {
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) return new Response("not configured", { status: 500 });
const body = await request.arrayBuffer(); // raw bytes, before any parse
const ok = await verifyWebhook({ body, headers: request.headers, secret });
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(new TextDecoder().decode(body));
console.log(event.event, event.job_id);
return new Response(null, { status: 204 });
}Should the relay transform the body at all?
Not for a signed delivery. Keep any Hookdeck rules to headers, path or query, and leave the body alone. Also remember Sume's attempt timeout is 10 seconds per try, with up to 10 attempts, and any 2xx counts as success.
Sources
Related posts
More in Developers
- Hume Octave 2 description field vs Sume TTS emotion controls
Hume lists a voice description field; acting instructions are coming soon. Sume TTS 1.0 has generation_config: emotion text, speed 0.6 to 1.5, volume 0.5 to 2.
- Hume Octave takes 5,000 characters per utterance in MP3, WAV or PCM
Hume's TTS overview lists 5,000 characters per utterance and MP3, WAV or PCM output. Sume TTS 1.0 takes 20,000 characters and mp3, wav or raw containers.
- Ideogram 4 describe image to JSON prompt vs Sume reference input
Ideogram's describe endpoint returns a structured json_prompt with optional bounding boxes. The Sume docs list no such route; pass the image as a reference.
- Ideogram ad localizer API: exact_copy vs a Sume edit prompt
Ideogram's ad-localizer takes one language per call and an exact_copy mapping. On Sume you send the ad as a reference and spell out the copy in the prompt.
Written by Sume