Trigger.dev wait for webhook: waitpoint tokens for Sume runs
Create a Trigger.dev waitpoint token, send token.url as a Sume run's webhook_url, and wait.forToken() returns when Sume POSTs the run's result.

To make a Trigger.dev task wait for a Sume webhook, create a waitpoint token with wait.createToken(), send token.url as the run's communication.webhook_url, and call wait.forToken(). When the run completes or fails, Sume POSTs its receipt to that URL once, and the JSON body becomes the token's output.
Sume has no Trigger.dev integration; the task calls the API with fetch. Sume facts come from Create a run, Runs and results, and Run webhooks; Trigger.dev behavior comes from its docs, read 2026-09-27. The webhook contract is covered in Sume Format run lifecycle.
How do waitpoint tokens map to a Sume run?
Each token rule has a consequence for the run you start.
| Trigger.dev | Sume side |
|---|---|
wait.createToken({ timeout }) returns a token with id and url. timeout defaults to "10m". | Set it past the 90-minute bound on a run. |
A POST to token.url completes the token; its JSON body becomes the output. | Sume POSTs the terminal receipt once, when the run completes or fails. |
wait.forToken() returns { ok, output, error }, and the only possible error is a timeout. | A canceled or skipped run never POSTs, so its token times out. |
Reusing an idempotencyKey on createToken returns the cached token (isCached: true). | A retry gets the same token.url, so the Sume request body stays the same. |
| Trigger.dev Cloud pauses a task that waits longer than a few seconds. | A video run takes minutes. |
What does the task look like?
Store the key as a Trigger.dev environment variable, marked Secret when you create it, and read it from process.env. The task creates the token, starts a catalog Format run with the token's URL, waits, then reads the run with its own key:
import { task, wait } from "@trigger.dev/sdk";
export const productVideo = task({
id: "product-video",
run: async (payload: { orderId: string; photoUrl: string }) => {
const auth = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };
const token = await wait.createToken({
timeout: "2h", idempotencyKeyTTL: "3h",
idempotencyKey: `sume-${payload.orderId}-v1`,
});
const res = await fetch("https://api.sume.com/v1/formats/sume/sume-product-commercial/runs", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": `order-${payload.orderId}-v1` },
body: JSON.stringify({
instruction: "Make a vertical product commercial from the attached photo.",
attachments: [{ type: "input_image", image_url: payload.photoUrl }],
communication: { webhook_url: token.url },
}),
});
const created = await res.json();
if (!res.ok) throw new Error(`Sume ${res.status} ${created.error?.code}`);
const result = await wait.forToken(token); // ok is false only on a timeout
const run = await fetch(`https://api.sume.com/v1/format-runs/${created.data.id}`, { headers: auth });
return { delivered: result.ok, run: (await run.json()).data };
},
});How long should the token wait?
Longer than the default 10 minutes: long-form host video typically finishes in 15 to 30 minutes. Sume force-finalizes a run as failed 90 minutes after created_at, so a two-hour timeout covers every run that ends on its own. Your timeout does not cancel the run; it keeps running and billing, which is why the task reads the run after a timeout too.
Can the task trust the token's output?
Not on its own. The output is the POST's JSON body, while Sume's HMAC signature travels in the x-sume-webhook-signature header, so the task cannot check it from the output. Treat the token as the signal to look, and read GET /v1/format-runs/{run_id} with your key, as above; its data is the same receipt the webhook carried. The read also covers a receipt over 1 MiB, which arrives with payload: null.
How do retries avoid a second paid run?
Trigger.dev retries a task when it throws, and a retry creates the token and the run again. Two idempotency keys and one habit make that safe:
- The Sume
Idempotency-Key: the same key and body returns200with the original run and no second charge. - The token's
idempotencyKey: a retry gets the cached token and its URL, so the Sume body really is the same. In current code the webhook URL is part of the body the Sume key is checked against, so a new URL with the same key gets409 idempotency_conflict.idempotencyKeyTTLdefaults to 1 hour, so set it past the whole wait. - Check
result.okinstead of calling.unwrap(), which throws on a timeout and sends the task into its retries.
Sources
Related posts
More in Integrations
- Vercel AI SDK: generate video with a Sume API tool call
Generate video from the Vercel AI SDK with a tool() that calls Sume's POST /v1/videos on your server, returns the job id, and polls for the clip.
- Vercel Cron Jobs: call the Sume API daily without duplicates
A Vercel cron job sends a GET to your route, which calls the Sume API with a date-based Idempotency-Key, so a duplicate invocation cannot bill twice.
- Vercel function timeout on video generation: use a webhook
A Vercel Function stops at 300 seconds by default; a Sume video run takes minutes. Submit with a webhook_url, return, then verify the signed POST.
- Windsurf MCP server: add Sume's hosted MCP in Devin Desktop
Windsurf is now Devin Desktop. Add Sume's hosted MCP server to the Devin Local agent or the legacy Cascade agent with an API-key header.
Written by Sume