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.

4 min readSume
All posts

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

All Developers posts

Written by Sume