Skip to content

API overview

Every CORE-M API call goes through a single API gateway. The gateway serves gRPC on port 9090 and REST on port 8080, and uses gRPC-Gateway to transcode REST requests into the equivalent gRPC call on the backend service. There is one contract — the protobuf service definitions — exposed two ways.

This page documents the cross-cutting behaviour that applies to every call: how requests are authenticated, paginated, rate-limited, and how errors come back. For the catalogue of resources, see the per-service tables below and the in-app OpenAPI/Swagger.

AspectRESTgRPC
Port80809090
EncodingJSONProtobuf
RoutingPath + method → gRPC methodNative service/method dispatch
Source of truthProtobuf google.api.http annotationsProtobuf service definitions
Metadata propagationHTTP headers → gRPC metadataNative metadata
Trace contexttraceparent / tracestate headersOTel gRPC interceptor

A GET /api/v1/devices/{device_id} REST call is transcoded to the DeviceRegistryService.GetDevice gRPC call against the device registry; the protobuf response is serialized back to JSON. A native gRPC client calling the same method on port 9090 is proxied through with full metadata propagation and the response returned unmodified.

ServicePurposeConfirmed REST routes
authTenants, login, tokens, API keys, groups, SSO, MFA, sessionsPOST /api/v1/auth/login, POST /api/v1/auth/refresh, POST /api/v1/auth/validate, POST /api/v1/auth/api-keys — see Swagger for the full set
device-registryDevice CRUD, groups, tags, statusPOST /api/v1/devices, GET /api/v1/devices/{device_id}, GET /api/v1/devices, PATCH/DELETE /api/v1/devices/{device_id}
device-linkDevice RPC, attributes, provisioning, gateways, OTAPOST /api/v1/provision, POST /api/v1/devices/{device_id}/rpc, GET /api/v1/devices/rpc/{correlation_id}, POST /api/v1/gateway/{gateway_device_id}/connect
telemetryIngest, latest snapshot, historical query, metricsPOST /api/v1/telemetry, GET /api/v1/telemetry/{device_id}/latest, GET /api/v1/telemetry/{device_id}
rules-engineCEL rule CRUD, enable/disable, validationPOST /api/v1/rules, GET /api/v1/rules/{rule_id}, PATCH/DELETE /api/v1/rules/{rule_id}, POST /api/v1/rules/{rule_id}/enable
chain-anchorBatch status, anchor historyGET /api/v1/anchors, GET /api/v1/anchors/batches/{batch_id}
verifierPublicly verifiable blockchain proofsGET /api/v1/verify/hash/{data_hash_hex}, POST /api/v1/verify/raw, GET /api/v1/verify/batch/{batch_id}
dashboard-bffReal-time fan-out to the dashboardWebSocket /ws (not REST)
gatewayEntry point, auth, rate limiting, ARC callback, healthPOST /api/v1/callbacks/arc, GET /healthz, GET /readyz

Every request is authenticated except the health and readiness endpoints (/healthz, /readyz) and the ARC callback (/api/v1/callbacks/arc, which is validated by HMAC instead). The gateway’s auth middleware validates the credential by calling the auth service’s ValidateToken RPC, extracts tenant_id and user_id into the request context, then forwards the request.

Two credential types are accepted:

TypeFormatHeaderUsed by
JWT access tokenEd25519-signed JWTAuthorization: Bearer <jwt>Users / dashboard
API keysk_live_...Authorization: Bearer sk_live_... or X-API-Key: sk_live_...Devices, service accounts, integrations
GET /api/v1/devices HTTP/1.1
Host: api.example.com:8080
Authorization: Bearer eyJhbGciOiJFZERTQSIs...
ConditionRESTgRPC
Valid credentialrequest proceedsrequest proceeds
Missing credential on a protected route401UNAUTHENTICATED
Expired JWT401 (body suggests the refresh endpoint)UNAUTHENTICATED
Token missing required scope403PERMISSION_DENIED
Health/readiness endpointallowed without authn/a

JWT access tokens are short-lived; exchange a refresh token at POST /api/v1/auth/refresh to obtain a new pair. Scoped API tokens carry an explicit scope set — see Security & compliance for the scope table and IP-allowlist behaviour, and Auth & multi-tenancy for how tenant isolation is enforced.

All list endpoints use opaque cursor pagination, never numeric offsets.

