Avatar video mode: sync waits 30 seconds, so use async or webhook

Sume's sync and subscribe modes wait at most 30 seconds. Avatar video usually takes longer. How to read the timed-out response and what to do next.

4 min readSume
All posts

For an avatar video, send mode: "async" (the default) or mode: "webhook", not sync. A sync call on Sume blocks for at most 30 seconds, and the docs say avatar-video and face-swap jobs "routinely" do not finish inside that budget. When the budget runs out you still get a 2xx and a job id; the response then has sync.timed_out: true, and you must poll status_url. subscribe is an alias of sync: the same 30-second wait, not a stream.

The mode only decides how you find out. It never changes cost, whether a job is created or how long the render takes.

The four modes

What each returns for a talking-video submit.

From Jobs and results, read 2026-10-01.
ModeHTTP resultBlocks the serverNext step
async (default)202 with job envelope and polling URLsNoPoll status_url until terminal, then read result_url
syncEnvelope after up to 30 s (wait_timeout_seconds 0 to 30)Yes, at most 30 sIf not terminal, poll; do not resubmit
subscribeIdentical to syncSameSame
webhook202; callback storedNoVerify the signature on the callback; keep polling as backup

A polling loop that behaves

Honor next_poll_after_seconds when the response has it, and back off otherwise. Stop on completed, failed or canceled. Do not treat a local timeout as a failed job; the render may still be running and billing.

import asyncio, os
import httpx

BASE = "https://api.sume.com"
HEAD = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

async def wait(job_id: str, limit: int = 600) -> dict:
    delay, waited = 5, 0
    async with httpx.AsyncClient(headers=HEAD, timeout=20) as c:
        while waited < limit:
            r = (await c.get(f"{BASE}/v1/jobs/{job_id}/status")).json()["data"]
            if r.get("terminal"):
                return r
            delay = r.get("next_poll_after_seconds") or min(delay * 2, 30)
            await asyncio.sleep(delay)
            waited += delay
    raise TimeoutError(job_id)

asyncio.run(wait(os.environ["JOB_ID"]))

When sync is fine

For avatar video, the rule of thumb in the docs is to submit async with an Idempotency-Key, store the four URLs, and either poll or take a webhook.

  • Short image jobs that usually finish inside 30 seconds.
  • Quick tests where you will poll anyway if it times out.
  • A UI where you can show a spinner and then switch to polling.

Limits

There is no SSE or WebSocket transport on the Developer API today; GET /v1/jobs/:id/events is a pull snapshot. If the API process has no spare waiter capacity it skips the blocking wait entirely and sets sync.capacity_exhausted, so your code has to handle an immediate non-terminal response even when you asked for a wait. In practice that means writing the polling path first and treating a finished sync response as a shortcut. A client built that way works the same whether the render took 20 seconds, four minutes, or hit a busy moment on the API. Log the job id on the first line of your handler, before you look at anything else in the response, so a crash after submit never leaves you with a paid job you cannot find.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume