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

# Rate Limits

> Request budgets for the Living Security Public API, the 429 response contract, and how to retry with Retry-After.

The `/v1` API enforces two independent request budgets. A request is refused
if either budget is exhausted.

| Budget | Applies to |
| - | - |
| Per API key | Every request presenting that key, from any address |
| Per client IP | Every request from that address, authenticated or not |

The per-key budget follows the key wherever it is used. The per-IP budget is
the only budget for unauthenticated traffic and a second ceiling for
authenticated traffic.

<Note>
  Limits are enforced before authentication, so requests with invalid or
  missing credentials also count against the source budget. Exact numeric
  budgets are operator-configured and may change; the response contract below
  is the stable interface to build against.
</Note>

Authenticate with either Basic credential shape — see
[Quickstart](/developers/v1/quickstart) (`apikey:secret` or `secret:`).
Set `BASE` to the organization's regional host — see
[Regions](/developers/v1/regions).

## The 429 response

When a budget is exhausted, the API returns `429 Too Many Requests` with a
`Retry-After` header (in seconds) and the standard problem-details body:

```json 429 Too Many Requests theme={null}
{
  "type": "https://docs.livingsecurity.com/developers/v1/errors/rate_limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Retry after the Retry-After interval.",
  "code": "rate_limited",
  "request_id": "req_...",
  "doc_url": "https://docs.livingsecurity.com/developers/v1/errors/rate_limited",
  "retryable": true
}
```

The response does not say which budget was exhausted. Treat every `429` the
same way: wait, then retry.

## Handling 429s

<Steps>
  <Step title="Honor Retry-After">
    Wait at least the number of seconds in the `Retry-After` header before
    retrying. Retrying earlier only re-consumes budget on requests that will be
    refused.
  </Step>

  <Step title="Back off exponentially on repeats">
    If a retry is refused again, increase your backoff and add random jitter
    so parallel workers do not retry in lockstep. Never shorten the server's `Retry-After` to fit a local
    cap. If the server asks you to wait longer than your retry budget allows,
    stop and schedule the work for later.
  </Step>

  <Step title="Smooth your request rate">
    Prefer a steady request rate over bursts. For bulk reads, use the maximum
    page size (`limit=500`) to cover the collection in fewer requests — see
    [pagination](/developers/v1/pagination). When several list walks run in
    parallel (for example Power BI tables), keep concurrency modest and honor
    `Retry-After` so a temporary 429 does not fail the whole refresh.
  </Step>
</Steps>

This example retries only `429` responses for a **GET** request, with at most
five attempts and at most 60 seconds of waiting between attempts. It stops
immediately on other errors. Network timeouts propagate to the caller. Configure
timeouts and retry budgets to suit your workload. `requests` interprets the read
timeout as the maximum idle time while receiving data, not an overall deadline.

Load `LIVING_SECURITY_API_KEY` from your secret manager. Install `requests`
before running this example. Call `get_with_retry("/organization")` to use it.

```python Python theme={null}
import os
import random
import time
import requests

# US: BASE = "https://api.us-east-1.livingsecurity.ai/v1"
BASE = "https://api.eu-west-1.livingsecurity.ai/v1"
AUTH = ("apikey", os.environ["LIVING_SECURITY_API_KEY"])

def get_with_retry(path, params=None, max_attempts=5):
    if not 1 <= max_attempts <= 5:
        raise ValueError("max_attempts must be between 1 and 5")
    for attempt in range(max_attempts):
        response = requests.get(
            f"{BASE}{path}", auth=AUTH, params=params,
            headers={"Living-Security-Version": "2026-07-01"},
            timeout=(5, 30), allow_redirects=False,
        )
        if response.status_code != 429:
            response.raise_for_status()
            if not 200 <= response.status_code < 300:
                raise RuntimeError("Unexpected redirect; check your regional API URL")
            return response

        # The API returns Retry-After as a whole number of seconds.
        raw_wait = response.headers.get("Retry-After", "")
        request_id = response.headers.get("Request-Id", "unknown")
        response.close()
        if attempt == max_attempts - 1:
            raise RuntimeError(f"Retry budget exhausted; request_id={request_id}")
        if not raw_wait.isascii() or not raw_wait.isdigit() or len(raw_wait) > 6:
            raise RuntimeError(f"Invalid Retry-After; request_id={request_id}")

        wait = max(int(raw_wait), min(2 ** attempt, 30))
        delay = wait + random.uniform(0, 1)
        if delay > 60:
            raise RuntimeError(f"Retry-After exceeds wait budget; request_id={request_id}")
        time.sleep(delay)
```

Do not copy this loop around a write operation. A timeout after a write may
mean the server committed it before the connection failed. Repeat a write only
when that operation's reference explicitly documents idempotency, preserving the
same key and payload. Otherwise reconcile the resource state before trying again.

## Recommended refresh schedule

Size refresh cadence against your key budget and page size. A full historical
load of a large collection is a one-time cost; prefer incremental windows for
steady-state refreshes so each run stays well under the budget. Always set
`limit=500` explicitly — the default of 25 multiplies request count.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.