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

# Error Handling

> Handle API errors gracefully in your application

# Error Handling

Ticksupply uses consistent error responses across all endpoints. This guide covers error formats, common error codes, and best practices for handling errors.

## Error response format

All errors follow this structure:

```json theme={"system"}
{
  "error": {
    "code": "not_found",
    "message": "Subscription sub_550e8400e29b41d4a716446655440000 not found"
  }
}
```

When the server has structured context to share, the optional `details` field is included with free-form JSON:

```json theme={"system"}
{
  "error": {
    "code": "invalid_argument",
    "message": "Schema has too many columns",
    "details": {
      "max_columns": 100,
      "provided": 142
    }
  }
}
```

| Field     | Description                                                                                                 |
| --------- | ----------------------------------------------------------------------------------------------------------- |
| `code`    | Machine-readable error code for programmatic handling                                                       |
| `message` | Human-readable description of the error                                                                     |
| `details` | Optional object with additional, error-specific context. Omitted when absent — do not rely on its presence. |

Every response also includes an `X-Request-Id` header (e.g., `req_7f8e9d0c1b2a3456`). Include this value when contacting support.

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

## Error codes reference

| Code                | HTTP Status | Description                                                                                    |
| ------------------- | ----------- | ---------------------------------------------------------------------------------------------- |
| `invalid_argument`  | 400         | Request validation failed                                                                      |
| `unauthenticated`   | 401         | Missing or invalid API key                                                                     |
| `permission_denied` | 403         | Valid key, but lacks permission                                                                |
| `not_found`         | 404         | Resource doesn't exist                                                                         |
| `already_exists`    | 409         | Resource already exists or idempotency conflict                                                |
| `request_timeout`   | 408         | Request did not complete within the 10-second deadline                                         |
| `rate_limited`      | 429         | Too many requests                                                                              |
| `payment_required`  | 402         | Billing-related denial (plan limit, trial cap, suspended account, or grace period restriction) |
| `internal`          | 500         | Server error (contact support if persistent)                                                   |
| `unavailable`       | 503         | Service temporarily unavailable                                                                |

## Handling errors by type

### Authentication errors (401)

```json theme={"system"}
{
  "error": {
    "code": "unauthenticated",
    "message": "Invalid or missing API key"
  }
}
```

**Common causes:**

* Missing `X-Api-Key` header
* Invalid or expired API key
* Typo in the API key

**Solution:**

```python theme={"system"}
def handle_auth_error(response):
    if response.status_code == 401:
        raise ValueError(
            "Invalid API key. Check your key at "
            "https://app.ticksupply.com/api-keys"
        )
```

### Validation errors (400)

```json theme={"system"}
{
  "error": {
    "code": "invalid_argument",
    "message": "Invalid datastream_id: must be a positive integer"
  }
}
```

**Common causes:**

* Missing required parameters
* Invalid parameter types or values
* Malformed request body

**Solution:**

```python theme={"system"}
def handle_validation_error(response):
    if response.status_code == 400:
        error = response.json()["error"]
        raise ValueError(f"Invalid request: {error['message']}")
```

### Not found errors (404)

```json theme={"system"}
{
  "error": {
    "code": "not_found",
    "message": "Subscription sub_550e8400e29b41d4a716446655440000 not found"
  }
}
```

**Common causes:**

* Resource was deleted
* Incorrect resource ID
* Resource belongs to a different account

**Solution:**

```python theme={"system"}
def get_subscription_safe(subscription_id):
    response = requests.get(
        f"https://api.ticksupply.com/v1/subscriptions/{subscription_id}",
        headers={"X-Api-Key": API_KEY}
    )
    
    if response.status_code == 404:
        return None  # Resource not found
    
    response.raise_for_status()
    return response.json()
```

### Request timeout errors (408)

```json theme={"system"}
{
  "error": {
    "code": "request_timeout",
    "message": "Request exceeded 10s deadline"
  }
}
```

Every endpoint has a hard 10-second deadline. If your request doesn't finish within that window, you receive a 408 with the standard error envelope. This is rare under normal conditions — typical responses are tens of milliseconds — and usually indicates transient backend pressure.

**Solution:** Retry with exponential backoff. If the same endpoint times out repeatedly, contact support with the `X-Request-Id`.

