Nuxt Nitro server route for an AI video webhook with readRawBody
A Nuxt server/api route calls readRawBody, checks Sume's sume-v1 HMAC and answers 401 on a mismatch before it queues the finished video download.

In a Nuxt server route (Nitro, built on h3), use readRawBody(event) to get the unparsed request body and getRequestHeader(event, name) for the signature headers. The h3 v1 docs say readRawBody returns a string by default and a Buffer if you pass a falsy encoding (h3 request utilities, read 2026-10-06). The string form is what a text HMAC wants.
This is the receiver for a Sume callback_url. Sume signs <timestamp>.<raw_body> with HMAC SHA-256 and sends sume-v1=<hex> in x-sume-webhook-signature (Sume webhooks guide, read 2026-10-06). Check h3's version: Nuxt 3 ships h3 v1, and later majors may rename helpers.
What does server/api/sume-hook.post.ts contain?
The .post.ts suffix limits the route to POST. createError with 401 stops the handler on a bad signature. The secret is read from the environment and an empty value is refused.
import { createHmac, timingSafeEqual } from 'node:crypto'
export default defineEventHandler(async (event) => {
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? ''
const ts = getRequestHeader(event, 'x-sume-webhook-timestamp') ?? ''
const sig = getRequestHeader(event, 'x-sume-webhook-signature') ?? ''
const raw = (await readRawBody(event)) ?? ''
const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300
const want = Buffer.from('sume-v1=' +
createHmac('sha256', secret).update(`${ts}.${raw}`).digest('hex'))
const ok = !!secret && fresh && sig.split(',').some((p) => {
const got = Buffer.from(p.trim())
return got.length === want.length && timingSafeEqual(got, want)
})
if (!ok) throw createError({ statusCode: 401 })
const body = JSON.parse(raw)
if (body.event === 'job.completed') {
// queue a download for body.job_id
}
return { received: true }
})Which events arrive for a video?
| event | status in the body | Meaning for a /v1/videos job |
|---|---|---|
| job.completed | OK | Output is ready; payload.artifacts lists it |
| job.failed | ERROR | No output; the hold is released |
| job.canceled | ERROR | Cancelled before generation started |
What about duplicates and missed deliveries?
Treat job_id as the key and make the handler idempotent. Keep a periodic status read as a safety net: the webhook is terminal-only, so a lost delivery would otherwise leave a clip marked running forever.
Sources
Related posts
More in Developers
- Omni scene extension on Sume: chain clips with a 3-second tail
Google's extension reads 10 seconds of prior context. Sume's Omni row takes video references of 3 seconds. Cut the tail with video-trim and chain clips by hand.
- One API key for Seedance 2.5, Wan 3.0, Kling 3 and MiniMax H3
Do you need a ByteDance, Alibaba, Kuaishou and MiniMax account to call their video models? On Sume one key and one wallet cover all four. How it works.
- One parser for Sume run webhooks and the poll response
A Sume run webhook payload is byte-identical to the data object from the poll endpoint, so one parser serves both. Handle outcome ok, degraded and error once.
- One output object for every episode: 1080x1920 at 30 fps in Timeline
Pin the same Timeline output block on every Shorts episode: 1080x1920 at 30 fps. Mixed-model clips then do not trigger the fps resample warning.
Written by Sume