Recast sync wait caps at 30 seconds: use a webhook for long sources
Sync and subscribe modes wait at most 30 seconds, so submit h3-max-recast jobs as async or webhook and poll /v1/jobs/{id}/status as a fallback.

A Recast job on Sume should be submitted as async or webhook, not sync. Every submit endpoint accepts a mode, but sync and subscribe block for at most wait_timeout_seconds, and that value is capped at 30 seconds (Jobs and results, read 2026-10-03). A 5 to 30 second source being rendered by a video model is not something to plan around finishing in half a minute, so design for the 202 that comes back when the wait runs out.
The wait is not a failure. The docs are explicit that a submit is accepted the moment Sume has a durable job id, so every mode returns a job you can follow. sync simply gives you one bounded chance to see a terminal state on the same response.
The four modes for a Recast job
Pick the mode by what your caller can do. A server with a public HTTPS endpoint should use webhook. A script on a laptop should use async and poll. A dashboard that wants a quick answer on a short clip can try sync with a short wait and fall back.
| Mode | What the client does | Good for Recast? |
|---|---|---|
async (default) | Gets the job id immediately, polls status | Yes, the simplest |
webhook | Sends webhook_url; Sume posts a terminal event | Yes, best for servers |
sync | Waits up to 30 s on the submit call | Only as a bounded first look |
subscribe | An alias of sync; no progress events | Same as sync |
Submit with a webhook and keep a polling fallback
The docs advise keeping the polling fallback alongside a webhook, because delivery can fail. The script submits in webhook mode, prints the job id and then polls the documented status route. The response envelope can nest the job, so it looks for job and falls back to data.
import asyncio
import os
import httpx
API = "https://api.sume.com"
async def main() -> None:
key = os.environ.get("SUME_API_KEY")
if not key:
raise SystemExit("set SUME_API_KEY")
headers = {"Authorization": f"Bearer {key}"}
body = {
"model": "h3-max-recast",
"video_url": "https://example.com/source.mp4",
"reference_image_urls": ["https://example.com/person.jpg"],
"resolution": "768p",
"mode": "webhook",
"webhook_url": "https://example.com/hooks/sume",
}
async with httpx.AsyncClient(headers=headers, timeout=60) as c:
r = await c.post(f"{API}/v1/video-router/generate", json=body,
headers={"Idempotency-Key": "recast-hook-001"})
r.raise_for_status()
env = r.json()
data = env.get("data", env)
job = data.get("job", data)
print("job", job.get("id"), job.get("status"))
s = await c.get(f"{API}/v1/jobs/{job['id']}/status")
print(s.json())
asyncio.run(main())Webhook hygiene
A webhook receiver should verify the signature before it trusts the body. Sume signs the raw JSON body and sends x-sume-webhook-timestamp and x-sume-webhook-signature headers; the webhooks guide lists the terminal events (job.completed, job.failed, job.canceled) and says there are no progress or partial deliveries. The webhook URL must be public HTTPS, so a localhost receiver is rejected at submit. If the receiver is down, the job still completes; you read the result by polling GET /v1/jobs/{id}/result.
Keep the endpoint idempotent too: a delivery can arrive more than once, so key your processing on the job id. See verify the signature before you download for the verifier.
What a failed webhook looks like in practice
Imagine your receiver returns a 500 for an hour because of a deploy. The Recast job did not care: it completed on its own clock, the result sits at /v1/jobs/{id}/result, and your poller can fetch it. What you lose is the push, not the data. That is why the docs recommend a polling fallback and why the script above prints the status immediately after submit: it proves the job exists before you rely on a callback that may never arrive.
The opposite failure is the more expensive one. If your code treats a missing callback as a failure and resubmits, you pay twice. Resubmitting with the same Idempotency-Key is safe, because a replay returns the original job, but a new key is a new job and a new reserve. Decide on the key before the first submit and store it next to your own record of the clip.
Finally, expect that a long source is more likely to hit your own timeouts than Sume's. A load balancer in front of your service may cut a connection at 30 or 60 seconds, which is another reason not to hold the submit open. Return to the caller as soon as you have the job id and let the webhook or the poller close the loop.
A rule of thumb
Use sync only when the answer is allowed to be "not yet". For Recast that is nearly always. Use a webhook when a server owns the workflow, and async plus polling when a person is watching a script. Whichever you choose, send an Idempotency-Key so a retry after a timeout returns the original job instead of charging a second time.
- Never block a request thread on a Recast job.
- Accept
202as a normal outcome. - Verify before you download the result.
Sources
Related posts
More in Developers
- Recraft V4 on Sume returns WebP only: convert to PNG or JPEG in Python
Recraft V4 on Sume outputs WebP and takes no references. A Pillow converter for PNG or JPEG, with transparency flattened onto white for JPEG delivery.
- Reel judders after mixing 24 and 30 fps clips: set Timeline output fps
Timeline repeats or drops frames when output fps differs from a source. Learn output_fps_resamples_sources, how the default is chosen, and when to pin 30.
- reference_video_urls or video_url? Reference footage vs edit source
On Sume, reference_video_urls guide a new clip; video_url is a source you edit or swap. They cannot be combined on Gemini Omni Flash. Which model takes which.
- Replace a product-page GIF with a muted MP4: autoplay loop
web.dev says a muted looping video replaces a GIF at a fraction of the size: 3.7 MB vs 551 KB in its example. The tag, the attributes and a silent Sume trim.
Written by Sume