If your client retries a failed request after a network blip or a queue worker crash, you used to risk paying for the same call twice. From today, both POST /v1/suggestions and POST /v1/redactions accept an idempotency key, and we'll dedupe.
The bigger win is on /v1/suggestions. That endpoint runs an agent over the document. It's the slowest, most expensive call in the API surface, and the one most likely to be retried over a flaky connection. Adding an idempotency key there means a retried request never re-runs the agent.
How it works
Send an Idempotency-Key header with any request to either endpoint. The first time we see a key, we run the operation. A retry that arrives while the original is still running gets 409 Conflict — no parallel agent run, no duplicate redaction job.
For JSON responses with no sensitive content in the body — /v1/suggestions with default fields, and /v1/redactions calls that use a connector — we remember enough of the response to replay it for 24 hours. A retry that arrives after the original completes returns the original answer (same status, same body), is not metered, and comes back with an X-Idempotent-Replayed: true header so your client can tell.
Sensitive content never enters the idempotency cache. Caching it would break the "we process, then forget" promise the rest of the API is built on. The redacted PDF returned by direct-upload /v1/redactions is excluded; so are the revealed text and explanation fields you can opt into on /v1/suggestions via sparse fieldsets. Anything that contains a literal extracted string from your document stays out of the cache.
The trade-off is that uncached requests don't replay. The in-flight 409 Conflict still applies while the original is running, but a retry that arrives after completion will re-run and re-bill. Use the connector flow for redactions if you need full replay safety, and reserve revealed-field suggestion calls for requests you don't intend to retry.
curl https://api.redactr.io/v1/suggestions \
-H "Authorization: Bearer $REDACTR_API_KEY" \
-H "Idempotency-Key: suggestions:9f3e2b8a-1d4c-4f5a-9e7b-2c1d8f3a4b5e" \
-F "file=@/path/to/document.pdf" \
-F "agent=english-dsar"Any opaque string up to 255 characters works as a key. We recommend a UUID generated by your client; the value doesn't need to be secret, but it does need to be unique across the requests you want to dedupe.
We also recommend prefixing the key with the operation name. Use suggestions:<uuid> for /v1/suggestions and redactions:<uuid> for /v1/redactions. The key stays self-documenting in your logs that way, and there's no chance of collision between the two endpoints.
What happens on retry
For cached requests, the retry replays the original — whether the original succeeded, failed with a 4xx (e.g. the file was rejected), or failed with a 5xx. Once we've responded, we stand by it; use a fresh key if you want to genuinely re-attempt after a failure.
For uncached requests (direct-upload /v1/redactions, and /v1/suggestions calls that reveal text or explanation), there's nothing to replay. A retry that arrives after the original completes is treated as a fresh request: it runs and is metered.
Keys identify the request, not its contents — we don't compare bodies. Reusing the same key for a different operation returns the cached response of the original (when there is one), so generate a fresh key per logical request to avoid surprising replays.
In a generated client
We don't ship official SDKs. We publish a versioned OpenAPI 3.1 spec on the docs site and recommend generating a client in your language of choice. Most generators expose per-request headers as an option, which is where the idempotency key goes:
const result = await client.suggestions.create({
body: { agent: 'english-dsar', file },
headers: {
'Idempotency-Key': `suggestions:${crypto.randomUUID()}`,
},
})result = client.suggestions.create(
body={"agent": "english-dsar", "file": file},
headers={"Idempotency-Key": f"suggestions:{uuid.uuid4()}"},
)If you don't pass a key, behaviour is unchanged: every request runs and is metered as before.