Skip to content

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.

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.

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.

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": {}
}
}

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.

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.

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.

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.

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.