Skip to content
opis.Nutrition API

Idempotency

Networks fail. A request can time out after the server has already acted on it. Send an Idempotency-Key header on every POST and DELETE, and you can retry safely: the second attempt returns the first attempt's response instead of creating a second analysis or confirming twice.

HTTP
POST /v1/meal-analyses HTTP/1.1
Authorization: Bearer opis_live_…
Idempotency-Key: 6f1c2e1a-4f0b-4a7e-9a0e-8d8f7d9a5c21
Content-Type: application/json

Rules

  • Key: any string up to 255 characters; a UUID v4 per logical operation is ideal. Generate it once, before the first attempt, and reuse it on every retry of that operation.
  • Scope: keys are scoped to your organization. Do not reuse a key across projects.
  • Lifetime: keys are remembered for 24 hours.
  • Fingerprint: we store a hash of the method, path, body and (for uploads) the file's SHA-256.
SituationResponse
First requestProcessed normally; the response is stored
Same key, same request, finishedThe stored response is replayed — same status and body — with Idempotent-Replayed: true
Same key, same request, still running409 idempotency_request_in_progress with Retry-After: 1
Same key, different request422 idempotency_key_reused
After 24 hoursThe key is forgotten and treated as new

Client errors (4xx) are stored and replayed like successes. Server errors (5xx) are not stored, so retrying with the same key after a 5xx runs the request again.

Retry recipe

async function postWithRetry(url: string, body: unknown, attempts = 4) {
  const key = crypto.randomUUID(); // one key for all attempts
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch(url, {
        method: 'POST',
        headers: {
          ...auth,
          'Content-Type': 'application/json',
          'Idempotency-Key': key,
        },
        body: JSON.stringify(body),
      });
      if (res.status !== 409 && res.status < 500) return res;
      const wait = Number(res.headers.get('retry-after') ?? 2 ** i);
      await new Promise((r) => setTimeout(r, wait * 1000));
    } catch {
      await new Promise((r) => setTimeout(r, 2 ** i * 1000)); // network error: retry, same key
    }
  }
  throw new Error('Gave up after retries');
}

Where it matters most

  • Creating analyses and estimates — a duplicate is billed twice.
  • Confirming — combine Idempotency-Key with If-Match (see Confirmation). A replayed confirm returns the original 202; without a key, a second confirm returns 409 invalid_state.
  • Uploads — a retried multipart upload with the same key returns the same file_….