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.
REST and gRPC: one contract, two surfaces
Section titled “REST and gRPC: one contract, two surfaces”| Aspect | REST | gRPC |
|---|---|---|
| Port | 8080 | 9090 |
| Encoding | JSON | Protobuf |
| Routing | Path + method → gRPC method | Native service/method dispatch |
| Source of truth | Protobuf google.api.http annotations | Protobuf service definitions |
| Metadata propagation | HTTP headers → gRPC metadata | Native metadata |
| Trace context | traceparent / tracestate headers | OTel 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.
Services behind the gateway
Section titled “Services behind the gateway”| Service | Purpose | Confirmed REST routes |
|---|---|---|
auth | Tenants, login, tokens, API keys, groups, SSO, MFA, sessions | POST /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-registry | Device CRUD, groups, tags, status | POST /api/v1/devices, GET /api/v1/devices/{device_id}, GET /api/v1/devices, PATCH/DELETE /api/v1/devices/{device_id} |
device-link | Device RPC, attributes, provisioning, gateways, OTA | POST /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 |
telemetry | Ingest, latest snapshot, historical query, metrics | POST /api/v1/telemetry, GET /api/v1/telemetry/{device_id}/latest, GET /api/v1/telemetry/{device_id} |
rules-engine | CEL rule CRUD, enable/disable, validation | POST /api/v1/rules, GET /api/v1/rules/{rule_id}, PATCH/DELETE /api/v1/rules/{rule_id}, POST /api/v1/rules/{rule_id}/enable |
chain-anchor | Batch status, anchor history | GET /api/v1/anchors, GET /api/v1/anchors/batches/{batch_id} |
verifier | Publicly verifiable blockchain proofs | GET /api/v1/verify/hash/{data_hash_hex}, POST /api/v1/verify/raw, GET /api/v1/verify/batch/{batch_id} |
dashboard-bff | Real-time fan-out to the dashboard | WebSocket /ws (not REST) |
gateway | Entry point, auth, rate limiting, ARC callback, health | POST /api/v1/callbacks/arc, GET /healthz, GET /readyz |
Authentication
Section titled “Authentication”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:
| Type | Format | Header | Used by |
|---|---|---|---|
| JWT access token | Ed25519-signed JWT | Authorization: Bearer <jwt> | Users / dashboard |
| API key | sk_live_... | Authorization: Bearer sk_live_... or X-API-Key: sk_live_... | Devices, service accounts, integrations |
GET /api/v1/devices HTTP/1.1Host: api.example.com:8080Authorization: Bearer eyJhbGciOiJFZERTQSIs...POST /api/v1/telemetry HTTP/1.1Host: api.example.com:8080Authorization: Bearer sk_live_a3bF7kLm9pQr2sT4uV6wX8yZContent-Type: application/jsonPOST /api/v1/telemetry HTTP/1.1Host: api.example.com:8080X-API-Key: sk_live_a3bF7kLm9pQr2sT4uV6wX8yZContent-Type: application/json| Condition | REST | gRPC |
|---|---|---|
| Valid credential | request proceeds | request proceeds |
| Missing credential on a protected route | 401 | UNAUTHENTICATED |
| Expired JWT | 401 (body suggests the refresh endpoint) | UNAUTHENTICATED |
| Token missing required scope | 403 | PERMISSION_DENIED |
| Health/readiness endpoint | allowed without auth | n/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.
Pagination
Section titled “Pagination”All list endpoints use opaque cursor pagination, never numeric offsets.
| Parameter | Where | Meaning |
|---|---|---|
page_size | query | Results per page. Defaults vary by endpoint (commonly 50); list endpoints cap the maximum. |
page_token | query | Opaque cursor from the previous response’s next_page_token. Omit for the first page. |
next_page_token | response | Cursor for the next page. An empty value means there are no more results. |
# First pagecurl "https://api.example.com:8080/api/v1/devices?page_size=20" \ -H "Authorization: Bearer $TOKEN"
# Next page — pass the cursor back verbatimcurl "https://api.example.com:8080/api/v1/devices?page_size=20&page_token=eyJ..." \ -H "Authorization: Bearer $TOKEN"Rate limiting
Section titled “Rate limiting”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
sessionsnamespace, atomic increment per request.
Every response carries the current rate-limit state:
| Header | Description | Present on |
|---|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window | every response |
X-RateLimit-Remaining | Requests remaining in the current window | every response |
X-RateLimit-Reset | Unix epoch second when the window resets (via TTL) | every response |
Retry-After | Seconds until the window resets | only 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 RequestsX-RateLimit-Limit: 1000X-RateLimit-Remaining: 0X-RateLimit-Reset: 1711000080Retry-After: 12Content-Type: application/json
{"code": "RESOURCE_EXHAUSTED", "message": "rate limit exceeded"}Error format
Section titled “Error format”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 code | HTTP | Meaning |
|---|---|---|
OK | 200 | Success |
INVALID_ARGUMENT | 400 | Malformed or invalid request |
FAILED_PRECONDITION | 400 | Tenant policy or missing prerequisite |
UNAUTHENTICATED | 401 | Missing or invalid credential |
PERMISSION_DENIED | 403 | Insufficient role, group permission, scope, or IP not allowlisted |
NOT_FOUND | 404 | Resource does not exist, or unknown route |
ALREADY_EXISTS | 409 | Duplicate resource |
RESOURCE_EXHAUSTED | 429 | Rate limit or tenant quota exceeded |
INTERNAL | 500 | Server error |
UNAVAILABLE | 503 | Service temporarily unavailable |
Unknown routes
Section titled “Unknown routes”A request to a route the gateway cannot match returns 404 with the standard
error body:
HTTP/1.1 404 Not FoundContent-Type: application/json
{"code": "NOT_FOUND", "message": "route not found"}ARC callback
Section titled “ARC callback”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.
| Step | Behaviour |
|---|---|
| Signature | ARC sends X-ARC-Signature: sha256=<hex> |
| Verification | Gateway computes HMAC-SHA256(body, ARC_CALLBACK_HMAC_SECRET) and compares in constant time |
| On match | Payload published to Redpanda anchor.tx.confirmed; 200 returned |
| On mismatch / missing | 401; payload not forwarded; corem_arc_callback_rejected_total{reason="invalid_signature"} incremented |
| Malformed JSON | 400; payload logged for debugging |
| Dev mode | If 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.
Health endpoints
Section titled “Health endpoints”Both endpoints are unauthenticated and are exposed by the gateway and by every backend service.
| Endpoint | Probe | Behaviour |
|---|---|---|
GET /healthz | Liveness | Always 200 while the process is alive |
GET /readyz | Readiness | 200 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.