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.

5 min readSume
All posts

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.

Serverless split for a long Sume job (read 2026-10-07)
FunctionTriggerDoesMust finish within
SubmitYour user's requestCreate the job, store the row, return 202Seconds
ReceiverSume webhookVerify the signature, record the event, return 2xx10 s (Sume's attempt budget)
ReconcilerA schedulePoll jobs older than you expect and write terminal stateSeconds per job
WorkerA queue messageDownload media, update your productYour 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

All Developers posts

Written by Sume