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 localStorage or sessionStorage — 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 /bookings without 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 StateAllowed Next StatesTrigger
SEARCHINGVERIFIEDSuccessful verify
VERIFIEDPREBOOKEDSuccessful prebook
PREBOOKEDSERVICES_ATTACHEDSuccessful attach
PREBOOKEDBOOKINGUser initiates payment
SERVICES_ATTACHEDBOOKINGUser initiates payment
BOOKINGCONFIRMEDSuccessful /bookings
BOOKINGFAILEDProvider error, payment failure
FAILEDSEARCHINGUser 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

DecisionRecommendationReason
API key locationBackend onlyNever expose to client
Session storageServer-side DB or RedisSurvives tab close, enables retry
SSE handlingClient-side, direct to your proxyStreaming can't be batched through most ORMs
Stripe confirmationClient-side onlyBrowser SDK requirement
Booking retryBackend onlyIdempotency must be enforced server-side
Price change decisionUser-driven, never silentBilling compliance
Webhook reconciliationRequiredHandles client disconnect between Stripe and /bookings
transactionId storageSession DB record, always latestPrevents stale-ID payment failures