```python theme={"system"}
def handle_timeout(response, attempt, max_retries=3):
    if response.status_code == 408 and attempt < max_retries:
        wait_time = 2 ** attempt  # Exponential backoff
        print(f"Request timed out. Retrying in {wait_time} seconds...")
        time.sleep(wait_time)
        return True  # Should retry
    return False
```

<Note>
  Heavy operations like creating an export return 202 Accepted immediately and finish asynchronously, so they never hit the 10-second deadline. Poll the resulting job for completion instead.
</Note>

### Rate limit errors (429)

```json theme={"system"}
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 30 seconds."
  }
}
```

**Headers included:**

* `Retry-After`: Seconds to wait before retrying

**Solution:**

```python theme={"system"}
def handle_rate_limit(response):
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 60))
        print(f"Rate limited. Retrying in {retry_after} seconds...")
        time.sleep(retry_after)
        return True  # Should retry
    return False
```

### Billing errors (402)

```json theme={"system"}
{
  "error": {
    "code": "payment_required",
    "message": "You have reached the export limit for your trial period. Your full plan limits will be available when your trial ends."
  }
}
```

**Common causes:**

* Trial account has reached its stream or export size limit
* Account is suspended for non-payment
* Account is in a billing grace period with restricted actions
* Plan does not include the requested action

**Returned by:** `POST /v1/subscriptions`, `POST /v1/subscriptions/{id}/resume`, and `POST /v1/exports`.

