Val Town free 1-minute timeout: a Sume webhook receiver val
Val Town's free plan stops a val at 1 minute and runs crons every 15 minutes at best. Submit Sume jobs async, then take the result by webhook or a slow cron.

On Val Town's free plan a val has 1 minute to finish, and the shortest cron interval is 15 minutes, so you cannot wait for a Sume video inside one val. Submit with mode: "webhook", answer the callback in a second HTTP val, and use a cron only as a backup poll.
Val Town's limits page gives different numbers for Pro and Business: 10 minutes per run and a 1-minute cron interval. Even 10 minutes is shorter than some video jobs, so the pattern below is the same on every plan.
What are the Val Town limits that matter here?
| Limit | Free | Pro and Business |
|---|---|---|
| Execution timeout | 1 min | 10 min |
| Minimum cron interval | 15 min | 1 min |
| Crons per val | 10 | 50 |
| Log retention | 3 days | 10 days |
| Request depth (all plans) | 15 | 15 |
How do I submit without waiting?
Send the job from a short val or from any client, with mode: "webhook", a webhook_url pointing at your receiving val, and an Idempotency-Key. The submit returns in well under a minute because it only creates the job. Store the returned job id with whatever you were processing, since the callback names it as job_id.
A cron val that submits many jobs should respect your Sume plan's concurrency. Jobs beyond it sit as queued, which the docs call a normal accepted state, and a 429 queue_full means the workspace queue is full; back off using retry-after when it is present rather than looping.
If one val needs to start a long chain, remember Val Town's request depth cap of 15 on vals that trigger other vals. A webhook callback into a second val counts as one hop, which is nowhere near the cap, but a recursive design would reach it.
Why does the 1-minute timeout rule out sync mode?
Sume's sync mode blocks for at most 30 seconds, and video, avatar and face-swap jobs routinely run longer; the jobs and results page says so. Even when the wait ends early, you would be spending half your minute for nothing. A submit in webhook mode returns a 202 with the job id at once and stores the callback.
What does the receiving val look like?
This is a plain fetch handler using Web Crypto (adjust the Deno.env lookup to however your runtime exposes secrets), so the verifier is the standard HMAC check from the webhook docs: HMAC-SHA256 over <timestamp>.<raw_body>, any sume-v1= entry may match during a secret rotation, and an empty secret is refused. Persist the event before answering, and dedupe on job_id.
const hex = (b: ArrayBuffer) => [...new Uint8Array(b)].map(x => x.toString(16).padStart(2, '0')).join('');
export default async function (req: Request): Promise<Response> {
const secret = Deno.env.get('SUME_COM_WEBHOOK_SIGNING_SECRET') ?? '';
if (!secret) return new Response('no secret', { status: 500 });
const raw = await req.text();
const ts = req.headers.get('x-sume-webhook-timestamp') ?? '';
const sig = req.headers.get('x-sume-webhook-signature') ?? '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return new Response('stale', { status: 401 });
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const want = 'sume-v1=' + hex(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(ts + '.' + raw)));
if (!sig.split(',').some(s => s.trim() === want)) return new Response('bad signature', { status: 401 });
const event = JSON.parse(raw);
console.log('sume event', event.event, event.job_id); // store it, dedupe on job_id
return new Response(null, { status: 204 });
}Which plan do I need?
The pattern works on the free plan because no val waits for Sume. What the plan changes is the safety net. A free cron every 15 minutes means a missed callback might sit that long before your poll catches it, while Pro and Business allow a 1-minute cron. If you do not mind a delay on rare misses, free is enough.
Pay attention to logs too. Free keeps 3 days and Pro 10 days per Val Town's page, so if a callback is disputed a week later, the val log may be gone. Store the event and job_id in your own storage and use Sume's GET /v1/jobs/{id}/events timeline, which includes webhook.delivery, for the delivery history.
What should the cron do?
On the free plan, a 15-minute cron is too slow to be the main path but fine as a safety net: list jobs that have no stored result and read GET /v1/jobs/{id}/status for each. Sume makes up to 10 delivery attempts with a fixed delay between them (30 seconds by default), so a callback that still fails after that needs the poll or a Redeliver call.
The string comparison above is not constant-time. The SDK's verifyWebhook compares in constant time; use it where you can import it, and treat this val as a minimal example, not a hardened one.
Sources
Related posts
More in Integrations
- 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.
- Windmill webhook token in the URL: calling it from a Sume job
Windmill prefers a bearer header, but Sume's webhook_url is just a URL, so the token must ride in the query string. How to scope it and still trust the result.
- Storage by Zapier 32-character keys: remember a Sume job id
Storage by Zapier keys are limited to 32 characters and 500 keys, and idle keys vanish after 2 months. Key by your row id, store the Sume job id as the value.
- Zed MCP: which agents use your Sume server (Panel, ACP, terminal)
Zed's own Agent uses context_servers directly, external agents get them over ACP, and terminal CLIs read their own config. Where the Sume MCP entry goes.
Written by Sume