genmux API is now generally available. Read the docs →
genmux API

Blog

Idempotency keys and exactly-once billing for image APIs

2 min readWritten by Unified Image API Team


Every metered API has the same nightmare scenario: the client times out, retries, and gets billed twice for one piece of work. Or the inverse — the provider fails mid-call and the customer pays for nothing. This post explains the guarantees we make, how they are implemented, and what they let you delete from your own code.

The guarantee

One credit is captured for at most one delivered image. Concretely:

  • Submitting reserves the job's cost atomically (a conditional single-row update — the balance can never go negative, and two concurrent submits can't both spend the last credits).
  • A job that fails or is cancelled refunds its reservation automatically. A ledger row unique per (job, kind) makes the refund itself exactly-once, even if two workers try.
  • The upstream model call is written ahead: before we call a provider, we record the attempt; if the process dies mid-call, reconciliation resolves the "unknown" state against the provider instead of guessing. No unknown call is ever silently billed.

Idempotency keys close the client half

Server-side guarantees are useless if a client retry creates a second job. That's what the mandatory Idempotency-Key header is for:

  • Same key + same body → the original job is returned (200, not 202). No new charge, ever.
  • Same key + different body → 422 idempotency_key_mismatch, because silently returning the old job would hide a bug in your code.
  • Keys are remembered long enough to cover any realistic retry window.

The practical rule: generate the key where the user intent originates (one key per "the user clicked generate"), not per HTTP attempt. Then let your HTTP layer retry as aggressively as it likes.

text
intent → key K
  attempt 1 (K) — network timeout, you never saw the response
  attempt 2 (K) — 200, returns the job created by attempt 1

What you can delete from your code

  • Reconciliation jobs that compare your DB against ours: the refund ledger makes sum(charges) == sum(delivered images) × price hold by construction.
  • "Did the retry double-charge us?" alerting — a retry with the same key cannot.
  • Defensive balance checks before each submit: 402 insufficient_credits is safe to just handle when it happens; nothing is partially charged.

What to still handle

  • failed jobs: refunded, but your UX should surface error.code (e.g. content_policy).
  • 429s: back off on Retry-After; the reservation for rejected requests is never made in the first place.
  • Presigned URL expiry (15 min): re-GET the job for fresh URLs — free.

The full behaviour is documented in the API reference; if you're comparing providers, ask each one what happens to your money when a job dies between their queue and their model. It's the question that separates metered APIs you can trust from ones you audit monthly.

About the author

Written by Unified Image API Team — genmux API.

More about us