Idempotency
Idempotency ensures that retrying a request produces the same result without duplicating the operation. This is essential for safely handling network failures and timeouts.
How idempotency works
When you include an Idempotency-Key header with a request:
The API creates a fingerprint of your request (method, path, body)
If the same key is used again with the same fingerprint, the original response is returned
If the key is reused with a different fingerprint, you receive a 409 Conflict error
Idempotency keys are scoped to your account. Different accounts can use the same key without conflict.
Using idempotency keys
Add the Idempotency-Key header to any POST or DELETE request:
IDEMPOTENCY_KEY = $( uuidgen | tr '[:upper:]' '[:lower:]' )
curl -X POST -H "X-Api-Key: YOUR_API_KEY" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY " \
-H "Content-Type: application/json" \
-d '{"datastream_id": 123}' \
https://api.ticksupply.com/v1/subscriptions
Key requirements
Requirement Details Format Must be a valid UUID (e.g., a1b2c3d4-e5f6-7890-abcd-ef1234567890) Uniqueness Must be unique per operation Expiration Keys expire after 24 hours
Idempotency keys must be valid UUIDs. Non-UUID formats will be rejected with a 400 Bad Request error.
Using the same idempotency key with different request bodies results in a 409 Conflict error.
Generating idempotency keys
Generate a new UUID for each unique operation:
import uuid
key = str (uuid.uuid4()) # e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
Store the UUID alongside your operation context if you need to retry later (e.g., after a server restart).
Handling concurrent requests
When a request is in progress and the same idempotency key is used, the API waits briefly for the original request to complete:
┌─────────────────────────────────────────────────────────────┐
│ Request 1: POST /subscriptions (Key: abc123) │
│ → Processing... (takes 2 seconds) │
├─────────────────────────────────────────────────────────────┤
│ Request 2: POST /subscriptions (Key: abc123) │
│ → Waiting for Request 1... │
│ → Returns same response as Request 1 │
└─────────────────────────────────────────────────────────────┘
If the original request doesn’t complete within ~2 seconds, the retry receives a response indicating the request is still in progress.
Error responses
Conflict (different request body)
{
"error" : {
"code" : "already_exists" ,
"message" : "Idempotency key already used with different request"
}
}
Solution : Use a different idempotency key or ensure the request body matches the original.
{
"error" : {
"code" : "invalid_argument" ,
"message" : "Invalid idempotency key format: expected UUID"
}
}
Solution : Use a valid UUID format (e.g., a1b2c3d4-e5f6-7890-abcd-ef1234567890).
Retry patterns
Safe retry with idempotency
import time
import uuid
def safe_create_subscription ( datastream_id , max_retries = 3 ):
idempotency_key = str (uuid.uuid4())
for attempt in range (max_retries):
try :
response = requests.post(
"https://api.ticksupply.com/v1/subscriptions" ,
headers = {
"X-Api-Key" : API_KEY ,
"Content-Type" : "application/json" ,
"Idempotency-Key" : idempotency_key
},
json = { "datastream_id" : datastream_id},
timeout = 30
)
if response.status_code == 429 :
retry_after = int (response.headers.get( "Retry-After" , 30 ))
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
# Safe to retry with same idempotency key
print ( f "Timeout on attempt { attempt + 1 } , retrying..." )
continue
except requests.exceptions.ConnectionError:
# Safe to retry with same idempotency key
time.sleep( 2 ** attempt) # Exponential backoff
continue
raise Exception ( "Max retries exceeded" )
Handling unknown response status
When a request times out, you don’t know if it succeeded. Using idempotency keys makes retries safe:
def create_subscription_safe ( datastream_id ):
idempotency_key = str (uuid.uuid4())
try :
response = requests.post(
"https://api.ticksupply.com/v1/subscriptions" ,
headers = {
"X-Api-Key" : API_KEY ,
"Content-Type" : "application/json" ,
"Idempotency-Key" : idempotency_key
},
json = { "datastream_id" : datastream_id},
timeout = 10
)
return response.json()
except requests.exceptions.Timeout:
# Unknown if request succeeded - retry with same key
print ( "Request timed out, retrying..." )
response = requests.post(
"https://api.ticksupply.com/v1/subscriptions" ,
headers = {
"X-Api-Key" : API_KEY ,
"Content-Type" : "application/json" ,
"Idempotency-Key" : idempotency_key # Same key!
},
json = { "datastream_id" : datastream_id},
timeout = 30
)
return response.json() # Either new or cached response
Best practices
Always use idempotency keys for mutating operations
Include Idempotency-Key for all POST and DELETE requests, even if you don’t plan to retry. This protects against accidental double-clicks and network retries.
Generate unique keys per operation
Don’t reuse keys across different operations. Each logical operation should have its own unique key.
Store keys with operation context
If you need to retry later (e.g., after a server restart), store the idempotency key alongside the operation context.
Don't rely on key expiration
Keys expire after 24 hours, but you shouldn’t retry operations after such a long delay. Use fresh keys for new operations.
Endpoints supporting idempotency
Endpoint Method Idempotency /v1/subscriptionsPOST ✅ Supported /v1/subscriptions/{id}DELETE ✅ Supported /v1/exportsPOST ✅ Supported All GET endpoints GET N/A (naturally idempotent)
Next steps