Idempotency key from the order id, not a fresh UUID per attempt

A random UUID generated inside the retry loop gives every attempt a new key and a new paid job. Derive the Sume Idempotency-Key from the order instead.

5 min readSume
All posts

The Sume docs say to send Idempotency-Key on submit requests that you may retry after a client timeout or network failure, and to reuse a key only for the same operation and payload. A common bug is the opposite: the key is generated with crypto.randomUUID() inside the function that gets retried, so each attempt looks like a new operation and Sume creates and bills a new job.

The fix is to derive the key from something that identifies the business operation, not the attempt. An order id plus the asset name plus a version is enough, for example order-8823-hero-v1.

import { createSumeClient, generateVideoV1 } from "@sume-com/sdk";

const client = createSumeClient({ apiKey: process.env.SUME_API_KEY! });

export async function submitHero(orderId: string, prompt: string) {
  // Same order + same payload => same key => same job on every retry.
  const key = `order-${orderId}-hero-v1`;
  for (let attempt = 0; attempt < 3; attempt++) {
    const { data, error } = await generateVideoV1({
      client,
      headers: { "idempotency-key": key },
      body: { prompt, mode: "async" },
    });
    if (data) return data.data.request_id; // the job id
    if (!isRetryable(error)) throw new Error(JSON.stringify(error));
    await new Promise((r) => setTimeout(r, 2000 * 2 ** attempt));
  }
  throw new Error("submit failed after 3 attempts");
}

declare function isRetryable(error: unknown): boolean;

What the server does with the key

A retry with the same key and payload returns the original job. Reusing the key with a different operation or payload returns 409 idempotency_conflict; use a key again only for an exact retry. Agent Completions work the same way and mark a replay with idempotency_hit: true.

Failed creates matter too. On the Formats surface the docs say a failed create releases its key, so retrying after a 4xx that you fixed is a new attempt, not a replay. For queue_full and provider_capacity_exceeded, the docs say to retry later with the same key.

Key rules by surface, as of 2026-10-09 (docs.sume.com)
SurfaceKeyReplay signal
Generation jobs (REST)Idempotency-Key headerOriginal job returned
Agent CompletionsIdempotency-Key headeridempotency_hit: true
Format runs (SDK)idempotencyKey option; auto UUID if omitted, null sends noneFinished run returns immediately
Hosted MCP writes and paid callsidempotency_key field, requiredDedup, not approval

Pitfalls

The SDK's subscribeFormatRun generates a UUID for you when you omit idempotencyKey. That is safe for a single call and unsafe for your own retry wrapper, because each outer retry gets a new default. Pass your own stable key if you wrap it.

  • Change the version suffix when the prompt or options change, or you will get a 409.
  • Do not put secrets or personal data in the key; it appears in logs.
  • A client timeout does not cancel the job. Keep the job id, poll it, and do not resubmit.
  • One key per business operation, not one key per process.

A test you can run

Submit the same payload twice with the same key and compare the two job ids; they should be equal. Then submit it with a changed prompt and the same key and expect 409 idempotency_conflict. Finally, simulate a timeout by dropping the first response and retrying, and check that your usage ledger at GET /v1/usage shows one reservation, not two.

Keep keys short and readable. A key like order-8823-hero-v1 is easy to search in your logs and easy to bump to v2 when the creative changes.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume