Passenger API
The Passenger API is the read-optimized surface that powers the passenger-facing mobile webapp. It is the CQRS read side of Nexa: every read is served from a denormalized snapshot store fully isolated from the operational cases database, so the tens of thousands of refreshes a Tier-1 hub closure generates cannot impact the operator UI.
Base URL: https://<airline>.passengers.nexastudio.io (per-tenant subdomain).
Audience: https://passengers.nexa/v1.
Auth: magic-link or PNR-login session token (Ed25519-signed). See Authentication.
Per-tenant subdomain routing
Passengers don't choose their tenant — they arrive via SMS or PNR-login, and the platform routes them. Each registered airline has its own subdomain (latam.passengers.nexastudio.io, avianca.passengers.nexastudio.io, …). The SMS template embeds the tenant subdomain so passengers always land on the right deployable.
A single shared passengers.nexastudio.io is not offered. Cross-tenant edge routing creates exactly the failure mode tenant isolation exists to prevent: one airline's hub closure overwhelming another airline's passenger surface.
Two entry paths
Path A — magic-link via SMS (the standard happy path)
- The platform issues an SMS to the passenger's contact number with a templated link:
https://<airline>.passengers.nexastudio.io?token=<jwt>. - The PWA loads the token from the URL, exchanges it for a session cookie, and removes the token from the URL.
- Subsequent requests use the session cookie.
The magic-link token is short-lived (single-use, 7-day TTL until exchanged) and case-scoped — it can only access the case it was minted for.
Path B — PNR-login on the webapp
A passenger who lost the SMS link or never received it can log in directly:
POST /v1/auth/pnr-login HTTP/1.1
Content-Type: application/json
{
"pnr": "XYZ123",
"lastName": "PEREZ",
"captcha": "<turnstile-token>"
}
Response on success:
{
"sessionToken": "eyJ…",
"caseUrn": "urn:case:c-7f8e1",
"expiresAt": "2026-05-13T18:00:00Z"
}
The PNR-login path is aggressively rate-limited:
- Per-IP: 5 attempts / 15 min.
- Per-PNR globally: 10 attempts / 24 h (across all IPs).
- Captcha: an edge-issued challenge token is required on every attempt.
This defeats credential-stuffing — an attacker brute-forcing PNRs hits the per-PNR limit before getting useful signal.
Session token
Both paths yield a token with claims:
{
"iss": "https://passengers.nexastudio.io/",
"sub": "urn:passenger-session:<uuid>",
"aud": "https://passengers.nexa/v1",
"exp": 1746489600,
"case_urn": "urn:case:c-7f8e1",
"group_id": "g-1124",
"tenant": "urn:airline:latam"
}
The token is case-scoped. A passenger cannot read another case even within their own airline. When a case CLOSED transition fires, all of its passenger tokens are revoked.
REST endpoints
Snapshot read path
| Method | Path | Notes |
|---|---|---|
GET | /v1/me | Passenger profile within the case (group, tier, language) |
GET | /v1/me/offer | Current offer — hotel, address, voucher QR, transport, wallet card link |
GET | /v1/me/voucher | Rendered HTML voucher (printable) |
GET | /v1/me/wallet | Issued card details — balance, available, transactions |
GET | /v1/me/wallet/reveal | Returns a short-lived URL to Pomelo's PCI iframe for PAN/CVV reveal |
GET | /v1/me/transport | Transport status (booked / pending / declined) |
GET | /v1/me/notifications | Notification log (SMS, email, WhatsApp) sent to this passenger |
All reads return the denormalized snapshot — one DB read per request, edge-cached for 60 s.
Write path
| Method | Path | Notes |
|---|---|---|
POST | /v1/me/offer/accept | Accept the current offer. Triggers RESOLVED transition on the sub-case. |
POST | /v1/me/offer/decline | Decline the offer. Triggers saga rollback (booking cancel) and operator alert. |
POST | /v1/me/check-in | Self check-in trigger. Used when the passenger arrives at the hotel. |
POST | /v1/me/transport/request | On-demand transport request (when the policy allows). |
POST | /v1/me/problem | Report a problem (lost room, wrong card, missing transport). Routes to operator manual-review queue. |
Writes are idempotent — replays with the same Idempotency-Key header return the same response. The accept/decline path additionally deduplicates by sub-case state: a POST /v1/me/offer/accept against an already-RESOLVED sub-case returns the cached response without re-publishing the event.
Edge caching
Read endpoints carry Cache-Control: private, max-age=60 and a Vary: Authorization header. The Nexa edge CDN caches per-session at the edge. Keys mutated by a write endpoint emit a cache-purge instruction; the next read pulls fresh.
The 60-second TTL is calibrated to the snapshot-update cadence — the longest gap between an operational state change and the snapshot reflecting it under burst load.
Rate limits
Per session:
| Surface | RPM |
|---|---|
| Read endpoints | 30 |
| Write endpoints | 10 |
POST /v1/auth/pnr-login | 5 / 15 min per IP, 10 / 24 h per PNR |
Idempotency-keyed retries within the rate-limit window are exempt from quota.
Eventual consistency
The passenger surface is eventually consistent. A typical lag from operational state change to snapshot read is sub-second; under burst load it can stretch to a few seconds. The PWA reflects this by:
- Showing a freshness timestamp on each fetch ("updated 12s ago").
- Polling
/v1/me/offerevery 30 seconds when the passenger is on the offer screen and the offer is in a non-terminal state. - Surfacing a non-blocking banner ("Your offer just changed — refresh to see the latest") when a polled response differs from the cached one.
For real-time passenger updates (e.g., transport ETA), the PWA opens a Server-Sent Events stream at GET /v1/me/events. SSE rather than WebSockets — passenger sessions are short-lived, the events flow one direction (server → client), and SSE works through the edge without sticky routing.
Privacy & PII
Logs and metrics on the passenger surface never carry passenger names, emails, phones, PNRs, or document numbers. Identifiers in logs are session URNs (urn:passenger-session:…) and case URNs only. PII fields appear in response payloads (the passenger needs to see their own name); they are stripped from access logs at the edge.
The wallet PAN/CVV reveal flow runs inside Pomelo's PCI-compliant iframe. Nexa's response to /v1/me/wallet/reveal is a short-lived URL to that iframe; Nexa never sees the PAN or CVV.
Errors
RFC 7807 Problem Details. The most common:
401 Unauthorized— session token expired or revoked. PWA should redirect to PNR-login.403 Forbidden— the case has been closed and the token revoked. Show "your stay is complete" UI.409 Conflictonaccept/decline— the offer was already accepted or declined (possibly from another tab). Reload/v1/me/offerto see the resolved state.429 Too Many Requests— honorRetry-After.
Where to next
- Authentication
- Case Lifecycle — the saga the passenger participates in.
- Operational Panic § CQRS — why the snapshot store is a separate read path.