Kling motion control sync mode: a 30-second wait, then poll
Sume's sync and subscribe modes on Kling 3.0 Motion Control wait at most 30 seconds. A clip usually outlasts that, so poll status_url; do not resubmit.

Using mode: "sync" on Kling 3.0 Motion Control does not make the call wait for the clip. Sume blocks for at most wait_timeout_seconds, clamped to 0 to 30, then returns the current job state with polling URLs. Motion clips can outlast that, so treat the response as a receipt, keep the job id, and poll status_url.
What do the four modes do?
async is the default. sync and subscribe are aliases for the same bounded wait. webhook returns at once and stores a callback. Every mode returns the job id in the first response, so a 2xx means the job exists and paid work is in flight, not that it finished.
| Mode | What the HTTP call does | Next step |
|---|---|---|
async | Returns 202 with the envelope | Poll status_url until terminal is true |
sync | Waits up to wait_timeout_seconds (max 30) | Not terminal: poll, do not resubmit |
subscribe | Same as sync | Same as sync |
webhook | Returns 202, stores the callback | Wait for the terminal callback and keep polling as a backup |
What if the wait runs out?
The response is still 2xx and still carries the job id. sync.timed_out is true, or sync.capacity_exhausted is true if the server skipped the wait because its waiter budget was full. Continue with GET status_url, honoring next_poll_after_seconds when present.
Never submit a new paid job for the same intent. Retrying the submit is fine only with the same Idempotency-Key, which returns the original job.
Should I use a webhook instead?
For anything that can run past 30 seconds, async or webhook is the recommended shape. The webhook is terminal-only: job.completed, job.failed, job.canceled, with no progress callbacks, so keep polling available.
The webhook URL must be public HTTPS; localhost, private-network and non-HTTPS URLs are rejected. Sending webhook_url with no mode selects webhook.
import asyncio, os
import httpx
KEY = os.environ.get("SUME_API_KEY", "")
if not KEY:
raise SystemExit("set SUME_API_KEY")
async def main():
body = {
"image_url": "https://example.com/still.png",
"motion_video_url": "https://example.com/move.mp4",
"duration_seconds": 10,
"mode": "async",
}
h = {"x-api-key": KEY, "Idempotency-Key": "mc-poll-001"}
async with httpx.AsyncClient(timeout=60) as c:
r = await c.post("https://api.sume.com/v1/kling/3.0/motion-control", headers=h, json=body)
r.raise_for_status()
d = r.json()["data"]
while not d["terminal"]:
await asyncio.sleep(d.get("next_poll_after_seconds") or 5)
d = (await c.get(d["status_url"], headers=h)).json()["data"]
print(d["terminal"], d["result_ready"], d["result_url"])
asyncio.run(main())Sources
Related posts
More in Developers
- LangGraph 1.2 node timeout: the Sume video job keeps billing
LangGraph 1.2 adds run_timeout and idle_timeout per node. A timeout stops your node, not the Sume job it started: store the job id and re-poll.
- LangGraph DeltaChannel: keep Sume artifact URLs in state, not video
LangGraph 1.2 DeltaChannel stores only the per-step delta. Even so, keep Sume media out of state: store the artifact URL and job id and fetch bytes when needed.
- LangGraph error_handler after retries: do not resubmit a Sume job
LangGraph 1.2 node error handlers run after retries are exhausted. For a Sume call, the handler should read job status, not submit a second paid request.
- LangGraph request_drain and GraphDrained: resume a Sume job later
LangGraph 1.2 can drain a run after the current superstep. If a Sume job is in flight, save its id so the resumed run polls instead of resubmitting.
Written by Sume