Skip to main content

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

  1. Generate a unique idempotency key (UUID recommended)
  2. Include it in the Idempotency-Key header
  3. If the same key is used within 24 hours with the same request body, the original response is returned
  4. If the request body differs, you'll receive a 409 Conflict error
  5. 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:

EndpointMethodDescription
/api/v1/agentsPOSTCreate agent
/api/v1/campaignsPOSTCreate campaign
/api/v1/leads:upsertPOSTCreate 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 data409 Conflict error

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-Key header 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