DEVELOPERS

THREVOX PUBLIC API

API reference

The ThreVox public API is the Tier 2/3 surface: everything your systems need to consume fleet telemetry, fault state, predictive-maintenance assessments and OTA rollouts. All traffic enters through one gateway.

Base URL
https://api.threvox.com

Every request carries a bearer token. The gateway validates it at the edge, strips identity-smuggling headers, attaches a correlation id and applies per-tenant rate limits; the owning service validates the token again and resolves your tenant from it - defense in depth, end to end.

Tenancy. You only ever see your own tenant's data. A resource id that belongs to another tenant answers 404, never 403, so foreign ids are indistinguishable from ids that do not exist.

Machine-readable OpenAPI documents are exposed per service at /v3/api-docs behind the same authentication.

Authentication

Authenticate with OAuth 2.0 client credentials against the ThreVox identity provider. API credentials are issued per tenant when your workspace is provisioned.

Request a token
curl -s https://id.threvox.com/realms/threvox/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id="$THREVOX_CLIENT_ID" \
  -d client_secret="$THREVOX_CLIENT_SECRET"
Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 300
}

Send the token on every request:

Usage
curl -s https://api.threvox.com/api/v1/fleet \
  -H "Authorization: Bearer $TOKEN"

Tokens are short-lived - refresh with a new client-credentials grant before expires_in elapses. Tokens carry aud: threvox-platform and your tenant claim; the tenant in the token scopes every query, topic and storage key. There is no way to select a tenant per request, by design.

Conventions

Versioning

Routes are versioned (/api/v1, /api/aemp/v1) and evolve additively: new fields are optional, existing fields are never repurposed. A breaking change ships as a new /api/vN route. Tolerate unknown response fields, and degrade gracefully on unknown enum values.

Errors

Errors are RFC 7807 problem details with content type application/problem+json. No stack traces, no internal identifiers.

429 Too Many Requests
{
  "type": "https://developers.threvox.com/errors/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Tenant publish budget exhausted; retry after the window resets.",
  "instance": "/api/v1/rollouts"
}

Rate limits

Budgets are per tenant and apply to the abuse-prone groups (publishing, rollout creation, authentication). Rejections are 429 problem details.

Idempotency

Publish and rollout-creation endpoints honor an Idempotency-Key header. Retrying with the same key returns the original result instead of repeating the side effect. Keys should be UUIDs, generated per logical operation.

Correlation

Send X-Correlation-Id (matching [A-Za-z0-9._-]{1,64}) to stitch platform traces into your own; the platform echoes it on the response and mints one when absent.

Timestamps

All timestamps are ISO-8601 in UTC, for example 2026-07-16T13:58:41Z.

Fleet & telemetry DASHBOARDSERVICE

Read models over everything your devices report: per-device summaries, the latest value of every decoded signal, and fleet-wide projections.

MethodPathPurpose
GET/api/v1/fleetDevice summaries: vin, last seen, active fault count, worst severity
GET/api/v1/fleet/{deviceId}/telemetryLatest value per SPN, name and unit enriched
GET/api/v1/fleet/{deviceId}/faultsDM1 fault state per source address
GET/api/v1/fleet/{deviceId}/maintenancePredictive-maintenance assessment for one device
GET/api/v1/fleet/maintenanceOne assessment per assessed device
GET/api/v1/fleet/rolloutsRollout state projections
GET/api/v1/fleet
Request
curl -s https://api.threvox.com/api/v1/fleet \
  -H "Authorization: Bearer $TOKEN"
Response 200
[
  {
    "deviceId": "TVX-0142",
    "vin": "3HAMMAAR1PL512345",
    "lastSeenAt": "2026-07-16T13:58:41Z",
    "activeFaultCount": 1,
    "worstSeverity": "WARNING"
  },
  {
    "deviceId": "TVX-0157",
    "vin": "3HAMMAAR7PL518220",
    "lastSeenAt": "2026-07-16T13:58:12Z",
    "activeFaultCount": 0,
    "worstSeverity": "NONE"
  }
]
GET/api/v1/fleet/{deviceId}/telemetry

The latest decoded value for every signal the device reports, enriched with the catalog name and unit.

Response 200
{
  "deviceId": "TVX-0142",
  "signals": [
    { "spn": 190, "name": "Engine Speed",                "value": 1287.5, "unit": "rpm",   "sampledAt": "2026-07-16T13:58:40Z" },
    { "spn": 110, "name": "Engine Coolant Temperature",  "value": 87,     "unit": "deg C", "sampledAt": "2026-07-16T13:58:40Z" },
    { "spn": 96,  "name": "Fuel Level",                  "value": 62.4,   "unit": "%",     "sampledAt": "2026-07-16T13:58:38Z" }
  ]
}

Faults DASHBOARDSERVICE

