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

# Rate limits

> Per-client_id sliding windows, the headers we return, and how to stay under your budget.

The DSS Public API rate-limits per `client_id` on a sliding window. There are
two windows; whichever has the tighter remaining budget wins.

## Default budgets

| Window | Budget per `client_id` |
| - | - |
| Per minute | 600 |
| Per day | 10,000 |

These apply to every authenticated `/v1/*` endpoint. Public endpoints
(`/healthz`, OAuth, OpenAPI) are not rate-limited per-partner — they have
infrastructure-level protection instead.

Need higher limits? Premium-tier partners can negotiate. Talk to your DSS
account contact.

## Headers we return

Every authenticated response (success, `304`, or `429`) carries:

| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | The window's budget (e.g. `600` for per-minute). |
| `X-RateLimit-Remaining` | How many requests you have left in this window. |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets. |
| `Retry-After` | Only on `429`. Seconds to wait before retrying. |

The values reflect the *tightest* window at the time of the response — when
the per-minute budget is healthier than the per-day budget, you'll see the
per-day numbers, and vice versa.

## The 429 response

```http theme={null}
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 36
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748535696
X-Request-Id: req_01HV9XK...

{
  "type": "https://docs.digitalseaservice.com/errors/rate_limit_exceeded",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "Rate limit of 600 requests exceeded. Retry after 36s.",
  "instance": "req_01HV9XK..."
}
```

See [Errors / rate\_limit\_exceeded](/errors/rate_limit_exceeded) for the
detail.

## Staying under budget

The default budget is generous for any reasonable integration. Here's how to
not waste it.

### 1. Use webhooks instead of polling

Webhooks cost zero rate-limit budget. A well-integrated partner sets up
webhooks on day one and only calls the API in response to events (or when
the user actively opens a page that needs fresh data).

See [Webhooks overview](/webhooks/overview).

### 2. Use conditional GETs

Every resource endpoint returns `ETag` and `Last-Modified` and supports
`If-None-Match` / `If-Modified-Since`. A `304 Not Modified` response counts
as **one request** against your budget but returns no body — much cheaper
than the alternative of "always full payload."

```bash theme={null}
# First request
curl -i -H "Authorization: Bearer $TOKEN" \
  https://api.digitalseaservice.com/v1/me/sea-time

# Note the ETag in the response. On the next poll:
curl -i -H "Authorization: Bearer $TOKEN" \
  -H 'If-None-Match: "8f1e6c..."' \
  https://api.digitalseaservice.com/v1/me/sea-time
# -> 304 Not Modified, no body
```

If you're polling on a schedule, cache the ETag per (user, endpoint) and
replay it on every request.

### 3. Don't tight-loop on 429

A retry loop that ignores `Retry-After` will keep hitting `429` and never
recover. Always respect the header.

```python theme={null}
import time

def with_rate_limit_backoff(call, max_retries: int = 5):
    for _ in range(max_retries):
        r = call()
        if r.status_code != 429:
            return r
        time.sleep(int(r.headers.get("Retry-After", "1")))
    return r  # final 429 returned to caller for handling
```

### 4. Batch background work to natural intervals

If you're refreshing N users' records every M minutes, schedule the
batches so they don't pile up in the same second. Spreading 1,000 users
evenly across a minute is a much smaller blast radius than 1,000 requests
in the same second.

## Recommended polling intervals

If you must poll (no webhooks available on your side):

| Endpoint | Recommended max polling interval |
| - | - |
| `GET /v1/me` | 1 hour |
| `GET /v1/me/sea-time` | 15 minutes |
| `GET /v1/me/sea-time/recent` | 1 hour |
| `GET /v1/me/vessels` | 1 hour |

Combined with `If-None-Match`, the realistic cost of these is small —
sea-time records change a few times per month for the average crew member.

## What happens on persistent overuse

If your `client_id` consistently saturates its budget we'll get in touch
before anything changes. We may suggest pushing more of your reads onto
webhooks + conditional GETs, or talk through a higher-tier plan. We don't
hard-suspend partners for rate-limit reasons without a conversation first.


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