Vercel Workflow createWebhook is token-only: verify Sume first

createWebhook trusts only the URL token. For a Sume callback, verify the sume-v1 signature in your own route, then resume a hook with resumeHook.

5 min readSume
All posts

Use createHook() behind your own route, not createWebhook(), to resume a Vercel workflow from a Sume callback. The Workflow SDK docs say the token in a webhook URL is the only authorization performed, so anyone holding the URL can resume the run, while Sume's callback carries an HMAC signature your route can check first.

This post covers the choice and the route. It does not cover the workflow function itself beyond the calls the SDK docs show, so check them for your SDK version.

What is the difference between createWebhook and createHook?

Both pause a run until something arrives. Per the docs, hooks accept arbitrary serializable data and you resume them with resumeHook(), while webhooks receive standard HTTP Request objects at a generated webhook.url. The createWebhook reference adds the warning that the URL token is the only authorization, and recommends createHook() behind your own authorized route when you need more.

Workflow SDK hooks and webhooks, read 2026-10-02
createHookcreateWebhook
InputAny serializable dataA standard HTTP Request
Entry pointYour route calls resumeHook(token, payload)Generated webhook.url
AuthorizationWhatever your route checksThe URL token only
Custom tokencreateHook({ token })Not supported; tokens are always generated

Why is the token-only model a poor fit for a Sume callback?

The Sume docs describe the webhook_url target and the signature Sume sends, and do not describe attaching your own auth header. What it sends is x-sume-webhook-signature: sume-v1=<hex> over <timestamp>.<raw_body>, plus a timestamp header, as described in the verifying webhooks docs. A token-only endpoint ignores all of that, so a leaked URL lets someone resume your workflow with any payload, including a fake job.completed.

The Workflow docs call generated tokens hard to guess without knowing the run, and custom hook tokens easier to reconstruct because they are built from domain data. A custom sume-<job_id> token is therefore fine only because your route checks the signature first.

What does the verifying route look like?

Set webhook_url to this route when you submit the job, create a hook in the workflow with a token derived from the job id, and let the route resume it only after the signature passes. verifyWebhook is async and returns false on a bad delivery, and it needs the raw body, so call request.text() first. The secret comes from GET /v1/webhooks/signing-secret or the dashboard. The empty-secret guard stops a missing env var from becoming an accept-everything check.

import { verifyWebhook } from "@sume-com/sdk";
import { resumeHook } from "workflow/api";

export async function POST(request: Request) {
  const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
  if (!secret) return new Response("no secret", { status: 500 });
  const body = await request.text();
  const ok = await verifyWebhook({ body, headers: request.headers, secret });
  if (!ok) return new Response("bad signature", { status: 401 });

  const event = JSON.parse(body);
  if (event.event?.startsWith("job.")) {
    await resumeHook(`sume-${event.job_id}`, event);
  }
  return new Response(null, { status: 204 });
}

Is there ever a reason to use createWebhook?

Yes, when the sender cannot be given a route you control and the payload is not security sensitive, or when the URL stays private between two systems you own. The createWebhook page also supports a manual response mode, respondWith: 'manual', for returning a custom Response, and the default answers 202 Accepted automatically.

For a paid generation callback, the cost of a spoofed completion is a workflow that moves on without a real result, then publishes or bills something. That is worth one route and a signature check. If you do use createWebhook, at least confirm the job with GET /v1/jobs/{id}/status before acting on it, as in the other receivers in this series.

What can still go wrong?

Order matters: the hook has to exist before the callback arrives, or resumeHook has nothing to resume, and the Workflow docs say registration is only committed when the workflow suspends. Create the hook right after the submit returns the job id, suspend on it, and keep a status poll (GET /v1/jobs/{id}/status) in the workflow as a backup, since Sume's own docs call a webhook a delivery optimization rather than the only recovery path.

Sume retries a refused delivery up to 10 times, so a 5xx from a route that cannot find the hook yet will be retried. Answer 204 for event types you do not handle; Sume's SDK docs advise that an unknown event should not become a retry storm. And dedupe on job_id, because Sume's Redeliver and retries can send the same terminal event more than once.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume