api-reference.md 6.4 KB

API reference

The OpenAPI document at /api/v1/openapi.yaml is canonical for endpoint shapes, request bodies, response schemas, and error envelopes. Browse it interactively at /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/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: <integer> 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=<header>
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:

{ "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.

{
  "error": "no_credential",
  "provider": "maxmind",
  "missing": "MAXMIND_LICENSE_KEY"
}

See also