Booking Flow Recipes

Practical implementation patterns for common flight booking scenarios using LiteAPI.

Recipe 1: Basic Booking Flow

When to use: MVPs, internal tools, B2B platforms where UX simplicity matters more than conversion optimization.

Flow

Search → Verify → Prebook → Book

Step-by-Step

1. Search Send a legs array with origin, destination, and date. Store the full offerId from the offer you want to book.

POST /flights/rates
{
  "legs": [{ "origin": "JFK", "destination": "LHR", "date": "2026-08-01" }],
  "adults": 1,
  "currency": "USD"
}

Store from response: offerId, pricing.display.total

2. Verify Confirm the offer is still live and get a binding price.

POST /flights/verify
{ "offerId": "<offerId>" }

Store from response: offerId (same), confirmed pricing. If changes is present, log it — in a basic flow, you can proceed or abort.

3. Prebook Reserve the offer, initiate Stripe payment intent.

POST /flights/prebooks
{
  "offerId": "<offerId>",
  "usePaymentSdk": true,
  "contact": { "firstName": "Jane", "lastName": "Smith", "email": "[email protected]", "phone": "+1234567890" },
  "passengers": [{ "type": "ADT", "firstName": "Jane", "lastName": "Smith", "birthday": "1990-01-01", "gender": "F", "document": { ... } }]
}

Store from response: prebookId, transactionId, secretKey

4. Confirm Stripe payment (client-side) Use the Stripe SDK with secretKey to confirm the payment intent before calling bookings.

5. Book

POST /flights/bookings
{
  "prebookId": "<prebookId>",
  "payment": { "method": "TRANSACTION_ID", "transactionId": "<transactionId>" }
}

Store from response: bookingId, bookingRef (airline PNR)

Key Decisions

  • usePaymentSdk: true is the correct default for Stripe.
  • Skip ancillaries entirely — servicesAttachable in the prebook response can be ignored for now.
  • On 404 at any step, the offer has expired — restart from search.

Pitfalls

  • Don't reuse an offerId from a previous session. They expire.
  • Don't skip verify. Even in a basic flow, an unverified price creates billing risk.
  • Passenger birthday and document fields are required by most providers. Omitting them causes prebook failures that are hard to debug.

Recipe 2: High-Conversion Checkout Flow

When to use: Consumer-facing products where checkout conversion is the primary KPI.

Flow

Search (SSE) → Select → Verify → Show Summary + Price Change UX → Prebook → Seat Selection → Stripe → Book → Confirm

Step-by-Step

1. Search with SSE Stream results as they arrive. Render each provider batch immediately.

POST /flights/rates
Headers: Accept: text/event-stream

Show a per-provider loading indicator. Users who see results arrive incrementally stay engaged longer than users waiting for a single load.

2. Select Display: fare family, baggage policy, isCheapest flag, price. Do not show fareBasisCode or bookingCode to end users.

3. Verify immediately on selection Don't wait for the user to reach checkout. Call verify as soon as the user taps "Select". This gives you time to surface price changes before they hit the summary page.

4. Handle price change If changes is present in the verify response:

  • Show old price (strikethrough) and new price
  • Require explicit user action: "Accept new price" button
  • Do not auto-proceed

See Recipe 3 for detailed price change handling.

5. Show booking summary Display: itinerary, passenger form, verified price, baggage. Keep the form short — only collect what's needed for the prebook call.

6. Prebook Same as Recipe 1. On success, don't immediately show the payment step.

7. Seat selection Use servicesAttachable.groups from the prebook response to render a seat map. Gate this step — if no seats are attachable, skip directly to payment.

If user selects seats, call:

POST /flights/prebooks/{prebookId}/services
{ "selectedServices": [...] }

Critical: Replace your stored transactionId and secretKey with the ones from this response.

8. Stripe + Book Confirm payment using the current secretKey. Then call /bookings with the current transactionId.

9. Confirmation Display bookingRef prominently. Email it immediately.

