Mental model
Flight booking in Travelbase is best understood as a distributed transaction with an external airline dependency. Unlike a typical REST request, the outcome is not determined by the HTTP response — it’s determined by the airline’s confirmation, which arrives asynchronously.- The API call creates intent — it does not confirm the booking
- The order enters a
pendingstate immediately - The final outcome (
ticketedorfailed) arrives via webhook - Your UI and backend must be designed around this three-state model
Idempotency is mandatory
Every order creation request must carry a unique
Idempotency-Key. Without it, retries during network failures can create duplicate charges.Webhooks are the source of truth
Never assume success from an HTTP
200. Listen to order.ticketed and order.failed webhooks to drive your confirmation flow.Offers are time-sensitive
An offer that was valid 90 seconds ago may already be expired or re-priced. Always validate immediately before booking.
Failures are recoverable
A
failed order means no ticket was issued and no charge was applied. Expose a clean retry path — don’t dead-end the user.End-to-end flow
Quickstart
Validate the offer
Before creating an order, confirm the selected offer is still valid. Calling the booking endpoint with an expired offer will return a Check the following before proceeding:
422 and waste a round trip.| Check | Field | Condition |
|---|---|---|
| Offer not expired | expires_at | Must be > now + 60s |
| Price still guaranteed | payment_requirements.price_guarantee_expires_at | Must be in the future |
| Balance sufficient | Internal | User wallet ≥ total_amount |
Create the order
Submit the booking with passenger details and the selected offer ID. This is the only step where idempotency is enforced — generate a unique key per user intent, not per retry.Response
status: "pending" is the only status you will see in this response. The order has been accepted — not confirmed. Proceed to the ticketing step immediately.Issue the ticket
Trigger ticketing by calling the ticket endpoint with the order ID returned in the previous step. This transitions the order into active processing with the airline.
The ticket endpoint also returns
pending. The airline confirmation happens out-of-band. Display a processing screen to the user and wait for the webhook to drive the next state.Idempotency in depth
Idempotency is your primary defence against duplicate bookings and double charges during network failures or retries. How to generate idempotency keys- Keys must be unique per booking intent — not per HTTP call
- Keys are scoped to your account and expire after 24 hours
- If Travelbase receives a duplicate key, it returns the original response without re-executing
- If the original request is still in flight, Travelbase returns
409 Conflict— back off and retry
Handling failures
order.failed — airline rejection
order.failed — airline rejection
The airline rejected the ticketing request. This is the most common failure mode and is typically caused by seat inventory changes between search and booking.
- Charge: Reversed automatically —
payment_reversed: truein the webhook - User experience: Surface a clear message that the booking didn’t go through, explain that no charge was applied, and offer to re-search the route
- Do not: Auto-retry with the same offer — it has been invalidated
422 — offer expired or no longer available
422 — offer expired or no longer available
The offer passed its
expires_at or was sold out before your order creation request arrived.- Prevention: Validate
expires_atwith a 60-second buffer before booking - Recovery: Redirect the user to re-search. Do not attempt to book the same offer ID.
409 — idempotency conflict
409 — idempotency conflict
A request with the same
Idempotency-Key is currently being processed.- Cause: Usually a client-side retry fired before the original request completed
- Recovery: Back off for 2–3 seconds and retry. Do not generate a new idempotency key.
402 — insufficient balance
402 — insufficient balance
The account wallet does not have sufficient funds to cover
total_amount.- Prevention: Check wallet balance against
total_amountbefore the booking flow - Recovery: Surface a top-up prompt and resume from the offer validation step
No webhook received after 60 seconds
No webhook received after 60 seconds
Webhook delivery can occasionally be delayed due to airline processing times or transient infrastructure issues.
- At T+30s: Poll
GET /v1/air/orders/:idonce to check current status - At T+60s: If still
in_progress, display a “still processing” state to the user - At T+5min: If unresolved, contact support with the
order_id— do not create a new order
Passenger data requirements
Incomplete or incorrect passenger data is the leading cause of airline rejections. Validate all fields client-side before submitting the order.| Field | Required | Notes |
|---|---|---|
given_name | ✅ | Must match travel document exactly — no nicknames |
family_name | ✅ | Must match travel document exactly |
born_on | ✅ | ISO 8601 date (YYYY-MM-DD) |
gender | ✅ | m or f — required by most airlines |
email | ✅ | Used for itinerary and disruption notifications |
phone_number | ✅ | E.164 format (e.g. +2348012345678) |
identity_documents | ✅ for intl. | Passport required for international routes |
title | Recommended | mr, ms, mrs, dr — some airlines require it |
Best practices
Generate idempotency keys early
Create the
Idempotency-Key when the user enters the checkout flow — not at the point of the API call. Reuse it on every retry for the same intent.Validate before you book
Always fetch the offer and check
expires_at, price_guarantee_expires_at, and wallet balance in a single pre-booking validation step.Treat webhooks as authoritative
Drive your confirmation UI and downstream fulfilment (emails, PDFs, CRM updates) from webhooks — not from the HTTP response of the order or ticket endpoint.
Show a processing state
After calling
/actions/ticket, display a loading or “booking in progress” screen. Users who see nothing assume failure and retry — causing duplicate attempts.Expose clean retry paths
A
failed order is recoverable. Re-search the route, pre-fill passenger details, and guide the user into a fresh booking — don’t dead-end them.Log every order ID
Persist
order.id and booking_reference as soon as the first POST /orders response arrives — even before ticketing. You’ll need them for support, audits, and cancellations.Common mistakes
| Mistake | Why it’s dangerous | Fix |
|---|---|---|
Booking without an Idempotency-Key | Network retries create duplicate orders and double charges | Always include a pre-generated, reused key |
| Using a new key on each retry | Defeats idempotency — each retry creates a new booking intent | Generate once per checkout session |
Assuming HTTP 200 = confirmed | The order is pending until the webhook arrives | Wait for order.ticketed before showing confirmation |
Ignoring order.failed | User thinks booking succeeded; support tickets follow | Handle failure state explicitly with retry UX |
Not validating expires_at | Order creation returns 422, wasting a round trip and frustrating the user | Check expiry with a 60s buffer before submitting |
Creating a new order during in_progress | Dual payment holds, possible duplicate ticket | Block re-submission while an order is actively processing |
Next steps
Manage orders
Handle post-booking actions — view itineraries, cancel bookings, and manage seat selections.
Configure webhooks
Set up and verify webhook delivery for
order.ticketed, order.failed, and schedule change events.Handle disruptions
Respond to schedule changes, cancellations, and rebooking requests initiated by airlines.

