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.

5 min readSume
All posts

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.

Webhook facts that matter for a Lambda receiver, from the Sume docs (read 2026-10-08)
ItemValue
Signed string<timestamp>.<raw_body>
Signature headerx-sume-webhook-signature: sume-v1=<hex>
Timestamp headerx-sume-webhook-timestamp (Unix seconds)
Replay window300 seconds by default
Secret env nameSUME_COM_WEBHOOK_SIGNING_SECRET
callback_urlmust 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 with account: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-fingerprint with 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

All Developers posts

Written by Sume