Key Decisions

  • Verify on selection, not on page load. Users who linger expire offers.
  • Seat selection before payment consistently outperforms post-booking upsell.
  • Use the expiration field from verify to show a countdown timer. It creates urgency without being dishonest.

Pitfalls

  • Forgetting to replace transactionId after attach-services is the most common production bug in this flow.
  • Collecting more passenger fields than required increases form abandonment. Collect the minimum the prebook endpoint needs.
  • Showing raw provider data (fareBasisCode, bookingCode) in the UI erodes trust.

Recipe 3: Handling Price Changes

When to use: Any flow where verify returns a changes block. This is not an edge case — price changes happen frequently on popular routes.

What Triggers a Price Change

  • Fare sold out between search and verify
  • Provider re-pricing the fare class
  • FX rate shift (if currency conversion is applied)

Detection

const verifyResponse = await verify(offerId);

if (verifyResponse.data[0].changes) {
  const { old: oldPricing, new: newPricing } = verifyResponse.data[0].changes.pricing;
  const delta = newPricing.display.total - oldPricing.display.total;
  showPriceChangeModal({ oldTotal: oldPricing.display.total, newTotal: newPricing.display.total, delta });
}

UX Recommendations

Price increased:

  • Show old price crossed out, new price highlighted in amber
  • Show the delta explicitly: "+$12.50"
  • One clear CTA: "Accept $312.50 and continue"
  • Secondary: "Go back to results"
  • Never auto-accept and proceed silently

Price decreased:

  • Show it as a positive — "Good news: price dropped to $287.00"
  • Auto-proceed is acceptable here, but a confirmation still builds trust

What not to do:

  • Do not hide the change and present only the new price as if nothing happened
  • Do not force users to restart the entire search — the verified offerId is still valid

Retry Logic

verify() →
  if 404: offer expired → restart search
  if changes.pricing: show price change modal
    if user accepts: proceed to prebook with same offerId
    if user rejects: return to results
  if no changes: proceed normally

Timing Matters

If your verify-to-prebook window is longer than 5 minutes, re-verify before prebook. Verified prices are not guaranteed indefinitely — they reflect a point-in-time snapshot.

Pitfalls

  • Ignoring the changes block entirely and proceeding — the user sees an unexpected charge and disputes it.
  • Treating any changes as a hard error and aborting — most users accept small price differences if shown transparently.
  • Verifying too early (e.g., on hover) and then showing stale verify data at checkout.

Recipe 4: Adding Ancillaries (Revenue Optimization)

When to use: Any consumer-facing product. Ancillaries are not optional polish — they are a meaningful revenue layer on every booking.

When to Show Ancillaries

Show ancillaries after prebook, before payment. This is the highest-conversion placement:

  • The user has committed intent (they completed the passenger form and triggered prebook)
  • Payment hasn't happened yet, so the total is still flexible
  • The servicesAttachable data from prebook is fresh and specific to this passenger/route

Do not show ancillaries:

  • Before verify (prices may change, making your upsell stale)
  • After booking confirmation (attach-services requires an active prebook)

Reading servicesAttachable

const { servicesAttachable } = prebookResponse.data[0];

// servicesAttachable.groups contains seat maps and baggage options
// Each group is linked to a segment via segmentKey

const seatGroups = servicesAttachable.groups.filter(g => g.type === 'SEAT');
const baggageGroups = servicesAttachable.groups.filter(g => g.type === 'BAGGAGE');

Prioritization

Show seats first. Seat selection has higher attach rates and higher perceived value than extra baggage. A visual seat map outperforms a text list.

Show baggage second. Pre-select 1 bag for routes where checked luggage is common (long-haul, international). Let users deselect rather than opt in.

Skip gracefully. If servicesAttachable is empty or has no attachable groups for a segment, skip the step silently. Don't show an empty "Add Extras" screen.

UX Tips

  • Show the per-seat price clearly in the seat map. Hidden pricing discovered at payment causes drop-off.
  • Use seatsRemaining from the fare data to create scarcity signals ("Only 3 seats left at this price").
  • Show baggage weight and dimensions — users make decisions based on this, not just price.
  • Offer a "Skip" option that's visible but not prominent. Users who skip once are more likely to select on future bookings.