GET/api/v1/fleet/{deviceId}/faults

Active DM1 fault state per source address, with lamp status and occurrence counts. Lamp values follow J1939 semantics: STOP, WARNING, PROTECT, MIL.

Query parameterTypeMeaning
includeClearedbooleanAlso return faults that are no longer active (DM2-style history). Default false.
Response 200
{
  "deviceId": "TVX-0142",
  "faults": [
    {
      "sourceAddress": 0,
      "spn": 100,
      "fmi": 1,
      "occurrenceCount": 3,
      "lamp": "WARNING",
      "description": "Engine Oil Pressure - data valid but below normal, most severe",
      "active": true,
      "firstSeenAt": "2026-07-16T09:14:02Z",
      "lastSeenAt": "2026-07-16T13:58:41Z"
    }
  ]
}

Predictive maintenance DASHBOARDSERVICE

GET/api/v1/fleet/{deviceId}/maintenance

The latest health assessment for a device, computed per tenant from fault history and signal trends. Answers 404 until the device has been assessed at least once. GET /api/v1/fleet/maintenance returns one assessment per assessed device.

Response 200
{
  "deviceId": "TVX-0142",
  "healthScore": 0.82,
  "riskBand": "WATCH",
  "topContributors": [
    { "spn": 100, "name": "Engine Oil Pressure", "weight": 0.41 }
  ],
  "modelVersion": "tenant-2026.06",
  "assessedAt": "2026-07-16T05:00:00Z"
}

Rollouts & OTA FWUPDATESERVICE

Publish UI layouts and firmware images, then deliver them with staged rollouts. All artifacts are signed; devices verify the ECDSA P-256 signature and SHA-256 digest before a slot is ever committed, and roll back to the last known-good on any failure.

MethodPathPurpose
POST/api/ui/publishPublish a UI layout (SUI payload; Idempotency-Key honored)
GET/api/ui/versionsPublished UI versions for the tenant
GET/api/ui/versions/{id}/usb-packageA published layout as a signed USB update file (.uf2)
POST/api/fw/publishPublish a firmware image (octet-stream; headers below)
GET/api/fw/versionsPublished firmware versions
GET/api/fw/versions/{id}/usb-packageA published firmware image as a signed USB update file; ?restart=true installs as soon as it verifies
GET/api/fw/signing-keyECDSA P-256 verification key devices pin
POST/api/v1/rolloutsCreate a staged rollout
POST/api/v1/rollouts/{id}/expandAlso: /pause, /resume, /rollback
GET/api/v1/rolloutsRollout list; /{id} for detail with per-device delivery
POST/api/fw/publish

Body is the firmware image as application/octet-stream. Metadata travels in headers: X-Version-Name (required), X-Hw-Compat (optional hardware compatibility tag), X-Mandatory (optional boolean).

GET/api/fw/versions/{id}/usb-package

For clusters with no connectivity. Answers application/x-uf2 with a Content-Disposition file name; copy the file onto the cluster's TVX UPDATE USB drive. The package is signed for your tenant and the version's hardware tag, and the cluster verifies it exactly as it verifies an over-the-air update. Without restart the image installs at the next power-up. A version outside your tenant answers 404; a stored artifact that no longer matches its published digest or signature answers 409. The layout form, /api/ui/versions/{id}/usb-package, takes no parameters.

POST/api/v1/rollouts
FieldTypeMeaning
artifactKindstringUI or FIRMWARE
versionNamestringA published version to deliver
deviceIdsstring[]Target devices, all within your tenant
canaryCountintegerDevices served first; expand promotes the rest
Request
curl -s -X POST https://api.threvox.com/api/v1/rollouts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 4b8ee1a6-3d0f-4a2e-9d5c-6f2a71c0f9d3" \
  -H "Content-Type: application/json" \
  -d '{
    "artifactKind": "FIRMWARE",
    "versionName": "fw-2026.07.1",
    "deviceIds": ["TVX-0142", "TVX-0143", "TVX-0157"],
    "canaryCount": 1
  }'

Rollouts move through expand, pause, resume and rollback as sub-resources. A device acknowledging DOWNLOADED means verified-and-armed; the swap happens on the device's next safe boot.

AEMP export DASHBOARDSERVICE

GET/api/aemp/v1/Fleet/{page}

An AEMP 2.0 / ISO 15143-3 XML snapshot of the fleet, for mixed-fleet tools that already speak the standard. Paged; page numbers start at 1.

Response 200 (abridged)
<Fleet xmlns="http://standards.iso.org/iso/15143/-3">
  <Equipment>
    <EquipmentHeader>
      <OEMName>THREVOX</OEMName>
      <Model>TVX-C43C</Model>
      <EquipmentID>TVX-0142</EquipmentID>
      <SerialNumber>3HAMMAAR1PL512345</SerialNumber>
    </EquipmentHeader>
    <CumulativeOperatingHours>
      <Hour>1287.4</Hour>
      <DateTime>2026-07-16T13:58:41Z</DateTime>
    </CumulativeOperatingHours>
  </Equipment>
