API overview
Nyxtrace has one REST API, version 1. The browser calls authenticated routes
under /api/v1 on the web layer; the web layer calls the Go core on loopback.
The Go core is never exposed to the browser or the network.
All timestamps are UTC RFC 3339 with millisecond precision. Identifiers and
cursors are opaque strings. Additive response fields are backwards compatible;
a field documented as required is never omitted; a nullable field is returned as
null rather than left out.
Authentication and tenancy
Section titled “Authentication and tenancy”The person authenticates against your OIDC provider. The web layer resolves their tenant and roles from that session, then signs each request to the Go core.
tenant_id is derived from the session and is never accepted from a query
parameter, a request body, a client-chosen cookie, a telemetry payload or a
sensor-supplied host name. The ingest listener derives it from the sensor
identity its token maps to. A person with access to several tenants selects one
through a server-side session operation before any data route is used.
Every database query and every state-store key is scoped by the trusted tenant. The query layer has no unscoped overload: its typed methods require a non-empty tenant identity as their first argument, SQL uses parameters rather than interpolation and always includes the tenant predicate, and every mutable table’s compound key begins with the tenant.
| Role | Required for |
|---|---|
reader |
Any read route |
analyst |
Incident workflow writes, device patches, proposal decisions |
responder |
Response proposal writes |
admin |
Administrative health detail |
Both layers enforce the role on every operation, independently.
The internal request signature
Section titled “The internal request signature”For completeness, because it is the boundary that matters: the web layer strips same-named browser headers, then sends the service identifier, tenant, subject, sorted roles, issue and expiry times, a nonce and a signature.
The signature is HMAC-SHA256 over a canonical payload: a version marker, the
uppercase method, the pathname exactly as received, the canonical query, the
lowercase hex SHA-256 of the exact request body, the tenant, the subject, the
comma-joined sorted roles, the timestamps and the nonce. The canonical query
sorts pairs by encoded name and value, preserves repeated keys, and uses %20
for a space. An absent body digests as the SHA-256 of zero bytes.
Claims live at most 60 seconds, clock skew allowance is 30 seconds, and write
nonces are single-use until expiry. A failure returns unauthenticated without
naming the field that failed.
Wire rules
Section titled “Wire rules”A singular response:
{ "data": {}, "meta": { "request_id": "req_01J...", "generated_at": "2026-09-10T12:00:00.000Z" }}A collection adds next_cursor, which is null on the last page. Requests take
limit from 1 to 500 (default 100) and an opaque cursor. Pagination is
cursor-based with a stable tie-breaker and never uses page numbers.
An error:
{ "error": { "code": "invalid_time_range", "message": "from must be earlier than to", "request_id": "req_01J...", "details": {} }}Stable error codes
Section titled “Stable error codes”unauthenticated, forbidden, not_found, invalid_request,
invalid_time_range, cursor_expired, revision_conflict,
provider_disabled, provider_rate_limited, provider_unavailable,
provider_response_invalid, budget_exhausted, payload_too_large,
source_unavailable, internal_error.
Idempotency and revisions
Section titled “Idempotency and revisions”Every write requires an Idempotency-Key header and returns the original
response when retried. The key is bound to tenant, signed subject, method, path,
query and body — so the same key with different content is not a replay.
Mutable resources carry an integer revision, and a write supplies
If-Match: "<revision>". A conflicting write returns revision_conflict
instead of silently overwriting someone else’s change.
A claim stays in progress for at most five minutes, so a crash between claiming and responding permits a safe retry after that lease. Completed responses stay replayable for the configured durable-state retention.
Time windows
Section titled “Time windows”A window is half-open: from <= observed_at < to. The default is the last 15
minutes. The maximum graph window is 24 hours; the maximum flow-list window is
31 days.
Value formats
Section titled “Value formats”Addresses are canonical text — compressed IPv6, dotted decimal IPv4. Byte and packet counters are JSON integers. Scores are numbers from 0 through 100. Confidence is from 0 through 1.
Deployment configuration
Section titled “Deployment configuration”GET /api/v1/site returns what the graph needs to draw itself:
{ "data": { "site_id": "example-home", "display_name": "Example Home", "home_coordinates": { "latitude": 51.7, "longitude": 8.7 }, "vlans": [{ "vlan_id": 104, "name": "IOT", "cidrs": ["192.0.2.0/24"] }] }, "meta": { "request_id": "req_01J...", "generated_at": "2026-09-10T12:00:00.000Z" }}Coordinates and VLAN networks come only from deployment configuration.
home_coordinates is nullable, and when it is absent the globe does not invent
a location.