Skip to content

Idempotency

Send an Idempotency-Key when you create a moderation, and a retry returns the first answer in place of a second moderation and a second charge.

A request can fail on the way back. The connection drops or your client times out, and you cannot tell whether the content was screened. Sending it again without care screens it twice and uses the checks twice.

Add an Idempotency-Key header and the second request is safe: Infrared recognises the key and returns the answer it gave the first time.

curl https://api.useinfrared.com/v1/moderations \
  -H "Authorization: Bearer $INFRARED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: message-7702-v1" \
  -d '{
    "type": "text",
    "input": "Is this still available? I can collect on Saturday.",
    "content_id": "message_7702"
  }'
200 OK, trimmedJSON
{
  "object": "moderation",
  "id": "mod_0IIDfydTJaTX7G3LGqYKrP",
  "status": "completed",
  "flagged": false,
  "recommendation": "allow",
  "content_id": "message_7702",
  "created_at": "2026-10-08T08:55:26.526Z",
  "completed_at": "2026-10-08T08:55:27.722Z"
}

Send the same request again and the same body comes back, with one more header:

The same request, sent againHTTP
HTTP/1.1 200 OK
Idempotent-Replayed: true
X-Request-Id: req_f99309b0e67f41b997231ae3737f4c77

Nothing ran the second time: the moderation is the same mod_0IIDfydTJaTX7G3LGqYKrP, no check was used and no event was sent.

One key, three requests
Sample data
Your server
Infrared
  1. Your server to Infrared: POST /v1/moderationsIdempotency-Key: message-7702-v1
  2. Infrared: Runs itstores the answer for 24 hours
  3. Infrared to Your server: 200 OKmod_0IIDfydTJaTX7G3LGqYKrP
  4. The same request again
  5. Your server to Infrared: POST /v1/moderationssame key, same body
  6. Infrared: Does not run itreturns the stored answer
  7. Infrared to Your server: 200 OKmod_0IIDfydTJaTX7G3LGqYKrPIdempotent-Replayed: true
  8. The same key, another body
  9. Your server to Infrared: POST /v1/moderationssame key, another body
  10. Infrared to Your server: 409 idempotency_key_reused

Sample data

A request with a key, the same request answered from the stored response, and the same key refused with another body.

Where it works

The header is honoured on two endpoints: Create a moderation and Create a batch. A batch is stored and replayed as a whole.

Every other endpoint ignores the header without an error, and a repeated request runs again:

Make your own code safe to repeat those, for example by reading the current state first.

The key

  • Any string of 1 to 255 characters. A longer one answers 400 invalid_request.
  • One key per piece of work: a UUID you generate, or an id of your own such as message-7702-v1. Keep it with the content, so that a retry after a crash finds the same key.
  • A key belongs to the API key that sent it and to the endpoint. The same string under another API key, or on the batch endpoint, is a different key.

What a repeated key returns

The second requestAnswer
Same key, same body, after the first finishedThe stored status and body, with Idempotent-Replayed: true.
Same key, while the first is still running409 idempotency_in_progress. Wait, then send it again.
Same key, another body409 idempotency_key_reused.
Same key, after 24 hoursIt runs as a new request.
409 ConflictJSON
{
  "error": {
    "code": "idempotency_key_reused",
    "message": "This Idempotency-Key was already used with a different request body",
    "details": [],
    "request_id": "req_cf1a0f13c08f4fcea2703c3353288092"
  }
}

A replay is the first answer, not the current state. For a queued moderation it is the 202 with status: "screening", even after the run has finished. Read the result with GET /v1/moderations/{id}.

The same body means the same bytes

Infrared compares the JSON you sent, key by key and in order. Spaces and line breaks do not matter. The order of the fields does: {"type":"text","input":"…"} and {"input":"…","type":"text"} are two bodies, and the second answers 409.

Serialise the body once, keep the string, and send that same string on every retry.

What is stored

The first answer is kept for 24 hours: its status and its body.

  • A refused request is stored too. Every answer below 500 that the endpoint gives is kept and replayed, including a 422 validation_error, a 404 template_not_found, a 402 quota_exceeded and a 403 plan_limit. After you fix the cause (correct the body, publish the template, add checks, change plan), send the request with a new key. With the old key and a changed body you get 409, and with the old key and the same body you get the old refusal.
  • A request refused before it reaches the endpoint is not stored. A 401, a 403 insufficient_scope and a 429 rate_limited are answered before the Idempotency-Key is read, so it is still unused.
  • A 5xx is not stored. Send the request again with the same key and it runs.
  • Headers are not stored. A replay has its own X-Request-Id. When the stored answer is an error, the request_id inside it is the one of the first request.
A stored refusal, replayedJSON
{
  "error": {
    "code": "template_not_found",
    "message": "Template no-such-template not found",
    "details": [],
    "request_id": "req_3192e04d52b4444f96e82ea53f67e94c"
  }
}

Limits to know

  • A first request that runs longer than 60 seconds stops holding its key. A retry sent after that runs alongside it, and the content is screened twice.
  • Keys are held in a cache. In the rare moment that cache cannot be reached, the header is ignored and the request runs.
  • A replay is still a request. It counts against the rate limit, and it needs a valid key with the moderate scope.
  • The stored answer is a copy of the response, with the scores and the text the policies quoted. It lives for the 24 hours whatever the project's retention is.

Next

Something missing or unclear on this page? Write to support@useinfrared.com.