</Fleet>

Billing BILLINGSERVICE

GET/api/v1/billing/entitlements

Your tenant's tier, subscription status and feature list. Answers 404 without an active subscription.

Response 200
{
  "tier": "TIER_3",
  "status": "ACTIVE",
  "features": ["TELEMETRY_API", "LIVE_STREAMS", "OTA_ROLLOUTS", "DASHBOARD", "PREDICTIVE_MAINTENANCE"],
  "currentPeriodEnd": "2026-08-01T00:00:00Z"
}

POST /webhooks/stripe exists on this service but is signature-authenticated for the payment provider - it is not a client endpoint.

User administration USERMANAGEMENTSERVICE

Tenant admins (owner or fleet-admin role) manage their own users. Everything is scoped to the caller's tenant; a user id from another tenant answers 404.

MethodPathPurpose
GET/api/v1/usersUsers in the tenant (roles, enabled, self flag)
GET/api/v1/users/assignable-rolesRoles the caller may assign
POST/api/v1/usersInvite a user; returns a one-time temporary password
PUT/api/v1/users/{id}/rolesReplace a user's roles
POST/api/v1/users/{id}/activateAlso: /deactivate
POST/api/v1/users/{id}/reset-passwordIssue a new one-time temporary password
Invite a user
curl -s -X POST https://api.threvox.com/api/v1/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "operator@customer.example", "roles": ["operator"] }'
Response 201
{
  "id": "8c2f1e0a-77b4-4f4e-9b6d-2f9d3a51c007",
  "email": "operator@customer.example",
  "roles": ["operator"],
  "enabled": true,
  "temporaryPassword": "returned-once-then-never-again"
}

Policy is enforced server-side: an admin can never grant a role above their own level, act on a higher-privileged user, change their own roles, deactivate themselves, or remove the tenant's last active owner (409). The internal platform-admin role is never assignable (400).

Reference catalog J1939 CATALOG

GET/api/catalog/pgns

The J1939 PGN/SPN reference rows the platform decodes with - useful for labeling and unit handling on your side of the API.

Response 200 (one row)
{
  "pgn": 61444,
  "label": "EEC1",
  "name": "Electronic Engine Controller 1",
  "spns": [
    { "spn": 190, "name": "Engine Speed", "unit": "rpm", "resolution": 0.125, "offset": 0, "lengthBits": 16 }
  ]
}

Live streams (SSE) DASHBOARDSERVICE

GET/api/v1/fleet/stream/telemetry
GET/api/v1/fleet/stream/faults

Server-sent events, tenant-scoped, kept alive with heartbeats. Reconnect with backoff; after a reconnect, fetch the REST read model once to resynchronize, then follow the stream.

Request
curl -N https://api.threvox.com/api/v1/fleet/stream/telemetry \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/event-stream"
Stream
event: telemetry
data: {"deviceId":"TVX-0142","spn":190,"name":"Engine Speed","value":1287.5,"unit":"rpm","at":"2026-07-16T13:58:40Z"}

event: telemetry
data: {"deviceId":"TVX-0142","spn":110,"name":"Engine Coolant Temperature","value":87,"unit":"deg C","at":"2026-07-16T13:58:41Z"}

event: heartbeat
data: {"at":"2026-07-16T13:58:55Z"}

MQTT device link DEVICE PLANE

Devices talk to the platform over MQTT with QoS 1 and per-device mutual TLS. The topic namespace is per-tenant, and a device's credentials only permit its own topics - the namespace is a hard isolation boundary, not a convention.

TopicDirectionPayload
tenants/{tenantId}/devices/{deviceId}/telemetrydevice to cloudTelemetry frames, sequence-numbered
tenants/{tenantId}/devices/{deviceId}/faultsdevice to cloudDM1/DM2 fault transitions
tenants/{tenantId}/devices/{deviceId}/otacloud to deviceSigned artifact notifications (S3 URL + digest)

Devices buffer locally while offline and replay with sequence numbers on reconnect; the platform deduplicates, so at-least-once delivery is safe. A Last Will and Testament marks dropout. Device credentials are provisioned with the unit and revoked on decommission - the device plane is not a general-purpose publish API.

Schemas & fixtures

Message schemas (device link, Kafka processing topics, the SUI layout binary and the BLE companion protocol) are versioned contracts, tested on both producer and consumer sides in CI. Payloads carry a schema version; consumers must tolerate unknown fields and reject missing required ones.

Per-service OpenAPI documents live at /v3/api-docs behind the same authentication. If you are building a deep integration and want the fixture bundle, write to us.