# API reference The OpenAPI document at **[`/api/v1/openapi.yaml`](http://localhost:8081/api/v1/openapi.yaml)** is canonical for endpoint shapes, request bodies, response schemas, and error envelopes. Browse it interactively at **[`/api/docs`](http://localhost:8081/api/docs)** (RapiDoc). This file documents the bits OpenAPI doesn't cleanly express. ## Rate-limit headers Public endpoints return `429 Too Many Requests` when the per-token bucket is exhausted, with: ```http HTTP/1.1 429 Too Many Requests Retry-After: 1 Content-Type: application/json {"error":"rate_limited"} ``` `Retry-After` is always `1` (second) — the bucket refills at `API_RATE_LIMIT_PER_SECOND` per second, so 1 s of wait restores at least one token. Don't sleep longer than that on a 429; just back off once and try again. Admin endpoints aren't rate-limited; the UI's request volume is bounded by human interaction. ## ETag semantics `GET /api/v1/blocklist` is the only endpoint that serves ETags today. Rules: - The ETag is `SHA-256` over the rendered body, **excluding** `generated_at` (which changes every cache rebuild). - Different `format=` values yield different ETags by design — the bytes differ, so the validator differs. - The api accepts both strong (`"abc"`) and weak (`W/"abc"`) forms in `If-None-Match`, and the wildcard `*`. - A 304 response carries no body and copies the `ETag` and the `X-Blocklist-*` headers from the would-be 200. The cache TTL is `BLOCKLIST_CACHE_TTL_SECONDS` (default 30 s) per policy. Mutations to `policies`, `manual_blocks`, or `allowlist` invalidate the affected cache entries; cross-replica visibility lags by up to that TTL when running multiple api replicas. ## Impersonation header The api accepts `X-Acting-User-Id: ` **only** in combination with a `service` token. On any other token kind, the header is silently ignored (it's not an error to send it; the api just doesn't use it). | Auth combination | Result | |---------------------------------------------------|---------------------------------| | Service token, header present, user exists | RBAC applied for the user; audited as `actor_kind=user, actor_id=
` | | Service token, header missing | `400 Bad Request` | | Service token, header malformed (non-integer) | `400 Bad Request` | | Service token, header points at unknown user | `404 Not Found` | | Service token, user is disabled | `403 Forbidden` | | Admin/reporter/consumer token, header present | header ignored; audited normally | | Admin/reporter/consumer token, header missing | normal path | The Auth API (`/api/v1/auth/*`) is service-token-only and **does not take the impersonation header** — those endpoints exist to *produce* the user record the BFF would later impersonate. Sending the header there is silently ignored. ## Response envelopes Most endpoints return a domain object directly (`Reporter`, `Consumer`, `Token`, etc.) for single-resource GETs and POSTs. Paginated lists use `{items, page, page_size, total}`. There is no top-level `data` wrapper today. Reasonable assumption for **future** batched endpoints (not yet built): a `meta` envelope alongside `items` would be added rather than re-shaping existing responses. The contract is additive; we won't introduce `{data: ...}` and break every existing client. Token creation has one envelope variation: it returns the normal `Token` shape **plus** a `raw_token` field that's the only place the raw value ever appears. Audit and list endpoints never carry `raw_token`. After the create response, the raw value can't be recovered — only revoked + re-issued. ## Date formats Every timestamp is UTC, ISO 8601 with `Z` suffix: `2026-04-29T10:00:00Z`. Inputs accept anything PHP's `DateTimeImmutable` parses (so `2026-04-29T10:00:00+00:00` works too), but outputs are canonicalised. Microsecond precision is **not** guaranteed in `v1`; the api stores `DATETIME(6)` on MySQL but only emits seconds. ## IP and CIDR formats - IPv4: dotted quad. Trailing whitespace is rejected; embedded whitespace is rejected. Stored as 16-byte binary (IPv4-mapped-in-IPv6 prefix `::ffff:0:0/96`). - IPv6: any RFC-4291 form. Canonicalised on output (no leading zeros, longest run of zeros compressed once). Zone identifiers (`fe80::1%eth0`) are rejected. - CIDRs: network address + prefix. Non-canonical input is silently normalized, with `normalized_from` echoed on the response when the normalization changed the input. URL path segments take an IP directly (`/api/v1/admin/ips/203.0.113.42`, `/api/v1/admin/ips/2001:db8::1`). Slim's default segment regex disallows colons; the api uses `/{ip:.+}` to allow IPv6 paths. ## Audit log shape The on-disk schema column names are `target_type` and `target_id`; the API surfaces them under the brief's vocabulary **`entity_type`** and **`entity_id`** for clarity. Filter parameters use the API names too. If you're inspecting the database directly, remember the renaming. `actor_kind` enum: `user`, `admin-token`, `reporter`, `consumer`, `system`. `actor_id` is a string in the API for forward-compat (token ids, reporter ids, user ids could diverge in shape over time); cast on the client. Audit emission failures are logged but never propagate. A successful state-changing call always commits; the audit row is best-effort. If you observe a state change without a corresponding audit row, check the api's structured log for `audit_emit_failed`. ## Job triggering `POST /api/v1/admin/jobs/trigger/{name}` whitelists the request body to: ```json { "full": true, "max_rows": 10000, "reenrich": true } ``` Other keys are silently dropped. The intent is to prevent admins from smuggling config-shaped values into the runner via shape mismatch. The `refresh-geoip` job short-circuits with `412 no_credential` when the configured provider is opt-in (MaxMind / IPinfo) and its credential isn't set. The DB-IP default never returns 412. ```json { "error": "no_credential", "provider": "maxmind", "missing": "MAXMIND_LICENSE_KEY" } ``` ## See also - [`api-overview.md`](./api-overview.md) — base URL, auth summary, conventions, worked examples. - [`auth-flows.md`](./auth-flows.md) — every auth path in detail. - [`architecture.md`](./architecture.md) — system overview. - [`frontend-development.md`](./frontend-development.md) — for replacement UIs.