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: trueis the correct default for Stripe.- Skip ancillaries entirely —
servicesAttachablein the prebook response can be ignored for now. - On
404at any step, the offer has expired — restart from search.
Pitfalls
- Don't reuse an
offerIdfrom a previous session. They expire. - Don't skip verify. Even in a basic flow, an unverified price creates billing risk.
- Passenger
birthdayanddocumentfields 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-streamShow 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
expirationfield from verify to show a countdown timer. It creates urgency without being dishonest.
Pitfalls
- Forgetting to replace
transactionIdafter 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
offerIdis 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
changesblock entirely and proceeding — the user sees an unexpected charge and disputes it. - Treating any
changesas 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
servicesAttachabledata 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
servicesAttachableconst { 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
seatsRemainingfrom 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 pricePitfalls
- Using the original
transactionIdafter 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
| Scenario | Which transactionId to use |
|---|---|
| No ancillaries attached | From /prebooks response |
| Ancillaries attached | From /prebooks/{id}/services response |
| Multiple service calls | From 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
/bookingsbefore 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
409without 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 whenusePaymentSdkis false but no credit line is configured. Validate the key before initializing Stripe.
Updated about 2 hours ago