**Solution:** Retrying does not help — the underlying billing state must change. Upgrade your plan, update your payment method, or wait for your trial to convert in [billing settings](https://app.ticksupply.com/settings/billing).

```python theme={"system"}
def handle_billing_error(response):
    if response.status_code == 402:
        error = response.json()["error"]
        raise BillingError(
            f"{error['message']} "
            "Update your plan at https://app.ticksupply.com/settings/billing"
        )
```

### Server errors (500, 503)

```json theme={"system"}
{
  "error": {
    "code": "internal",
    "message": "An internal error occurred"
  }
}
```

**Solution:**

```python theme={"system"}
def handle_server_error(response, retry_count=0, max_retries=3):
    if response.status_code >= 500:
        if retry_count < max_retries:
            wait_time = 2 ** retry_count  # Exponential backoff
            print(f"Server error. Retrying in {wait_time} seconds...")
            time.sleep(wait_time)
            return True  # Should retry
        
        request_id = response.headers.get("X-Request-Id", "unknown")
        raise Exception(
            f"Server error after {max_retries} retries. "
            f"Request ID: {request_id} - "
            f"Contact support@ticksupply.com"
        )
    return False
```

## Complete error handling example

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

  class TicksupplyError(Exception):
      def __init__(self, code, message, request_id=None):
          self.code = code
          self.message = message
          self.request_id = request_id
          super().__init__(f"{code}: {message} (request_id: {request_id})")

  def api_request(method, path, max_retries=3, **kwargs):
      """Make an API request with comprehensive error handling."""
      url = f"https://api.ticksupply.com{path}"
      headers = kwargs.pop("headers", {})
      headers["X-Api-Key"] = API_KEY
      
      for attempt in range(max_retries + 1):
          try:
              response = requests.request(
                  method, url, headers=headers, timeout=30, **kwargs
              )
              
              # Success
              if response.status_code in (200, 201, 202, 204):
                  if response.status_code == 204:
                      return None
                  return response.json()
              
              # Parse error response
              try:
                  error = response.json().get("error", {})
              except:
                  error = {"code": "unknown", "message": response.text}

              code = error.get("code", "unknown")
              message = error.get("message", "Unknown error")
              request_id = response.headers.get("X-Request-Id", "")
              
              # Rate limit - always retry
              if response.status_code == 429:
                  retry_after = int(response.headers.get("Retry-After", 30))
                  print(f"Rate limited. Waiting {retry_after}s...")
                  time.sleep(retry_after)
                  continue
              
              # Server error - retry with backoff
              if response.status_code >= 500:
                  if attempt < max_retries:
                      wait_time = 2 ** attempt
                      print(f"Server error. Waiting {wait_time}s...")
                      time.sleep(wait_time)
                      continue
              
              # Client errors - don't retry
              raise TicksupplyError(code, message, request_id)
              
          except requests.exceptions.Timeout:
              if attempt < max_retries:
                  print(f"Timeout. Retrying ({attempt + 1}/{max_retries})...")
                  continue
              raise
          except requests.exceptions.ConnectionError:
              if attempt < max_retries:
                  time.sleep(2 ** attempt)
                  continue
              raise
      
      raise Exception("Max retries exceeded")

  # Usage
  try:
      subscription = api_request("GET", f"/v1/subscriptions/{sub_id}")
      print(f"Found subscription: {subscription['id']}")
  except TicksupplyError as e:
      if e.code == "not_found":
          print("Subscription not found")
      elif e.code == "unauthenticated":
          print("Invalid API key")
      else:
          print(f"API error: {e}")
  ```

  ```javascript JavaScript theme={"system"}
  class TicksupplyError extends Error {
    constructor(code, message, requestId) {
      super(`${code}: ${message} (request_id: ${requestId})`);
      this.code = code;
      this.requestId = requestId;
    }
  }

  async function apiRequest(method, path, options = {}, maxRetries = 3) {
    const url = `https://api.ticksupply.com${path}`;
    const headers = {
      "X-Api-Key": API_KEY,
      ...options.headers
    };
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        const response = await fetch(url, {
          method,
          headers,
          ...options
        });
        
        // Success
        if (response.ok) {
          if (response.status === 204) return null;
          return response.json();
        }
        
        // Parse error
        let error;
        try {
          const body = await response.json();
          error = body.error || {};
        } catch {
          error = { code: "unknown", message: await response.text() };
        }

        const { code = "unknown", message = "Unknown error" } = error;
        const requestId = response.headers.get("X-Request-Id") || "";
        
        // Rate limit
        if (response.status === 429) {
          const retryAfter = parseInt(response.headers.get("Retry-After") || "30");
          console.log(`Rate limited. Waiting ${retryAfter}s...`);
          await sleep(retryAfter * 1000);
          continue;
        }
        
        // Server error
        if (response.status >= 500 && attempt < maxRetries) {
          const waitTime = Math.pow(2, attempt) * 1000;
          console.log(`Server error. Waiting ${waitTime}ms...`);
          await sleep(waitTime);
          continue;
        }
        
        throw new TicksupplyError(code, message, requestId);
        
      } catch (err) {
        if (err instanceof TicksupplyError) throw err;
        if (attempt < maxRetries) {
          await sleep(Math.pow(2, attempt) * 1000);
          continue;
        }
        throw err;
      }
    }
  }

  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Usage
  try {
    const subscription = await apiRequest("GET", `/v1/subscriptions/${subId}`);
    console.log(`Found subscription: ${subscription.id}`);
  } catch (err) {
    if (err instanceof TicksupplyError) {
      if (err.code === "not_found") {
        console.log("Subscription not found");
      } else {
        console.log(`API error: ${err.message}`);
      }
    } else {
      throw err;
    }
  }
  ```
</CodeGroup>

## Request tracing

Every response includes an `X-Request-Id` header:

```
X-Request-Id: req_7f8e9d0c1b2a3456
```

<Tip>
  Log the `X-Request-Id` for all requests. When contacting support, include this ID to help us investigate issues quickly.
</Tip>

## Best practices

<AccordionGroup>
  <Accordion title="Always check response status codes">
    Don't assume requests succeed. Check status codes and handle errors appropriately.
  </Accordion>

  <Accordion title="Implement exponential backoff">
    For retryable errors (429, 5xx), use exponential backoff to avoid overwhelming the API.
  </Accordion>

  <Accordion title="Log request IDs">
    Store `X-Request-Id` values in your logs for debugging and support requests.
  </Accordion>

  <Accordion title="Use idempotency keys for retries">
    When retrying mutating operations, use the same idempotency key to avoid duplicates.
  </Accordion>

  <Accordion title="Don't retry client errors">
    4xx errors (except 429) indicate issues with your request. Fix the request rather than retrying.
  </Accordion>
</AccordionGroup>

## Getting help

When contacting support about errors:

1. Include the `X-Request-Id` header value from the response
2. Describe what you were trying to do
3. Include the request method and endpoint
4. Note the approximate time of the error

Email: [support@ticksupply.com](mailto:support@ticksupply.com)

## Next steps

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

  <Card title="Idempotency" icon="rotate" href="/guides/idempotency">
    Safely retry requests using idempotency keys
  </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>
