Flight Booking Architecture
A practical reference for engineering teams building production flight booking systems on LiteAPI
1. System Overview
LiteAPI is a provider-agnostic flight aggregation layer. It abstracts GDS, NDC, and LCC inventory behind a unified API and handles provider fan-out, pricing normalization, and payment intent creation. Your system owns the UX, state management, and business logic.
The API is stateful across the booking flow. An offerId generated at search is the thread that connects every downstream call. Lose it, corrupt it, or substitute it — everything breaks. Your architecture must treat offer state as a first-class concern.
Provider model: LiteAPI proxies to multiple upstream providers. Provider errors are common, response times vary, and prices change in real time. Build for partial failure from day one.
2. Frontend Responsibilities
The frontend drives the user experience but must not own booking state. Its job is to display, collect, and delegate.
What the frontend owns:
- Search form input and result rendering
- Streaming SSE connection and incremental result display
- Passenger data collection and client-side validation
- Stripe SDK initialization and payment confirmation
- Session-scoped state:
offerId,prebookId,transactionId,secretKey - UX flows: price change modals, expiry countdowns, seat maps
What the frontend must not do:
- Call LiteAPI directly from the browser — your API key must never be exposed client-side
- Store booking state in
localStorageorsessionStorage— tab closes and refreshes will break mid-flow users - Retry booking calls — retry logic belongs in the backend where idempotency can be enforced correctly
- Make decisions about price change acceptance — surface it, let the user decide, send the decision to the backend
Session state model:
Frontend session (memory only, per tab):
{
offerId: string,
prebookId: string,
transactionId: string, // always the latest — overwrite on attach-services
secretKey: string, // always the latest — overwrite on attach-services
verifiedPrice: number,
currency: string
}
Flush this state on booking confirmation or tab close. Never persist it across sessions.
3. Backend Responsibilities
Your backend is the only layer that should communicate with LiteAPI. It owns API key management, business logic, logging, and failure handling.
Core responsibilities:
- Proxy all LiteAPI calls with your API key injected server-side
- Persist booking state across request boundaries
- Enforce flow integrity (prevent
/bookingswithout a completed prebook) - Handle retries with correct idempotency semantics
- Log provider errors, price changes, and payment events for ops visibility
- Reconcile payment state with Stripe webhooks independently of the booking flow
Minimal data model:
-- Session table (ephemeral, TTL-driven)
booking_sessions (
session_id UUID PRIMARY KEY,
user_id UUID,
offer_id TEXT NOT NULL,
prebook_id TEXT,
transaction_id TEXT, -- updated on each attach-services call
verified_price DECIMAL,
currency CHAR(3),
status ENUM('SEARCHING','VERIFIED','PREBOOKED','SERVICES_ATTACHED','BOOKING','CONFIRMED','FAILED'),
expires_at TIMESTAMP,
created_at TIMESTAMP,
updated_at TIMESTAMP
)
-- Confirmed bookings (permanent)
bookings (
id UUID PRIMARY KEY,
session_id UUID REFERENCES booking_sessions,
user_id UUID,
booking_id TEXT NOT NULL, -- from LiteAPI
booking_ref TEXT NOT NULL, -- airline PNR
status TEXT,
total_charged DECIMAL,
currency CHAR(3),
provider_payload JSONB, -- full LiteAPI response
stripe_payment_id TEXT,
created_at TIMESTAMP
)Flow enforcement middleware:
Route: POST /api/checkout/prebook
→ validate session exists with status = 'VERIFIED'
→ validate session not expired
→ call LiteAPI /flights/prebooks
→ update session status → 'PREBOOKED'
Route: POST /api/checkout/book
→ validate session exists with status IN ('PREBOOKED', 'SERVICES_ATTACHED')
→ validate stripe payment confirmed (webhook or intent status check)
→ call LiteAPI /flights/bookings
→ insert into bookings table
→ update session status → 'CONFIRMED'
4. Payment Handling
Stripe Flow (Standard)
Stripe is the default and recommended path. The payment intent is created server-side by LiteAPI during prebook. Your backend never directly creates or charges a Stripe intent.
LiteAPI /prebooks ──► Stripe (creates PaymentIntent server-side)
│
▼
returns transactionId + secretKey
│
Backend stores transactionId
│
Frontend receives secretKey
│
Stripe SDK confirms payment (client-side)
│
Backend calls /flights/bookings
Key constraint: Stripe confirmation happens client-side. Booking confirmation happens server-side. These are two separate calls with a dependency: booking must not be called before Stripe confirms.
Webhook reconciliation: Run a Stripe webhook handler independently of your booking flow. On payment_intent.succeeded, mark the associated session as payment-confirmed in your database. This provides a reliable fallback if the client disconnects after Stripe confirms but before /bookings is called.
stripe_webhook → payment_intent.succeeded
→ update booking_sessions set stripe_confirmed = true where transaction_id = event.data.id
→ if status = 'BOOKING' and stripe_confirmed = true: retry /flights/bookings
Credit Line Flow
Only available when your account has payment bypass enabled. No Stripe SDK involvement.
POST /flights/bookings
{ "payment": { "method": "CREDIT" } }
The API validates credit line eligibility server-side. If the account is not configured for this, the request returns 400. Never expose credit line logic to the frontend — the decision of which payment method to use should be made server-side based on account configuration.
transactionId Lifecycle
This is the most operationally risky part of the flow.
prebook response → transactionId_v1, secretKey_v1
attach-services response → transactionId_v2, secretKey_v2 ← supersedes v1
/bookings call → must use transactionId_v2
Rule: always use the transactionId from the most recent API response.
Store transactionId in your session record. Overwrite it on every attach-services response. Your /book endpoint should always read from the session record, never from a client-supplied value.
5. State Management
Every booking is a state machine. Transitions are one-directional. Any attempt to skip a step or jump backwards should be rejected.
State Machine
SEARCHING
│
▼
VERIFIED ← POST /flights/verify succeeded, price stored
│
▼
PREBOOKED ← POST /flights/prebooks succeeded, prebookId stored
│
├──────────────────────────────────────┐
▼ ▼
SERVICES_ATTACHED (skip ancillaries)
│ │
└──────────────┬───────────────────────┘
▼
BOOKING ← POST /flights/bookings in-flight
│
┌────────┴────────┐
▼ ▼
CONFIRMED FAILED
State Transition Rules
| Current State | Allowed Next States | Trigger |
|---|---|---|
| SEARCHING | VERIFIED | Successful verify |
| VERIFIED | PREBOOKED | Successful prebook |
| PREBOOKED | SERVICES_ATTACHED | Successful attach |
| PREBOOKED | BOOKING | User initiates payment |
| SERVICES_ATTACHED | BOOKING | User initiates payment |
| BOOKING | CONFIRMED | Successful /bookings |
| BOOKING | FAILED | Provider error, payment failure |
| FAILED | SEARCHING | User restarts |
Session Expiry
Sessions expire when the offerId or prebookId can no longer be used. Enforce expiry server-side.
SEARCHING state: 30-minute TTL (user browsing)
VERIFIED state: 5-minute TTL (offer has expiration)
PREBOOKED state: 15-minute TTL (provider hold duration varies)
SERVICES_ATTACHED: 15-minute TTL (same as prebook)
BOOKING (in-flight): 2-minute TTL (network/provider timeout)
On expiry, return the user to SEARCHING state with a clear error. Never attempt to use an expired offerId or prebookId — provider rejections are inconsistent and hard to debug.
6. Failure Scenarios
Price Change
Detection: changes block present in verify response.
Flow:
POST /flights/verify
│
├── changes absent → proceed normally
│
└── changes present
│
├── price decreased → auto-proceed OR notify (low risk)
│
└── price increased
│
├── delta < threshold (e.g. < $10) → show inline, require click-through
│
└── delta >= threshold → full modal, require explicit accept
│
├── user accepts → update session.verified_price → proceed to prebook
│
└── user rejects → return to SEARCHING
Backend handling:
verify_response = liteapi.verify(offer_id)
if verify_response.changes:
old_price = verify_response.changes.pricing.old.display.total
new_price = verify_response.changes.pricing.new.display.total
session.price_change_delta = new_price - old_price
session.status = 'PRICE_CHANGED'
return { "priceChanged": True, "oldPrice": old_price, "newPrice": new_price }
session.verified_price = verify_response.journey.pricing.display.total
session.status = 'VERIFIED'Never silently absorb a price increase. It is a billing discrepancy and a compliance risk.
Provider Error
Provider errors surface as 502 (provider error) or 503 (service unavailable). They are distinct from client errors and require different handling.
At search (/flights/rates):
502 → show partial results from other providers, do not block UI
503 → show retry button, wait 2–3 seconds before retry
At verify (/flights/verify):
502 → retry once after 1s
→ if retry fails: offer_id may be stale, return to search with message
503 → display: "Prices are updating. Try again in a moment." + retry button
404 → offer expired, return to search
At prebook (/flights/prebooks):
502 → do NOT auto-retry (provider may have partially reserved)
→ wait 5s, call GET /flights/bookings to check for existing prebook
→ if none found: retry prebook once
→ if retry fails: set status = FAILED, prompt user to restart
503 → retry with exponential backoff (1s, 2s, 4s), max 3 attempts
At booking (/flights/bookings):
502 → safe to retry (endpoint is idempotent on prebookId)
→ retry up to 3 times with backoff
503 → retry with backoff
409 → do NOT retry, check for existing confirmed booking first
Ops requirement: Log all 5xx responses with provider, endpoint, offerId, and sessionId. This data is essential for identifying systematic provider degradation.
Payment Failure
Payment can fail at two points: Stripe confirmation (client-side) or the /bookings call (server-side).
Stripe confirmation failure:
stripe.confirmCardPayment() → error
│
├── card_declined → show decline message, offer retry with different card
│ prebookId is still valid, transactionId is still valid
│ user can retry payment without re-preboking
│
├── insufficient_funds → same as card_declined
│
├── expired_card → prompt for new card details
│
└── generic_error → show retry, log error.code for ops
The prebook is still valid after a Stripe failure. Do not discard the session. Allow payment retry without re-running prebook.
Booking call failure after Stripe success:
This is the critical scenario. Stripe has captured funds but LiteAPI /bookings failed.
stripe.confirmCardPayment() → succeeded
POST /flights/bookings → 502 or 503
│
├── Retry /bookings up to 3 times (idempotent)
│
├── If all retries fail:
│ → Set session.status = 'PAYMENT_CAPTURED_BOOKING_FAILED'
│ → Trigger ops alert immediately
│ → Show user: "Your payment was received. We're confirming your booking."
│ → Do NOT show an error to the user
│ → Background job retries /bookings every 30s for 10 minutes
│ → On success: send confirmation email, update session
│ → On permanent failure: initiate Stripe refund, notify user
│
└── Stripe webhook reconciliation catches any client-disconnect cases
Never show a payment error when Stripe has already charged the card. The user's money is gone — your job is to complete the booking or refund promptly.
7. Architecture Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT (Browser) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ Search UI │ │ Checkout UI │ │ Stripe SDK (payment form) │ │
│ │ (SSE stream) │ │ (state form) │ │ confirmCardPayment() │ │
│ └──────┬───────┘ └──────┬───────┘ └────────────┬──────────────┘ │
│ │ │ │ │
└─────────┼─────────────────┼────────────────────────┼─────────────────┘
│ REST │ REST │ Stripe API
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────┐
│ YOUR BACKEND │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ API Gateway / BFF │ │
│ │ /api/search → /api/verify → /api/prebook → /api/book │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼──────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Session │ │ Booking State │ │ Payment │ │
│ │ Store │ │ Machine │ │ Reconciliation │ │
│ │ (Redis/DB) │ │ (enforce flow) │ │ (Stripe webhooks) │ │
│ └──────┬──────┘ └──────────────────┘ └──────────────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────────────────────┐ │
│ │ LiteAPI Client │ │
│ │ (API key injected, retry logic, error normalization) │ │
│ └──────┬──────────────────────────────────────────────────────┘ │
│ │ │
└─────────┼────────────────────────────────────────────────────────────┘
│ HTTPS
▼
┌─────────────────────────────────────────────────────────────────────┐
│ LiteAPI │
│ │
│ /flights/rates ──► Provider Fan-out ──► GDS / NDC / LCC │
│ /flights/verify │
│ /flights/prebooks ──────────────────► Stripe (PaymentIntent) │
│ /flights/prebooks/{id}/services │
│ /flights/bookings │
│ /flights/bookings/{id} │
│ │
└─────────────────────────────────────────────────────────────────────┘
Side channels:
┌──────────────────┐ ┌──────────────────────────────────────┐
│ Stripe │─webhook─► Your Webhook Handler │
│ (payment) │ │ payment_intent.succeeded → │
└──────────────────┘ │ reconcile + retry if needed │
└──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ Background Jobs │
│ - Session expiry cleanup │
│ - PAYMENT_CAPTURED_BOOKING_FAILED retry (30s interval) │
│ - Stripe refund trigger on permanent booking failure │
└──────────────────────────────────────────────────────────────────┘
Architecture Decisions Summary
| Decision | Recommendation | Reason |
|---|---|---|
| API key location | Backend only | Never expose to client |
| Session storage | Server-side DB or Redis | Survives tab close, enables retry |
| SSE handling | Client-side, direct to your proxy | Streaming can't be batched through most ORMs |
| Stripe confirmation | Client-side only | Browser SDK requirement |
| Booking retry | Backend only | Idempotency must be enforced server-side |
| Price change decision | User-driven, never silent | Billing compliance |
| Webhook reconciliation | Required | Handles client disconnect between Stripe and /bookings |
transactionId storage | Session DB record, always latest | Prevents stale-ID payment failures |
Updated about 2 hours ago