SvelteKit +server.ts endpoint for an AI video webhook: request.text()
A SvelteKit POST handler reads request.text(), verifies Sume's HMAC over timestamp.body with node:crypto and returns 401 for bad or missing signatures.

A SvelteKit webhook receiver is a +server.ts file that exports POST and calls request.text() to get the raw body before any JSON parsing. SvelteKit's routing docs describe server endpoints as functions that receive a RequestEvent and return a standard Response, and the request is a standard Request, so await request.text() returns the exact string that was sent (SvelteKit routing docs, read 2026-10-06).
Use it as the callback_url for a Sume video: the job calls it once on job.completed, job.failed or job.canceled, signed with sume-v1 HMAC SHA-256 over <timestamp>.<raw_body> (Sume webhooks guide, read 2026-10-06).
What is in src/routes/hooks/sume/+server.ts?
The secret comes from $env/dynamic/private, so it never reaches the client bundle. An empty or missing secret returns 401 rather than verifying against an empty key.
import { createHmac, timingSafeEqual } from 'node:crypto';
import { env } from '$env/dynamic/private';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request }) => {
const secret = env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? '';
const ts = request.headers.get('x-sume-webhook-timestamp') ?? '';
const sig = request.headers.get('x-sume-webhook-signature') ?? '';
const raw = await request.text();
if (!secret || !/^\d+$/.test(ts)) return new Response(null, { status: 401 });
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300)
return new Response(null, { status: 401 });
const want = Buffer.from('sume-v1=' +
createHmac('sha256', secret).update(`${ts}.${raw}`).digest('hex'));
const ok = sig.split(',').some((p) => {
const got = Buffer.from(p.trim());
return got.length === want.length && timingSafeEqual(got, want);
});
if (!ok) return new Response(null, { status: 401 });
const event = JSON.parse(raw);
if (event.event === 'job.completed') {
// enqueue a download for event.job_id
}
return new Response(null, { status: 200 });
};Does CSRF protection block the route?
SvelteKit's built-in origin check targets form submissions from browsers. A server-to-server JSON post is not a form post, so a normal webhook delivery is not blocked. If your deployment sets a custom origin policy, test with a delivery from the Sume dashboard before relying on it.
Where does it run?
On any SvelteKit adapter that gives you node:crypto (Node, most serverless Node runtimes). On an edge runtime without Node crypto, swap in Web Crypto's crypto.subtle HMAC; the signed string stays the same. Whatever you run, return fast and hand the download off to a queue, and keep a status poll as backup for a missed delivery.
Sources
Related posts
More in Developers
- Swap the AI image model without a redeploy: JSON config hot reload
Read the Sume image model id from a JSON file that reloads when it changes, so a gpt-image-1 shutdown fix is a one-line edit with no deploy. Python, stdlib.
- Test a Sume poll loop without waiting: inject sleep, assert delays
Unit test a job poll loop in milliseconds by injecting the fetch and the sleep. Assert that next_poll_after_seconds is obeyed and the 20-minute deadline holds.
- Text-to-speech API with curl and jq: one shell script to an MP3
Call the Sume TTS Router from a shell: submit with curl, loop on the status URL with jq until terminal, then download the audio artifact to voiceover.mp3.
- Sume Timeline output: H.264, AAC 192k and 1-second keyframes
What a Timeline 1.0 MP4 contains for a Reel, Short or TikTok upload: libx264 CRF 20, yuv420p, AAC 192k, faststart, 1-second keyframes, and what you cannot set.
Written by Sume