OpenRouter puts an idempotency key on video webhooks; Sume uses job_id
OpenRouter's video guide adds an idempotency key header to each delivery. Sume dedupes on job_id and takes Idempotency-Key on submit. Which key goes where.

There are two different idempotency problems in an async video API, and the keys for them live in different places. The first is the submit: a client retries a POST and must not create (and pay for) a second job. The second is the delivery: a server retries a webhook and the receiver must not process a result twice. OpenRouter's video guide, read 2026-10-05, says each webhook delivery includes an idempotency key header so receivers can dedupe retries. Sume covers the submit with an Idempotency-Key request header on /v1/videos, and the delivery with a stable job_id in the payload.
Submit side
On Sume, Idempotency-Key on POST /v1/videos makes a retry return the original job. Reusing a key with a different body gives 409. An idempotent replay gets the same route and the same price, even for sume/auto, because the resolution is a pure function of the normalized request plus the catalog version. This is Sume's addition to the OpenRouter wire format; clients written only against OpenRouter's docs do not send it unless you add it.
| Layer | Problem | OpenRouter guide | Sume |
|---|---|---|---|
| Submit | Retried POST creates a second paid job | Not described on the fetched page | Idempotency-Key header; 409 on a different body |
| Delivery | Retried webhook processed twice | Idempotency key header on each delivery | Dedupe on job_id (jobs) or request_id (runs) |
| Signature | Forged delivery | X-OpenRouter-Signature, HMAC-SHA256, optional | x-sume-webhook-signature: sume-v1=..., HMAC-SHA256 |
| Event names | Branch on outcome | video.generation.completed and siblings | job.completed, job.failed, job.canceled |
Receiver side
If you built a receiver for OpenRouter and read the idempotency header, port it by changing the key source, not the logic. Take job_id from the Sume payload, insert it into a table with a unique constraint, and skip the work when the insert conflicts. Sume's docs say it explicitly: use job_id as the idempotency key on your side. Redeliver re-sends the real event with a new timestamp and signature, so the signature differs between deliveries while job_id stays the same, which is why the signature cannot serve as a dedupe key.
- Do not use the signature or the timestamp as a dedupe key. Both change on every attempt.
- Add
Idempotency-Keyto the submit helper, one key per clip. - Keep the polling fallback:
GET /v1/videos/{job_id}returns the OR-shaped job object withusage.cost. - Map
cancelledin OpenRouter-shaped responses to Sume'scanceledjob status.
Test both layers
Write two tests. One submits the same body twice with the same key and asserts one job id. The other delivers the same signed payload twice to your receiver and asserts one side effect. If both pass, a flaky network can no longer cost you a duplicate render or a duplicate email.
Sources
Related posts
More in Developers
- OpenRouter video callbacks vs Sume job webhooks: what differs
OpenRouter video callback_url reports completed, failed, cancelled, expired. Sume job webhooks send completed, failed, canceled, signed with HMAC-SHA256.
- Submit 60 Gemini Omni 4K jobs on a Pro plan: pace in waves of 24
A Pro workspace holds 24 accepted generation jobs (4 running, 20 queued). Pace 60 Omni 4K submits in waves with asyncio and retry queue_full safely.
- Perl HTTP::Tiny: submit a MiniMax H3 video job and save it
A 23-line Perl script with core modules only: POST /v1/videos for minimax-h3, poll, download. A 5-second 768p clip is $0.375; 480p is $0.3125.
- PHP cURL: save a Sume-generated image to disk with CURLOPT_FILE
Two cURL calls in plain PHP: POST to /v1/images, read data[0].url on a 200, then stream the file to disk with CURLOPT_FILE. Handles 202 and a missing key.
Written by Sume