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"
}'const response = await fetch("https://api.useinfrared.com/v1/moderations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRARED_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "message-7702-v1",
},
body: JSON.stringify({
type: "text",
input: "Is this still available? I can collect on Saturday.",
content_id: "message_7702",
}),
});
const moderation = await response.json();import os
import requests
response = requests.post(
"https://api.useinfrared.com/v1/moderations",
headers={
"Authorization": f"Bearer {os.environ['INFRARED_KEY']}",
"Idempotency-Key": "message-7702-v1",
},
json={
"type": "text",
"input": "Is this still available? I can collect on Saturday.",
"content_id": "message_7702",
},
)
moderation = response.json(){
"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:
HTTP/1.1 200 OK
Idempotent-Replayed: true
X-Request-Id: req_f99309b0e67f41b997231ae3737f4c77Nothing ran the second time: the moderation is the same mod_0IIDfydTJaTX7G3LGqYKrP, no check was used and no event was sent.
- Your server to Infrared: POST /
v1/ moderations Idempotency-Key: message-7702-v1 - Infrared: Runs itstores the answer for 24 hours
- Infrared to Your server: 200 OK
mod_0IIDfydTJaTX7G3LGqYKrP - The same request again
- Your server to Infrared: POST /
v1/ moderationssame key, same body - Infrared: Does not run itreturns the stored answer
- Infrared to Your server: 200 OK
mod_0IIDfydTJaTX7G3LGqYKrPIdempotent-Replayed: true - The same key, another body
- Your server to Infrared: POST /
v1/ moderationssame key, another body - Infrared to Your server: 409 idempotency_
key_ reused
Sample data
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:
- resolving and reopening a queue item, and executing an action;
- re-moderating a moderation;
- the author endpoints;
- creating or changing guidelines, wordlists and their terms;
- creating, changing and replaying on webhook endpoints.
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 request | Answer |
|---|---|
| Same key, same body, after the first finished | The stored status and body, with Idempotent-Replayed: true. |
| Same key, while the first is still running | 409 idempotency_in_progress. Wait, then send it again. |
| Same key, another body | 409 idempotency_key_reused. |
| Same key, after 24 hours | It runs as a new request. |
{
"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 404template_not_found, a 402quota_exceededand a 403plan_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_scopeand a 429rate_limitedare answered before theIdempotency-Keyis 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, therequest_idinside it is the one of the first request.
{
"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
moderatescope. - 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
- Sync and asyncText is answered in the same response and images are queued. Choose with mode, then read a queued result from a webhook event or by polling.
- ErrorsEvery error has one JSON shape. Branch on its code, show its details and quote its request id. The table lists each code the API returns and when.
- Going liveWhat to settle before production traffic depends on Infrared, from keys and retries to the review queue. Each item links to the page behind it.
Something missing or unclear on this page? Write to support@useinfrared.com.