Recast Idempotency-Key from a hash of the request (Python)

Derive the Idempotency-Key from the canonical request body so a retry is a replay and an edited request is a new job. Python code for Recast on /v1/videos.

4 min readSume
All posts

A Recast job is priced per second of source video. fal lists H3 Max Recast at $0.30 per second at 768p and $0.45 per second at 1080p (read 2026-10-03), and Sume bills the list price times 1.25. A 30 second clip submitted twice by a flaky client is a real double charge, so the submit call needs a key that makes a retry harmless.

Sume's Idempotency-Key header does that. Send the same key with the same request and you get the original job back with the same id. Send the same key with a different request and you get 409 idempotency_conflict. The sentence that matters in practice is the second one: a hand-typed key reused after you edit the prompt fails instead of silently submitting the old job.

Let the request name its own key

The safest key is a function of the body. Serialize the request with sorted keys and fixed separators, hash it, and use the digest. An identical retry produces the identical key, and any change to a photo URL, the resolution or the prompt produces a different one, so the 409 case cannot occur by accident. The take argument is a deliberate salt: bump it when you want a new take of the same request, because a second take of an unchanged body would otherwise replay the first.

import hashlib, json, os, urllib.request

API = "https://api.sume.com"


def idem_key(body: dict, take: int = 1) -> str:
    canon = json.dumps(body, sort_keys=True, separators=(",", ":"))
    return f"recast-t{take}-" + hashlib.sha256(canon.encode()).hexdigest()[:32]


def submit(body: dict, take: int = 1) -> dict:
    req = urllib.request.Request(
        f"{API}/v1/videos", json.dumps(body).encode(), method="POST",
        headers={"x-api-key": os.environ["SUME_API_KEY"],
                 "content-type": "application/json",
                 "Idempotency-Key": idem_key(body, take)})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)


body = {"model": "h3-max-recast", "resolution": "768p", "input_references": [
    {"type": "video_url", "video_url": {"url": "https://media.sume.com/source.mp4"}},
    {"type": "image_url", "image_url": {"url": "https://media.sume.com/host.jpg"}}]}
first, again = submit(body), submit(body)
assert first["id"] == again["id"], "same body, same key: one job"
print(first["id"], submit(body, take=2)["id"])

What the sample proves

  • Two submits of one body return one job id, which the assert checks.
  • take=2 changes only the prefix, so you pay for a second job on purpose.
  • The key is 32 hex characters plus a short prefix, short enough to log on every call.
  • Hash the normalized request, not the headers or a timestamp. A timestamp in the key defeats the point.

Edge cases to plan for

A job refused with queue_full releases its key, so replaying the same request under the same key creates a fresh job once capacity returns. That is the behaviour you want for retry loops. Identical requests share one job, so add an order id to the hashed body if two orders must be billed separately.

If your source or photo URLs are signed and expire, hash a stable asset id instead of the URL. Otherwise a retry after the signature rotates looks like a new request and spends again. The video guide lists every field the /v1/videos surface accepts, and the jobs guide explains how replays and polling fit together.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume