Operator API
The Operator API is the primary surface that powers the Nexa operations console. It is an API Composition / Backend-For-Frontend (BFF) that aggregates per-domain services (cases, booking, policies, wallet, notifications, audit, …) into a single operator-shaped surface, plus a real-time WebSocket presence channel for the operator swarm.
Base URL: https://na.api.nexastudio.io (or your regional equivalent).
Audience: https://api.nexa/v1.
Auth: OIDC JWT with https://nexa/airline, https://nexa/role, https://nexa/operator_urn claims. See Authentication.
Roles
| Role | Inherits |
|---|---|
ADMIN | OPS_SUPERVISOR, AIRPORT_OPERATOR, FINANCE_AUDIT |
OPS_SUPERVISOR | AIRPORT_OPERATOR |
AIRPORT_OPERATOR | — |
FINANCE_AUDIT | — (parallel; read-only) |
Per-route role requirements are declared at the proxy edge; the role symbol below is the minimum role required (e.g., AIRPORT_OPERATOR+ means the role plus anything that inherits from it).
REST endpoints
Cases
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/cases | OPS_SUPERVISOR+ | Manual disruption declaration |
GET | /v1/cases | AIRPORT_OPERATOR+ | Cursor-paginated; filter by status, airport, from, to |
GET | /v1/cases/dashboard | AIRPORT_OPERATOR+ | Aggregated dashboard view (composition route) |
GET | /v1/cases/{case_urn} | AIRPORT_OPERATOR+ | Case detail |
GET | /v1/cases/{case_urn}/manifest | AIRPORT_OPERATOR+ | Paginated sub-cases for the case |
POST | /v1/cases/{case_urn}/passenger-groups | AIRPORT_OPERATOR+ | Group passengers for allocation |
POST | /v1/cases/{case_urn}/allocate | OPS_SUPERVISOR+ | Composition: validates via cases, dispatches to allocation |
POST | /v1/cases/{case_urn}/book-rooms | OPS_SUPERVISOR+ | Composition: validates + dispatches to booking |
Sub-cases (the saga unit)
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/subcases/{sub_case_urn}/lock | AIRPORT_OPERATOR+ | Acquire entity lock; returns fencing token |
POST | /v1/subcases/{sub_case_urn}/unlock | AIRPORT_OPERATOR+ | Explicit release |
POST | /v1/subcases/{sub_case_urn}/compensate | AIRPORT_OPERATOR+ | Submit compensation. Returns 202 Accepted in under 50 ms; saga runs async. |
The lock surface is detailed in Operational Panic § Entity locks.
Hotels & booking
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/hotels/search | AIRPORT_OPERATOR+ | Scatter-gather across vendors with 2 s time-box |
GET | /v1/hotels/list | AIRPORT_OPERATOR+ | Curated catalog |
GET | /v1/hotels/cases/{case_urn} | AIRPORT_OPERATOR+ | Hotels assigned to a case |
GET | /v1/booking/cases/{case_urn}/reservations | AIRPORT_OPERATOR+ | |
GET | /v1/booking/reservations/{reservation_urn} | AIRPORT_OPERATOR+ | |
GET | /v1/booking/reservations/{reservation_urn}/voucher/html | AIRPORT_OPERATOR+ | Rendered voucher (printable) |
POST | /v1/booking/manual-reservations | OPS_SUPERVISOR+ | Out-of-band manual booking |
GET | /v1/booking/manual-review | OPS_SUPERVISOR+ | Review queue |
POST | /v1/booking/manual-review/{review_urn}/{assign|resolve|escalate} | OPS_SUPERVISOR+ | Operator decisions on the review queue |
Policies
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/policies | OPS_SUPERVISOR+ | Create policy (manually-authored) |
POST | /v1/policies/generate-ai | OPS_SUPERVISOR+ | AI-driven synthesis from natural-language description |
GET | /v1/policies/active | AIRPORT_OPERATOR+ | List active policies for the tenant |
GET | /v1/policies/resolve | AIRPORT_OPERATOR+ | "Which policy applies here?" preview |
POST | /v1/policies/{policy_urn}/activate | ADMIN | Atomic activate (deactivates the previous active for the same scope) |
POST | /v1/policies/{policy_urn}/patch | OPS_SUPERVISOR+ | Bumps version |
POST | /v1/policies/{policy_urn}/deactivate | ADMIN | Archive |
Demand & allocation
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/demand/case/{case_urn} | AIRPORT_OPERATOR+ | Request rooms (INITIAL or incremental) |
POST | /v1/demand/{demand_urn}/approve | OPS_SUPERVISOR+ | Wave approval |
POST | /v1/demand/{demand_urn}/reject | OPS_SUPERVISOR+ | |
GET | /v1/demand/pending/approvals | OPS_SUPERVISOR+ | Approval queue |
POST | /v1/allocation/allocate | OPS_SUPERVISOR+ | Trigger allocation explicitly |
Contracts (direct hotel deals)
| Method | Path | Min role | Notes |
|---|---|---|---|
GET | /v1/contracts | OPS_SUPERVISOR+ | List active contracts |
POST | /v1/contracts | OPS_SUPERVISOR+ | Create / update direct contract |
POST | /v1/contracts/analyze-pdf | OPS_SUPERVISOR+ | AI wizard — extract contract terms from PDF or image |
Audit & notifications
| Method | Path | Min role | Notes |
|---|---|---|---|
GET | /v1/audit/case/{case_urn} | FINANCE_AUDIT, OPS_SUPERVISOR+ | Read-only audit log |
GET | /v1/audit/entity/{entity_urn} | FINANCE_AUDIT, OPS_SUPERVISOR+ | |
GET | /v1/notifications/case/{case_urn} | AIRPORT_OPERATOR+ | Per-case notification log |
POST | /v1/notifications/{case_urn}/send | AIRPORT_OPERATOR+ | Manual send (e.g., resend voucher) |
Exception agent (LLM)
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/agent/triage | OPS_SUPERVISOR+ | Triage a stuck case — classify cause, propose next-best-action |
POST | /v1/agent/{agent_action_urn}/{approve|reject|execute} | OPS_SUPERVISOR+ | Operator-in-the-loop |
The agent is constrained to an explicit tool allow-list. It cannot perform irreversible operations (booking confirmation, payment capture) on its own — every action requires operator approval. See Exception Agent.
Airports
| Method | Path | Min role | Notes |
|---|---|---|---|
GET | /v1/airports/search | AIRPORT_OPERATOR+ | Airport catalog with coordinates |
Orchestration progress
| Method | Path | Min role | Notes |
|---|---|---|---|
GET | /v1/orchestration/cases/{case_urn}/progress | AIRPORT_OPERATOR+ | Runtime progress of saga steps for the case |
Transport
| Method | Path | Min role | Notes |
|---|---|---|---|
POST | /v1/transport/cases/{case_urn}/transfers/request | AIRPORT_OPERATOR+ | Request ground transport (Uber / Cabify) |
WebSocket presence channel
The operator UI uses a Socket.IO connection at wss://na.api.nexastudio.io/ws/cases to subscribe to real-time state changes.
Authentication
Pass the operator access token in the auth payload during the Socket.IO handshake:
import { io } from 'socket.io-client';
const socket = io('wss://na.api.nexastudio.io', {
path: '/ws/cases',
auth: { token: accessToken },
transports: ['websocket'],
});
The token is validated at the upgrade. Failures close the socket with code 4401.
Events
| Event | Direction | Payload | Meaning |
|---|---|---|---|
case:updated | server → client | { caseUrn, status, … } | Case state changed (any sub-case transition fans up) |
subcase:state-changed | server → client | { subCaseUrn, status, lockedBy? } | Sub-case transition or lock change |
graph:step:started | server → client | { caseUrn, stepUrn, name, startedAt } | Saga step started |
graph:step:completed | server → client | { caseUrn, stepUrn, completedAt, result } | Saga step completed |
graph:step:progress | server → client | { caseUrn, stepUrn, progress, message? } | Long-running step heartbeat |
notification:dispatched | server → client | { caseUrn, channel, recipient } | Voucher dispatched on any channel |
Lock state in the UI
When an operator on another browser locks a sub-case you are watching, you receive subcase:state-changed with lockedBy: "urn:user:auth0|…". Render a 🔒 indicator. When that operator's WebSocket drops, you receive a second event with lockedBy: null within 50–200 ms. There is no need to poll.
Reconnection
The Socket.IO client handles reconnect automatically. On reconnect, the server replays missed case:updated events for rooms you were subscribed to over the last 60 seconds.
Idempotency
Every POST accepts an Idempotency-Key header (UUID v4 recommended). Replays return the same response and status code without re-executing the side effect. Idempotency state is retained for 24 hours.
The compensate endpoint additionally deduplicates by the operator-supplied compensation payload's hash — replays return 202 with the same correlationUrn.
Pagination
Cursor-based:
GET /v1/cases?limit=50&cursor=eyJzdGFydCI6Mn0%3D
{
"items": [ … ],
"nextCursor": "eyJzdGFydCI6NTJ9",
"hasMore": true
}
Pages are stable: a cursor returned at time T produces the same page at time T + δ even if new items are inserted, because cursors encode a snapshot offset.
Errors
RFC 7807 Problem Details. The instance field carries the W3C correlation URN. The most operationally important codes:
409 ConflictonPOST .../lock— sub-case is already locked. Body includeslockedByandexpiresInSeconds.409 ConflictonPOST .../compensate— OCC version mismatch (someone else already moved the sub-case). Body includes the currentversion.503 Service Unavailable— downstream circuit open. Body includesfailedDomain(e.g.,booking). HonorRetry-After: 30. Do not auto-retry — the circuit breaker is what prevents a vendor outage from cascading.
Composition routes
A handful of routes fan out to multiple backends and join the responses (e.g., GET /v1/cases/dashboard, POST /v1/cases/{urn}/allocate). Properties:
- Concurrent fan-out only. All sub-calls go in flight simultaneously.
- Per-call timeout: 3 seconds. A failed sub-call returns a partial-failure section in the response with the failed domain identified — the rest of the response still renders.
- No business logic in the join. The proxy assembles, the originating domain decides.
Where to next
- Authentication — token shape, refresh, revocation.
- Case Lifecycle — sub-case state machine the API drives.
- Operational Panic — the locks, soft-holds, and saga the API surfaces.
- OpenAPI — live spec for SDK generation.