Idempotency
Make API requests safe to retry by using idempotency keys. This ensures that retrying the same request multiple times has the same effect as making it once.
Overview
Idempotency is critical for:
- Network retries (timeouts, connection errors)
- Webhook retries (delivery failures)
- User-initiated retries (double-clicks, refresh)
- Distributed systems (ensuring exactly-once semantics)
The Agoralia API uses idempotency keys to cache responses and return the same result for duplicate requests within a 24-hour window.
How It Works
- Generate a unique idempotency key (UUID recommended)
- Include it in the
Idempotency-Keyheader - If the same key is used within 24 hours with the same request body, the original response is returned
- If the request body differs, you'll receive a
409 Conflicterror - After 24 hours, the key expires and can be reused
Request Matching
The API matches idempotency keys based on:
- Tenant ID (your workspace)
- API Key ID (which API key made the request)
- Idempotency Key (the key you provide)
- HTTP Method (POST, PUT, PATCH)
- Path (e.g.,
/api/v1/campaigns) - Request Body (must match exactly)
Important: The request body must match exactly for the same idempotency key to work. If you use the same key with different request data, you'll receive a 409 Conflict error.
Example
First Request
curl -X POST "https://api.agoralia.app/api/v1/campaigns" \
-H "X-API-Key: ago_ETWIQRPbBXBrMxwcyxqUFLlYGErtFOaa" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 Sales Campaign",
"purpose": "quote_request",
"agent_ref_id": 1,
"extraction_schema": {},
"display_columns": []
}'
Response: 201 Created
{
"id": 5,
"name": "Q1 Sales Campaign",
"status": "draft"
}
Retry with Same Key (within 24h)
curl -X POST "https://api.agoralia.app/api/v1/campaigns" \
-H "X-API-Key: ago_ETWIQRPbBXBrMxwcyxqUFLlYGErtFOaa" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{
"name": "Q1 Sales Campaign",
"purpose": "quote_request",
"agent_ref_id": 1,
"extraction_schema": {},
"display_columns": []
}'
Response: 200 OK (same response as first request)
{
"id": 5,
"name": "Q1 Sales Campaign",
"status": "draft"
}
Notice: The status code is 200 OK instead of 201 Created because the resource was already created.
Supported Endpoints
Idempotency is supported for write operations that create or modify resources:
| Endpoint | Method | Description |
|---|---|---|
/api/v1/agents | POST | Create agent |
/api/v1/campaigns | POST | Create campaign |
/api/v1/leads:upsert | POST | Create or update leads |
Read operations (GET) are naturally idempotent and don't require idempotency keys.
Best Practices
Generate Unique Keys
Use UUIDs for idempotency keys:
Python:
import uuid
idempotency_key = str(uuid.uuid4())
# Example: "550e8400-e29b-41d4-a716-446655440000"
Node.js:
const { randomUUID } = require('crypto');
const idempotencyKey = randomUUID();
Bash:
idempotency_key=$(uuidgen)
Store Keys with Requests
For webhook integrations or background jobs, store the idempotency key with the request payload:
# Store in your database
webhook_request = {
"idempotency_key": idempotency_key,
"payload": {...},
"status": "pending",
"retry_count": 0
}
Use Different Keys for Different Requests
Each unique request should have a unique idempotency key:
# ✅ Good: Different keys for different campaigns
create_campaign("Campaign A", idempotency_key="key-1")
create_campaign("Campaign B", idempotency_key="key-2")
# ❌ Bad: Same key for different requests
create_campaign("Campaign A", idempotency_key="key-1")
create_campaign("Campaign B", idempotency_key="key-1") # Will fail with 409
Handle 409 Conflicts
If you receive a 409 Conflict, it means:
- The idempotency key was used with different request data
- You should use a new key or match the original request exactly
try:
response = create_campaign(data, idempotency_key=key)
except HTTPException as e:
if e.status_code == 409:
# Key conflict - use a new key
new_key = str(uuid.uuid4())
response = create_campaign(data, idempotency_key=new_key)
Request Body Matching
The API compares request bodies to ensure idempotency. For the same idempotency key to work:
- ✅ Same data → Returns cached response
- ❌ Different data →
409 Conflicterror
Best Practice: Always use the exact same request body when retrying with the same idempotency key. If you need to change the request data, use a new idempotency key.
Key Expiration
Idempotency keys expire after 24 hours. After expiration:
- The key can be reused
- Previous responses are no longer cached
- A new request with the same key will be processed normally
Example Timeline:
Day 1, 10:00 AM - Request with key "abc123" → Creates campaign #5
Day 1, 10:05 AM - Retry with key "abc123" → Returns campaign #5 (cached)
Day 2, 10:01 AM - Retry with key "abc123" → Creates NEW campaign #6 (key expired)
Response Caching
When an idempotency key is used:
- The response status code is cached
- The response body is cached
- The response headers are NOT cached
Important: The cached response is returned even if the underlying resource has changed. This ensures idempotency but means you might get stale data if you retry after a long delay.
Error Handling
409 Conflict
Cause: Idempotency key was used with different request data.
Response:
{
"detail": "Idempotency key already used with different request body"
}
Solution: Use a new idempotency key or match the original request exactly.
Key Expired
Cause: Idempotency key expired (more than 24 hours old).
Behavior: The request is processed normally (no error, but no caching).
Solution: Generate a new key if you need idempotency.
Implementation Example
Python with Retry Logic
import uuid
import requests
import time
def create_campaign_with_retry(name, agent_id, max_retries=3):
"""Create campaign with automatic retry and idempotency"""
idempotency_key = str(uuid.uuid4())
url = "https://api.agoralia.app/api/v1/campaigns"
headers = {
"X-API-Key": "ago_ETWIQRPbBXBrMxwcyxqUFLlYGErtFOaa",
"Idempotency-Key": idempotency_key,
"Content-Type": "application/json"
}
data = {
"name": name,
"purpose": "quote_request",
"agent_ref_id": agent_id,
"extraction_schema": {},
"display_columns": []
}
for attempt in range(max_retries):
try:
response = requests.post(url, json=data, headers=headers)
if response.status_code in [200, 201]:
return response.json()
elif response.status_code == 409:
# Conflict - use new key
idempotency_key = str(uuid.uuid4())
headers["Idempotency-Key"] = idempotency_key
continue
elif response.status_code == 429:
# Rate limited - wait and retry
retry_after = int(response.headers.get("Retry-After", 2))
time.sleep(retry_after * (2 ** attempt))
continue
else:
response.raise_for_status()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
raise Exception("Max retries exceeded")
Troubleshooting
Key Not Working
Problem: Retries are creating duplicate resources instead of returning cached response.
Possible causes:
- Idempotency key not included in header
- Key expired (more than 24 hours old)
- Request body differs (even slightly)
Solution:
- Verify
Idempotency-Keyheader is present - Check request body matches exactly
- Generate a new key if needed
409 Conflict on First Request
Problem: Receiving 409 Conflict even on first request.
Possible causes:
- Key was used previously with different data
- Key was used by another API key in the same tenant
Solution: Use a new idempotency key.
Next Steps
- Endpoints Reference - See which endpoints support idempotency
- Error Handling - Handle API errors gracefully
- Rate Limits - Understand API quotas