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.

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.
| createHook | createWebhook | |
|---|---|---|
| Input | Any serializable data | A standard HTTP Request |
| Entry point | Your route calls resumeHook(token, payload) | Generated webhook.url |
| Authorization | Whatever your route checks | The URL token only |
| Custom token | createHook({ 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
- VS Code mcp.json: servers or mcpServers key for Sume's hosted MCP?
VS Code's .vscode/mcp.json uses a top-level servers key; the portable .mcp.json uses mcpServers. Put Sume's entry under the key that matches its file.
- WhatsApp image message: JPEG or PNG, 5 MB, 1024-character caption
WhatsApp Cloud API image messages take 8-bit RGB or RGBA JPEG or PNG up to 5 MB, with a caption up to 1024 characters. Make a compliant file with Sume Images.
- WhatsApp sticker limits: WebP, 100 KB static, 500 KB animated
WhatsApp Cloud API stickers must be WebP, 100 KB if static and 500 KB if animated. Sume Images can output static WebP; it does not make animated ones.
- WhatsApp webhook media IDs expire in 7 days: download and store
WhatsApp webhook media IDs expire after 7 days and media URLs after 5 minutes. Download inbound media promptly, then import it into Sume to edit it.
Written by Sume