Idempotency
Safe write retries with the Idempotency-Key header.
Network calls fail in ambiguous ways: a timeout after a POST /work-orders leaves you unsure whether the work order was created. The Idempotency-Key header makes write retries safe.
How it works
Send a unique key (a UUID is ideal) with any write request:
curl -X POST "https://api.owner.dynvolt.com/v1/sites/pv-ljubas/defects" \
-H "Authorization: Bearer $DYNVOLT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0d6a9c42-3f1e-4d5b-9a8c-7e6f5d4c3b2a" \
-d '{"title": "Cracked module, row 14", "severity": "low"}'- The first request with a given key executes normally, and the response is recorded against the key.
- Any retry with the same key and the same body within 24 hours does not execute again — it replays the recorded response, byte for byte.
- After 24 hours the key expires and may be reused.
The header is optional. Requests without it execute unconditionally, every time.
Conflicts
Reusing a key with a different body is rejected with 422 and error code idempotency_conflict:
{
"error": {
"code": "idempotency_conflict",
"message": "Idempotency-Key was already used with a different request body.",
"request_id": "req_5c1d9e2f7b",
"details": null
}
}This is a bug guard, not a retry signal: it means your client generated the same key for two different requests. Generate a fresh key per logical operation — not per HTTP attempt, and never a constant.
Recommended client pattern
import uuid
def create_work_order(client, site_id, payload):
key = str(uuid.uuid4()) # one key per logical operation
for attempt in range(5):
try:
resp = client.post(
f"/v1/sites/{site_id}/work-orders",
headers={"Idempotency-Key": key}, # same key on every retry
json=payload,
timeout=15,
)
if resp.status_code < 500:
return resp
except Exception:
pass # timeout / connection error: retry with the SAME key
raise RuntimeError("work order creation failed after retries")Which endpoints support it
All write endpoints accept Idempotency-Key:
POST /v1/sites/{site_id}/work-orders,PUT .../work-orders/{item_id},POST .../work-orders/{item_id}/approvePOST /v1/sites/{site_id}/defects,PUT .../defects/{item_id}POST .../alerts/{item_id}/acknowledge,POST .../alerts/{item_id}/resolvePOST .../reports/subscriptions/{item_id}/run
Reads are naturally idempotent and ignore the header.

