API Gateway to Lambda: verify a Sume video webhook on the raw bytes
A Sume callback_url can point at a Lambda behind an HTTP API. Decode the body to bytes first, then verify sume-v1 with the SDK. Handler code is under 30 lines.

To verify a Sume video webhook inside a Lambda handler, turn the event body back into the exact bytes that Sume signed, then pass those bytes to verifyWebhook from @sume-com/sdk. If the event marks the body as base64, decode it. If you parse the JSON first and serialize it again, the signature fails, because key order and whitespace are part of the signed data.
What Sume signs
Sume signs the raw JSON body with HMAC-SHA256 over <timestamp>.<raw_body> and sends the result as sume-v1=<hex> in x-sume-webhook-signature, with the timestamp in x-sume-webhook-timestamp. A video job that you submitted with callback_url on POST /v1/videos delivers the standard Sume job envelope, not an OpenRouter video.generation.* event, so the same verifier works for it.
| Item | Value |
|---|---|
| Signed string | <timestamp>.<raw_body> |
| Signature header | x-sume-webhook-signature: sume-v1=<hex> |
| Timestamp header | x-sume-webhook-timestamp (Unix seconds) |
| Replay window | 300 seconds by default |
| Secret env name | SUME_COM_WEBHOOK_SIGNING_SECRET |
| callback_url | must be HTTPS |
The handler
The handler below refuses to run without a secret, builds a Buffer from the event body, and returns a non-2xx status for a bad signature. verifyWebhook accepts a typed array as the body and a plain object as the headers, and it reads header names without regard to case. It returns false for a malformed delivery and does not throw.
import { verifyWebhook } from "@sume-com/sdk";
export const handler = async (event) => {
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET;
if (!secret) return { statusCode: 500, body: "webhook secret not set" };
// Rebuild the exact bytes Sume signed. Never JSON.parse before verifying.
const raw = event.isBase64Encoded
? Buffer.from(event.body ?? "", "base64")
: Buffer.from(event.body ?? "", "utf8");
const ok = await verifyWebhook({ body: raw, headers: event.headers, secret });
if (!ok) return { statusCode: 401, body: "bad signature" };
const delivery = JSON.parse(raw.toString("utf8"));
// Store delivery.job_id durably first (it is your idempotency key).
console.log(delivery.event, delivery.job_id);
return { statusCode: 204 };
};Checks before you go live
Open a real delivery in your logs and look at two things: whether the event body arrives as text or as base64, and whether your gateway lower-cases header names. Neither one changes the verifier, but the first one decides which line of the handler runs.
Sume waits 10 seconds for each attempt and retries a non-2xx answer, up to 10 attempts, 30 seconds apart. Keep the handler short: verify, write job_id to a table with a unique key, return the 2xx, and do the video copy somewhere else.
- Set the secret from
GET /v1/webhooks/signing-secret(needs an API key withaccount:read) or the dashboard Webhooks tab. - Keep the secret out of the API key variable. They are two different values.
- If a signature fails, compare
x-sume-webhook-secret-fingerprintwith the fingerprint in the dashboard.
Related
Read the timestamp is seconds post if you check the clock yourself, and ack fast, then enqueue for the part after verification. The OpenAI Videos API that many of these callers came from was removed on 2026-09-24 (OpenAI deprecations, read 2026-10-08).
Sources
Related posts
More in Developers
- Sume ignores aspect_ratio when image_size is set: Flux, Seedream
On Sume's custom-pixel image rows, image_size wins and aspect_ratio is ignored. Nano Banana turns WxH into an enum ratio. What each model does with both fields.
- Assemble a three-clip montage with fades through the Sume API
A working Timeline 1.0 request that joins three hosted clips over one audio spine with fade and dissolve transitions, plus the Python to poll it, for $0.10.
- Balance needed to submit 10 or 50 video jobs: reserve per model
Sume reserves each job's estimate at submit. A table of the balance 10 and 50 ten-second clips need on six video models, and where a 402 lands in a batch.
- Read /v1/balance: state, micros and the expiring-soon amount
What GET /v1/balance returns, why an empty state means a 402 is coming, and a Node check that compares available micros to a job's cost before you submit.
Written by Sume