> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ticksupply.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Safely retry requests using idempotency keys

# 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

<Note>
  Idempotency keys are scoped to your account. Different accounts can use the same key without conflict.
</Note>

## Using idempotency keys

Add the `Idempotency-Key` header to any POST or DELETE request:

<CodeGroup>
  ```bash cURL theme={"system"}
  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
  ```

  ```python Python theme={"system"}
  import uuid

  def create_subscription_idempotent(datastream_id, idempotency_key=None):
      if idempotency_key is None:
          idempotency_key = str(uuid.uuid4())
      
      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}
      )
      
      return response
  ```

  ```javascript JavaScript theme={"system"}
  async function createSubscriptionIdempotent(datastreamId, idempotencyKey) {
    const key = idempotencyKey || crypto.randomUUID();

    const response = await fetch("https://api.ticksupply.com/v1/subscriptions", {
      method: "POST",
      headers: {
        "X-Api-Key": API_KEY,
        "Content-Type": "application/json",
        "Idempotency-Key": key
      },
      body: JSON.stringify({ datastream_id: datastreamId })
    });

    return response;
  }
  ```
</CodeGroup>

## Key requirements

| Requirement | Details                                                                                                                           |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Format      | Any valid UUID (v4 recommended for uniqueness), up to 128 characters. Hyphenated, simple, braced, and URN forms are all accepted. |
| Uniqueness  | Must be unique per operation                                                                                                      |
| Expiration  | Keys expire after 24 hours                                                                                                        |

<Warning>
  * 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.
</Warning>

## Generating idempotency keys

Generate a new UUID for each unique operation:

<CodeGroup>
  ```python Python theme={"system"}
  import uuid

  key = str(uuid.uuid4())  # e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  ```

  ```javascript JavaScript theme={"system"}
  const key = crypto.randomUUID();  // e.g., "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  ```

  ```bash Bash theme={"system"}
  # Using uuidgen
  key=$(uuidgen | tr '[:upper:]' '[:lower:]')

  # Or using Python
  key=$(python3 -c "import uuid; print(uuid.uuid4())")
  ```
</CodeGroup>

<Tip>
  Store the UUID alongside your operation context if you need to retry later (e.g., after a server restart).
</Tip>

## 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`:

```json theme={"system"}
{
  "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

<Note>
  Match on `code` rather than `message`. Message text is for humans and may change; `code` is the stable contract.
</Note>

### Conflict (different request body)

```json theme={"system"}
{
  "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

```json theme={"system"}
{
  "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

```python theme={"system"}
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:

```python theme={"system"}
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

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="Generate unique keys per operation">
    Don't reuse keys across different operations. Each logical operation should have its own unique key.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

## 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.

| Method | Endpoint                          |
| ------ | --------------------------------- |
| 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

<CardGroup cols={2}>
  <Card title="Error Handling" icon="circle-exclamation" href="/guides/error-handling">
    Handle API errors gracefully in your application
  </Card>

  <Card title="Rate Limiting" icon="gauge" href="/guides/rate-limiting">
    Understand rate limits and how to handle them
  </Card>

  <Card title="Pagination" icon="list" href="/guides/pagination">
    Navigate large result sets efficiently
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/overview">
    Explore the complete API documentation
  </Card>
</CardGroup>
