Idempotent requests
Reference article: Part of the API reference.
A network error can leave you not knowing whether a request worked. Retrying a read is harmless, but retrying "post this message" might post it twice. An idempotency key makes that retry safe: send the same Idempotency-Key header with the retry, and if the first attempt already succeeded, the API returns its response again instead of doing the work a second time.
Idempotency-Key: 0b9f6c1e-4d2a-4c8e-9f7b-3a5d8e1c2b64
Endpoints that accept it
These endpoints create something, so they accept Idempotency-Key:
| Endpoint | Creates |
|---|---|
POST /v1/workstreams |
A workstream |
POST /v1/workstreams/{workstream}/messages |
A message |
POST /v1/loops/{loop}/execute |
A loop execution |
POST /v1/apps |
An App |
Other endpoints ignore the header. GET and DELETE requests are already safe to repeat, and PATCH requests set fields to the values you send, so repeating one changes nothing further. Rolling a webhook signing secret twice gives you two new secrets in turn; use the one from the last response. Ask the agent doesn't accept it: a retry starts a new answer. Endpoints whose response contains a secret, creating a webhook endpoint and installing an App, don't accept it either, because a saved response would keep that secret stored for a day; check with a list request before retrying one.
How it works
- Generate a unique key for each operation, such as a UUID v4, and send it in the
Idempotency-Keyheader. - If the request succeeds, the API saves its status and body against the key for 24 hours.
- Within those 24 hours, a request with the same key and the same request (method, path, and body) gets the saved response back, with the header
Idempotent-Replayed: true. Nothing is created again. - After 24 hours the key is free, and a request with it runs as new.
::: code-group
curl https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/messages \
-H "Authorization: Bearer $NIMBUS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b9f6c1e-4d2a-4c8e-9f7b-3a5d8e1c2b64" \
-d '{"text": "Nightly build **passed** on main."}'
import { randomUUID } from 'node:crypto'
const idempotencyKey = randomUUID()
const body = JSON.stringify({ text: 'Nightly build **passed** on main.' })
const post = () =>
fetch('https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NIMBUS_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body, // the same bytes on every attempt
})
let response: Response | undefined
for (let attempt = 1; attempt <= 3; attempt++) {
try {
response = await post()
if (response.status !== 409 && response.status < 500) break
} catch {
// Network error: safe to retry with the same key.
}
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt))
}
console.log(response?.headers.get('Idempotent-Replayed')) // "true" if an earlier attempt succeeded
import json
import os
import time
import uuid
import requests
idempotency_key = str(uuid.uuid4())
body = json.dumps({"text": "Nightly build **passed** on main."})
response = None
for attempt in range(1, 4):
try:
response = requests.post(
"https://api.gonimbus.ai/v1/workstreams/q4-pricing-review/messages",
headers={
"Authorization": f"Bearer {os.environ['NIMBUS_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
data=body, # the same bytes on every attempt
)
if response.status_code != 409 and response.status_code < 500:
break
except requests.ConnectionError:
pass # Network error: safe to retry with the same key.
time.sleep(attempt)
print(response.headers.get("Idempotent-Replayed")) # "true" if an earlier attempt succeeded
:::
Tip: Serialize the body once and send those exact bytes on every attempt. The API compares the raw body, so the same data with keys in a different order, or different spacing, counts as a different request.
Rules
| Rule | Detail |
|---|---|
| Length | Up to 255 characters. A longer key fails with 400 parameter_invalid and param set to Idempotency-Key. |
| Scope | Keys belong to your organization, not to one API key. Two services using different API keys must still not share idempotency keys. |
| Window | 24 hours from the first request. |
| Only successes are saved | If the request fails, for example with a 400 or a 500, nothing is saved and the key is released, so you can fix the problem and retry with the same key. |
| Replays | Return the first response's status and body. The Request-Id is new, so each attempt can still be found in the request log. |
Errors
Reusing a key incorrectly fails with 409 and the type idempotency_error. The param is Idempotency-Key.
| Code | When | What to do |
|---|---|---|
idempotency_key_reused |
The key was used within 24 hours for a different request: another path, method, or body. | Use a new key for a new request. Don't retry. |
idempotency_request_in_progress |
A request with this key is still being processed. | Wait a moment, then retry with the same key. |
FAQ
What should I use as an idempotency key?
A random UUID v4 per operation works well. If the operation comes from a record in your own system, such as a build ID, a key derived from it (build-8812-status) means a crash-and-restart of your worker can't create a duplicate either.
Do I need an idempotency key on every request?
No. It's optional, and only the endpoints above use it. Send one whenever creating something twice would be a problem, which is most of the time for messages and loop executions.
Is a replayed request logged?
Yes. Every attempt appears on the Developers page with its own Request-Id, and the log shows the Idempotency-Key each one sent.
Why did I get idempotency_key_reused on a genuine retry?
The retry wasn't byte-for-byte the same request. Common causes are rebuilding the JSON body on each attempt (so a timestamp or key order changes), or reusing one key for a different workstream. Build the body once and reuse it.
Can I replay a streamed agent answer?
No. Streams can't be replayed, and Ask the agent doesn't take an idempotency key. If a stream drops, retrieve the response by its id instead of asking again.