Skip to main content
Network requests can fail anywhere — connection reset, timeout, gateway 502, ambiguous 5xx. To make retries safe on write endpoints without creating duplicate resources, send an Idempotency-Key header on the original request and on every retry.

How it works

When the API receives a request with an Idempotency-Key:
  1. First time with that key + tenant: process the request normally and cache the response keyed off (tenant_id, idempotency_key, body_hash) for 24 hours.
  2. Same key + same body: return the cached response. The request is not processed again.
  3. Same key + different body: return idempotency_conflict (HTTP 409) so you know the key was reused with mismatched intent.
Idempotency keys are tenant-scoped; another tenant with the same key value sees no collision.

Where to use it

Use Idempotency-Key on every write call:
  • POST /api/v1/policies/
  • POST /api/v1/yara-rules/
  • POST /api/v1/yara-policies/
  • POST /api/v1/sdp/policies/
  • POST /api/v1/safety-policies/
  • POST /api/v1/api-keys/
  • POST /api/v1/tenants/
  • POST /api/v1/billing/portal-session
  • POST /api/v1/billing/create-checkout-session
  • POST /api/v1/support/tickets
Read endpoints (GET) are naturally idempotent and ignore the header. The hero POST /api/v1/analyze/ deliberately does not dedupe on Idempotency-Key. Each analysis is a fresh decision; you almost never want a cached one. If you need dedupe at your application layer (e.g. retrying the same prompt should not run analyzers twice in 100 ms), do it in your own caller.

Generating a key

A UUID v4 is the simplest choice. Both SDKs accept any string; pick something with enough entropy that you will not accidentally collide with another caller in your tenant.
Reuse the same key across every retry of the same logical write. If you generate a fresh key per retry, you defeat the protection.

SDK helpers

Python

The Python SDK auto-generates an idempotency key on every retried write when you do not pass one explicitly. To control it yourself:

TypeScript

Conflicts

If you reuse a key with a different body, the API returns:
Recovery: pick a new key and re-send.

Cache lifetime

  • 24 hours from the first request. After that the key is forgotten and a new request with the same key is treated as a fresh write.
  • Cache entries are not visible across tenants.
  • Cache entries survive Cloud Run autoscale events (the cache is Postgres-backed).

Common pitfalls

  • Generating the key inside the retry loop. A fresh key per retry is a different request as far as Agnes is concerned. Generate the key once outside the retry loop.
  • Using the same key for different operations. A key scopes to the full request body, so different operations naturally have different bodies — but using "create-policy-1" for both a policy create and a rule create is asking for confusion. Prefer UUIDs.
  • Sending an empty key. The API ignores empty / whitespace-only values. To use idempotency, send a real value.

Next