After Attachment

POST /flights/prebooks/{prebookId}/services
{
  "selectedServices": [
    { "serviceId": "SEAT_12A", "segmentKey": "d0a8a7dd", "passengerRef": "PAX1" },
    ...
  ]
}

Always update your payment state after this call:

const { transactionId, secretKey, price, currency } = servicesResponse.data[0];
// Replace stored transactionId and secretKey
// Update displayed total with new price

Pitfalls

  • Using the original transactionId after attach-services. The new one supersedes it completely.
  • Showing seats for segments where the carrier doesn't support seat selection — check that the group has selectable seats before rendering the UI.
  • Not updating the displayed total after attachment — users see one price, get charged another.

Recipe 5: Payment Handling

When to use: Every flow. Payment is the most failure-prone step. Handle it with care.

Standard Stripe Flow

Step 1: Get credentials from prebook (or attach-services)

// From prebook response (or services response if extras were added)
const { transactionId, secretKey, publishableKey } = prebookResponse.data[0];

Step 2: Confirm payment on the client

const stripe = Stripe(publishableKey);

const { error, paymentIntent } = await stripe.confirmCardPayment(secretKey, {
  payment_method: { card: cardElement }
});

if (error) {
  // Handle Stripe-level failure — don't call /bookings
  showError(error.message);
  return;
}

// Only proceed if paymentIntent.status === 'succeeded'

Step 3: Call /bookings only after Stripe confirms

const bookingResponse = await fetch('/flights/bookings', {
  method: 'POST',
  body: JSON.stringify({
    prebookId,
    payment: { method: 'TRANSACTION_ID', transactionId }
  })
});

transactionId Rules

ScenarioWhich transactionId to use
No ancillaries attachedFrom /prebooks response
Ancillaries attachedFrom /prebooks/{id}/services response
Multiple service callsFrom the last /services response

One rule: always use the most recent transactionId. The API does not validate which one you send — using a stale one causes a payment mismatch that's hard to debug.

Retry Logic

The /bookings endpoint is idempotent on prebookId. Safe retry behavior:

async function bookWithRetry(prebookId, transactionId, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      const response = await callBookings({ prebookId, transactionId });

      if (response.status === 201) return response; // Success
      if (response.status === 409) throw new Error('NON_RETRYABLE'); // Don't retry
      if (response.status >= 500) {
        if (attempt === maxAttempts) throw new Error('MAX_RETRIES_EXCEEDED');
        await sleep(attempt * 1000); // Exponential backoff
        continue;
      }
    } catch (err) {
      if (err.message === 'NON_RETRYABLE') throw err;
    }
  }
}

Retryable: 502, 503, 504, network timeouts Not retryable: 400, 401, 403, 404, 409

Handling 409 Conflict

A 409 means the prebook is in an invalid state: already booked, ticketed, or a duplicate attempt. Do not retry.

if (response.status === 409) {
  // Check if booking already exists for this prebookId
  const existing = await getBookingByPrebookId(prebookId);
  if (existing) {
    // Idempotency: show existing confirmation
    return existing;
  }
  // Otherwise: show error, contact support
  showError('Booking conflict. Please contact support.');
}

Credit Line Flow (Non-Stripe)

Only available if your account has an enabled credit line with payment bypass configured.

POST /flights/bookings
{
  "prebookId": "<prebookId>",
  "payment": { "method": "CREDIT" }
}

Skip Stripe entirely. The API handles the charge server-side. If your account isn't configured for this, the request returns a 400 validation error — do not surface this to users.

Pitfalls

  • Calling /bookings before Stripe confirms. The charge won't be captured and the booking may fail or create an unpaid reservation.
  • Not handling Stripe errors before the booking call. A failed payment intent leaves the prebook in a reserved state.
  • Displaying a generic error on 409 without checking for an existing booking. The booking may have succeeded on a previous attempt — always check before showing an error.
  • Using publishableKey: null — this happens when usePaymentSdk is false but no credit line is configured. Validate the key before initializing Stripe.