Skip to content

Authentication & limits

Authentication

All /v1 routes require a Bearer API key:

Authorization: Bearer rok_...

/health/* and the interactive /docs are public. A missing, malformed, unknown, or revoked key yields 401 with the standard error envelope and code unauthorized.

Try it from /docs

The OpenAPI document declares the key as a bearer security scheme, so /docs has an Authorize button — paste a rok_… key there and the Try it out requests carry it. Clients generated from /openapi.json pick the header up the same way.

Keys are minted and revoked self-serve from your account dashboard (/account — live usage metrics, and as many active keys as your plan allows), or by the operator CLI (python -m app.auth.cli) on any plan. Either way the plaintext is shown once at creation and only a SHA-256 hash is stored. Revocation takes effect immediately.

Limits apply to your account, not to each key

Your rate limit and monthly quota are counted across all the keys on your account. Extra keys are for separating environments and rotating secrets — they do not add capacity. Usage is still reported per key, so you can see which one is spending.

Calling from a browser

CORS is enabled for all origins on GET/HEAD — dashboards, Observable notebooks, and other browser-based consumers can call the /v1 data API directly with the Authorization header. The Bearer token is the only credential the data API accepts: cookies play no part in /v1 requests (the account dashboard's session cookie is scoped to the account pages only), so the wildcard origin is safe. Rate-limit and caching headers (X-RateLimit-*, ETag, X-Cache, Retry-After) are exposed to scripts. HEAD is supported on every GET route for monitors and link-checkers.

Plans

Plan Price / month Requests / minute Requests / month Active keys
free €0 60 10,000 5
starter €19 120 100,000 10
pro €59 300 1,000,000 25
business €199 1,200 10,000,000 50
enterprise custom 6,000 custom unlimited

Two gates are enforced in order on every request, both counted per account:

  1. a per-minute rate limit (burst control), then
  2. a monthly quota (volume control).

A rejected request never consumes quota — the rate gate rejects before the usage counter increments, and a quota rejection is rolled back.

Cache hits still count

Responses served from the API's Redis cache (X-Cache: HIT) and bodyless 304 replies still pass through auth, rate limiting, and metering. Caching saves latency and database work — not quota.

Rate-limit headers

Successful responses report your current rate-limit state:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 42

X-RateLimit-Reset is the number of seconds until the current window resets. When a limit is exceeded, the API returns 429 (code rate_limited) with a Retry-After header — sleep that many seconds and retry:

curl -i -H "Authorization: Bearer $API_KEY" \
  "https://api.canovix.ro/v1/transport/vehicle_registry?limit=1"
# HTTP/1.1 429 Too Many Requests
# Retry-After: 17
# {"error":{"code":"rate_limited","message":"Rate limit exceeded"}}
import time

import httpx

def get_with_retry(client: httpx.Client, url: str, **kwargs) -> httpx.Response:
    response = client.get(url, **kwargs)
    if response.status_code == 429:
        time.sleep(int(response.headers.get("Retry-After", "5")))
        response = client.get(url, **kwargs)
    return response
async function getWithRetry(url, options) {
  let res = await fetch(url, options);
  if (res.status === 429) {
    const wait = Number(res.headers.get("Retry-After") ?? 5);
    await new Promise((r) => setTimeout(r, wait * 1000));
    res = await fetch(url, options);
  }
  return res;
}

Good citizenship

  • Reuse a single HTTP client/connection pool instead of reconnecting per request.
  • Use cursor pagination or a bulk export instead of hammering deep offset pages.
  • Honor Retry-After — retrying immediately just re-hits the same window.
  • Send If-None-Match on repeat reads; 304s are much faster (see Caching).