Idempotency-Key per shot: rerun one failed AI video shot in Python

One key per shot, derived from project, shot number and revision: a retry returns the same Sume job, a changed prompt gets a new key. Python key helper inside.

5 min readSume
All posts

Give every shot in a multi-shot video its own Idempotency-Key, built from the project, the shot number and a revision counter. A network retry of the same shot sends the same key and Sume returns the original job instead of creating and reserving a second one. When you edit the prompt, bump the revision so the key changes. Reusing a key with a different payload returns 409 idempotency_conflict.

This is the pattern to use when Kling 4.0 style plans become many separate jobs. The fal explainer (read 2026-10-05) lists up to 10 keyframes and 30 second clips in one model; on Sume today a long video is a list of shots, and each shot needs to be safe to retry on its own.

The rule in the docs

The video docs list the difference from OpenRouter in one line: send Idempotency-Key to make retries safe, and a replay returns the original job. The generation admission docs give the matching client behaviour for errors: retry queue_full, rate_limited and provider_capacity_exceeded with the same key, and use a key again only for an exact retry.

That rule defines the key design. If the key were random per attempt, a retry after a timeout could create a second paid job. If it were fixed per shot slot and never changed, an edited prompt would hit 409 idempotency_conflict. A key that includes a hash of the request body, or an explicit revision, handles both.

When to keep or change the key

Idempotency-Key behaviour for POST /v1/videos, per the Sume video and admission docs, read 2026-10-05
SituationKeyResult
Timeout or dropped response, same bodySameReplay returns the original job
429 queue_full or rate_limitedSameRetry after backoff; no second reserve
Edited prompt or new frame imageNewA new job is created
Same key, different bodySame409 idempotency_conflict
Failed job you want to rerun unchangedDecide per policySee the note below

Rerunning one failed shot

The last row is a policy choice. The docs only say that a replay of the same key returns the original job, so rerunning a job that ended failed is safest with a new revision number rather than relying on the old key. Record the revision in your own store, next to the job id, and the next attempt is a one-line change.

A failed shot should not block the rest. Poll each job independently, collect the ones that are completed, and resubmit only the shot that failed. Because every shot is separate, you pay once for each finished shot and only reserve again for the retry.

A key helper

This helper builds the key and the payload for each shot, and shows that the key changes only when the body changes. It runs offline.

import hashlib, json

def shot_key(project, shot_no, payload, revision=1):
    body = json.dumps(payload, sort_keys=True).encode()
    digest = hashlib.sha256(body).hexdigest()[:10]
    return f"{project}-s{shot_no:02d}-r{revision}-{digest}"

shots = [
    {"model": "seedance-2.5", "prompt": "Wide street at dusk", "duration": 6,
     "resolution": "720p", "aspect_ratio": "21:9"},
    {"model": "seedance-2.5", "prompt": "Close on the neon sign", "duration": 4,
     "resolution": "720p", "aspect_ratio": "21:9"},
]
keys = [shot_key("promo", i + 1, p) for i, p in enumerate(shots)]
print(keys)
print(keys[0] == shot_key("promo", 1, shots[0]))
edited = dict(shots[0], prompt="Wide street at dawn")
print(keys[0] == shot_key("promo", 1, edited, revision=2))

Webhooks and restarts

You can skip polling for most shots by passing callback_url, an HTTPS address that receives a webhook when the job completes. Sume's standard job webhook envelope carries an x-sume-webhook-signature header, which your receiver should verify with the webhook secret before it trusts the body. Keep polling as a fallback for shots whose webhook you did not see.

Either way, store the shot key beside the job id. When your worker restarts, it can rebuild the key from the project, shot number, payload and revision, resubmit, and get the original job back. That makes the whole storyboard loop safe to run twice: a second run costs nothing for shots already submitted, and only creates jobs for shots that were never sent. It also keeps the audit trail simple, because one shot maps to one key and one job.

Sending it

Send the key as a header on POST /v1/videos along with your bearer token, then poll GET /v1/videos/{id} as in the video generation docs. The same job is also visible on GET /v1/jobs/{id}/status and /result, which the jobs docs describe. Keep the key, the job id and the usage.cost together in your shot table, and you can audit every shot of a video later.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume