Start a Sume render from a serverless function: submit, save, 202
A function must not wait for a video. Submit with mode webhook and a stable Idempotency-Key, save the status URL, return 202, and let the signed webhook finish.

Do the minimum inside the function: send the paid request with mode: "webhook" and a stable Idempotency-Key, save the returned status URL in your database, and return 202. A signed webhook from Sume finishes the work later. Whatever your platform's time limit is, a render that can run for minutes should never be held open by a request.
That matters most on platforms with short, hard limits, where the platform sets a hard request limit of its own. Sume's own sync wait is capped at 30 seconds for the same family of reasons, so a function that blocks on a video is racing two clocks and will lose one of them.
Three jobs, three functions
Split the flow so each function has one short task, and so that a platform timeout can kill none of the important work.
| Function | Trigger | Does | Must finish within |
|---|---|---|---|
| Submit | Your user's request | Create the job, store the row, return 202 | Seconds |
| Receiver | Sume webhook | Verify the signature, record the event, return 2xx | 10 s (Sume's attempt budget) |
| Reconciler | A schedule | Poll jobs older than you expect and write terminal state | Seconds per job |
| Worker | A queue message | Download media, update your product | Your platform's limit |
The submit function
The function below takes an order id and a prompt, derives a key from the order, and skips the Sume call if it has already stored the job. It sends webhook_url, which must be public HTTPS, and mode: "webhook", so the response is the 202 job envelope with its poll URLs and no wait. The in-memory map stands in for a table with a unique key.
const jobs = new Map(); // swap for a database table keyed by idempotency key
export async function POST(request) {
const { orderId, prompt } = await request.json();
const key = `render-${orderId}`;
const known = jobs.get(key);
if (known) return Response.json(known, { status: 202 }); // same order, same job
const res = await fetch("https://api.sume.com/v1/images", {
method: "POST",
headers: {
"x-api-key": process.env.SUME_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({
model: "sume/auto",
prompt,
mode: "webhook",
webhook_url: "https://example.com/hooks/sume", // public HTTPS
}),
});
const data = await res.json();
if (!res.ok) return Response.json({ error: data.error?.code }, { status: 502 });
const row = { orderId, statusUrl: data.status_url, state: "submitted" };
jobs.set(key, row); // persist before returning, not after
return Response.json(row, { status: 202 });
}Why persist before returning
If the function dies between the Sume call and the write, the job exists and your database does not know about it. A retry of the same request sends the same Idempotency-Key, and Sume returns the original job instead of billing a second one, which is why the key comes from the order and not from a random number. A failed create releases its key, so a retry after a 4xx that you corrected is a clean start.
Never let the webhook be the only record that a job exists. The receiver should be able to handle an event for an id it has not stored yet, by writing it down and reconciling later.
Keep the public webhook URL stable: the signature is the authentication, not the path. The URL must be public HTTPS, so a local address will be rejected, and a tunnel is the usual way to test a function before you deploy it. Store the signing secret in the function's environment as SUME_COM_WEBHOOK_SIGNING_SECRET, and let the receiver refuse to start when it is empty.
Last, give the user a way to see progress that does not depend on Sume at all. Your row has three states, submitted, working and done, and the page that shows them can poll your own database cheaply, so the number of visitors watching a render never turns into read traffic on your Sume key.
Close the loop
Delivery is at-least-once, up to ten attempts at a fixed 30-second spacing for jobs, so the receiver needs a unique key on the job id and a terminal state that only moves forward. The real work, such as copying media, belongs to a queue consumer so that the receiver can answer in a fraction of its 10-second budget.
Add the reconciler anyway. A webhook can be exhausted, and a job still ends. A schedule that reads the status of anything older than your expected duration, with terminal and result_ready, makes delivery an optimization and not a dependency. If a delivery was missed after the job finished, you can also ask Sume to send the real event again with POST /v1/jobs/{id}/webhook/redeliver, which needs a key with jobs:write.
Sources
Related posts
More in Developers
- A Sume job looks stuck: wait, cancel or poll the events?
Read status, then events. Queued and processing mean wait, cancel only works before generation starts, and a client timeout never cancels the job.
- Sume timeouts in one table: 30 s, 55 s, 10 s, 90 minutes
Every wait in the Sume API has its own number: sync 30 s, jobs_wait 55 s, webhook attempts 10 s, SDK helpers 10 and 20 minutes, Format runs 90 minutes.
- Can a Sume webhook arrive twice? Build an idempotent receiver
Sume retries failed webhook deliveries up to 10 times and Redeliver replays a real event, so one terminal event can reach you twice. Dedupe on job_id or run_id.
- 10 hooks by 10 endings: a 100-variant grid in one Sume bulk queue
A 10 by 10 hook and ending grid is exactly 100 items, the bulk queue maximum. How to build the items array, pick concurrency up to 16, and read the result.
Written by Sume