ParameterWhereMeaning
page_sizequeryResults per page. Defaults vary by endpoint (commonly 50); list endpoints cap the maximum.
page_tokenqueryOpaque cursor from the previous response’s next_page_token. Omit for the first page.
next_page_tokenresponseCursor for the next page. An empty value means there are no more results.
Terminal window
# First page
curl "https://api.example.com:8080/api/v1/devices?page_size=20" \
-H "Authorization: Bearer $TOKEN"
# Next page — pass the cursor back verbatim
curl "https://api.example.com:8080/api/v1/devices?page_size=20&page_token=eyJ..." \
-H "Authorization: Bearer $TOKEN"

The gateway enforces a per-tenant sliding window counter, backed by an atomic increment on the Aerospike key ratelimit:{tenant_id}:{window_minute}. The window resets automatically via the key’s TTL.

  • Default limit: 1000 requests per minute per tenant.
  • Window: one minute, sliding.
  • Storage: Aerospike sessions namespace, atomic increment per request.

Every response carries the current rate-limit state:

HeaderDescriptionPresent on
X-RateLimit-LimitMaximum requests allowed in the windowevery response
X-RateLimit-RemainingRequests remaining in the current windowevery response
X-RateLimit-ResetUnix epoch second when the window resets (via TTL)every response
Retry-AfterSeconds until the window resetsonly on 429

When the limit is exceeded the gateway returns 429 Too Many Requests (gRPC RESOURCE_EXHAUSTED) with a Retry-After header, and increments corem_gateway_rate_limited_total{tenant=...}.

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711000080
Retry-After: 12
Content-Type: application/json
{"code": "RESOURCE_EXHAUSTED", "message": "rate limit exceeded"}

Errors are returned as a consistent JSON object regardless of which service produced them. The string code is the gRPC status code name; the HTTP status is derived from it by gRPC-Gateway.

{
"code": "NOT_FOUND",
"message": "device not found: dev-770e8400-..."
}
gRPC codeHTTPMeaning
OK200Success
INVALID_ARGUMENT400Malformed or invalid request
FAILED_PRECONDITION400Tenant policy or missing prerequisite
UNAUTHENTICATED401Missing or invalid credential
PERMISSION_DENIED403Insufficient role, group permission, scope, or IP not allowlisted
NOT_FOUND404Resource does not exist, or unknown route
ALREADY_EXISTS409Duplicate resource
RESOURCE_EXHAUSTED429Rate limit or tenant quota exceeded
INTERNAL500Server error
UNAVAILABLE503Service temporarily unavailable

A request to a route the gateway cannot match returns 404 with the standard error body:

HTTP/1.1 404 Not Found
Content-Type: application/json
{"code": "NOT_FOUND", "message": "route not found"}

POST /api/v1/callbacks/arc is the one public, unauthenticated API route. It receives transaction-confirmation callbacks from the ARC Merchant API and forwards the payload to the chain-anchor pipeline over Redpanda. Authenticity is established by HMAC, not by a bearer credential.

StepBehaviour
SignatureARC sends X-ARC-Signature: sha256=<hex>
VerificationGateway computes HMAC-SHA256(body, ARC_CALLBACK_HMAC_SECRET) and compares in constant time
On matchPayload published to Redpanda anchor.tx.confirmed; 200 returned
On mismatch / missing401; payload not forwarded; corem_arc_callback_rejected_total{reason="invalid_signature"} incremented
Malformed JSON400; payload logged for debugging
Dev modeIf ARC_CALLBACK_HMAC_SECRET is unset and ARC_CALLBACK_SKIP_VALIDATION=true, validation is skipped and a warning is logged

See Anchoring overview for how the confirmation flows through to proof finalization.

Both endpoints are unauthenticated and are exposed by the gateway and by every backend service.

EndpointProbeBehaviour
GET /healthzLivenessAlways 200 while the process is alive
GET /readyzReadiness200 if all dependencies are healthy; 503 if any is unhealthy
// GET /readyz — 200
{"auth": "healthy", "device-registry": "healthy", "telemetry": "healthy", "aerospike": "healthy"}
// GET /readyz — 503
{"auth": "healthy", "telemetry": "unhealthy: connection refused", "aerospike": "healthy"}

/healthz reports liveness and /readyz reports readiness: a dependency outage fails readiness without failing liveness.

The gateway allows cross-origin requests from the configured dashboard origin, including preflight OPTIONS requests with the matching Access-Control-Allow-Origin and method/header allowances. Other origins are not granted CORS access.