Skip to main content

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

  1. The platform issues an SMS to the passenger's contact number with a templated link: https://<airline>.passengers.nexastudio.io?token=<jwt>.
  2. The PWA loads the token from the URL, exchanges it for a session cookie, and removes the token from the URL.
  3. 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

MethodPathNotes
GET/v1/mePassenger profile within the case (group, tier, language)
GET/v1/me/offerCurrent offer — hotel, address, voucher QR, transport, wallet card link
GET/v1/me/voucherRendered HTML voucher (printable)
GET/v1/me/walletIssued card details — balance, available, transactions
GET/v1/me/wallet/revealReturns a short-lived URL to Pomelo's PCI iframe for PAN/CVV reveal
GET/v1/me/transportTransport status (booked / pending / declined)
GET/v1/me/notificationsNotification 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

MethodPathNotes
POST/v1/me/offer/acceptAccept the current offer. Triggers RESOLVED transition on the sub-case.
POST/v1/me/offer/declineDecline the offer. Triggers saga rollback (booking cancel) and operator alert.
POST/v1/me/check-inSelf check-in trigger. Used when the passenger arrives at the hotel.
POST/v1/me/transport/requestOn-demand transport request (when the policy allows).
POST/v1/me/problemReport 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:

SurfaceRPM
Read endpoints30
Write endpoints10
POST /v1/auth/pnr-login5 / 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/offer every 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 Conflict on accept/decline — the offer was already accepted or declined (possibly from another tab). Reload /v1/me/offer to see the resolved state.
  • 429 Too Many Requests — honor Retry-After.

Where to next

Was this helpful?