Skip to main content

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:
  1. The API creates a fingerprint of your request (method, path, body)
  2. If the same key is used again with the same fingerprint, the original response is returned
  3. 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

RequirementDetails
FormatAny valid UUID (v4 recommended for uniqueness), up to 128 characters. Hyphenated, simple, braced, and URN forms are all accepted.
UniquenessMust be unique per operation
ExpirationKeys expire after 24 hours
  • Idempotency keys must parse as a UUID. Non-UUID values are 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 again, the API briefly waits (up to about 3 seconds with exponential backoff) for the original to complete:
  • If the original finishes in time, the retry returns the original response — same status code, same body.
  • If it does not finish in time, the retry returns 409 Conflict:
{
  "error": {
    "code": "already_exists",
    "message": "Request is still in progress. Please retry later."
  }
}
Wait briefly and retry — the original is still running, so the next call will either return its cached response or, if it has finished, the final response. This 409 shares code: already_exists with the “different request body” 409 below. Distinguish them by the message field, or by checking whether your request body has changed.

Error responses

Match on code rather than message. Message text is for humans and may change; code is the stable contract.

Conflict (different request body)

{
  "error": {
    "code": "already_exists",
    "message": "Idempotency key reused with different request parameters"
  }
}
Solution: Use a different idempotency key or ensure the request body matches the original.

Invalid key format

{
  "error": {
    "code": "invalid_argument",
    "message": "Invalid idempotency key format"
  }
}
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

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.
Don’t reuse keys across different operations. Each logical operation should have its own unique key.
If you need to retry later (e.g., after a server restart), store the idempotency key alongside the operation context.
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

The Idempotency-Key header is honored on every POST and DELETE endpoint. PUT endpoints do not use this header — the backing resource is replaced on each request by design. GET is naturally idempotent.
MethodEndpoint
POST/v1/subscriptions
POST/v1/subscriptions/{id}/pause
POST/v1/subscriptions/{id}/resume
DELETE/v1/subscriptions/{id}
POST/v1/exports
POST/v1/exports/{id}/cancel
DELETE/v1/exports/{id}
POST/v1/export-schemas
DELETE/v1/export-schemas/{id}
POST/v1/export-schemas/{id}/draft
DELETE/v1/export-schemas/{id}/draft
POST/v1/export-schemas/{id}/publish

Next steps

Error Handling

Handle API errors gracefully in your application

Rate Limiting

Understand rate limits and how to handle them

Pagination

Navigate large result sets efficiently

API Reference

Explore the complete API documentation
Last modified on April 17, 2026