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

WindowBudget per client_id
Per minute600
Per day10,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:
HeaderMeaning
X-RateLimit-LimitThe window’s budget (e.g. 600 for per-minute).
X-RateLimit-RemainingHow many requests you have left in this window.
X-RateLimit-ResetUnix timestamp (seconds) when the window resets.
Retry-AfterOnly 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/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 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.

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.”
# 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.
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. If you must poll (no webhooks available on your side):
EndpointRecommended max polling interval
GET /v1/me1 hour
GET /v1/me/sea-time15 minutes
GET /v1/me/sea-time/recent1 hour
GET /v1/me/vessels1 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.