Skip to main content
Booking a flight is a stateful, financial operation. Every step must be idempotent, every outcome must be handled, and ticketing is always asynchronous. Read this guide fully before integrating.

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.
Your system → Travelbase API → Airline GDS → Confirmation
This means:
  • The API call creates intent — it does not confirm the booking
  • The order enters a pending state immediately
  • The final outcome (ticketed or failed) 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

1

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 422 and waste a round trip.
GET /v1/air/offers/off_0000AEdFkLPQNPPHhwYUJk
Authorization: Bearer <token>
{
    "data": {
    "id": "off_0000AEdFkLPQNPPHhwYUJk",
    "total_amount": "432.50",
    "total_currency": "USD",
    "expires_at": "2025-09-14T12:45:00Z",
    "payment_requirements": {
    "requires_instant_payment": true,
    "price_guarantee_expires_at": "2025-09-14T12:30:00Z"
}
}
}
Check the following before proceeding:
CheckFieldCondition
Offer not expiredexpires_atMust be > now + 60s
Price still guaranteedpayment_requirements.price_guarantee_expires_atMust be in the future
Balance sufficientInternalUser wallet ≥ total_amount
Apply a 60-second buffer when checking expires_at. Clocks drift, and a booking initiated at T-5s before expiry will almost certainly fail by the time it reaches the airline.
2

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.
POST /v1/air/orders
Authorization: Bearer <token>
Idempotency-Key: usr_29fGhq-1726310400000
Content-Type: application/json
{
    "selected_offers": ["off_0000AEdFkLPQNPPHhwYUJk"],
    "passengers": [
{
    "id": "pas_0000AEdFkLPQNPPHhwYUJa",
    "title": "mr",
    "given_name": "James",
    "family_name": "Okafor",
    "gender": "m",
    "born_on": "1990-07-22",
    "email": "james.okafor@example.com",
    "phone_number": "+2348012345678",
    "identity_documents": [
{
    "type": "passport",
    "number": "A12345678",
    "issuing_country_code": "NG",
    "expires_on": "2030-01-01"
}
    ]
}
    ],
    "payments": [
{
    "type": "balance",
    "amount": "432.50",
    "currency": "USD"
}
    ]
}
Response
{
    "data": {
    "id": "ord_0000AEdGhpKLMNOPQrsTUv",
    "booking_reference": "TBXQ7F",
    "status": "pending",
    "created_at": "2025-09-14T12:28:03Z",
    "passengers": [...],
    "slices": [...],
    "total_amount": "432.50",
    "total_currency": "USD"
}
}
The Idempotency-Key must be unique per booking intent — not per API call. Generate it once when the user initiates checkout and reuse it on every retry for that same booking. Using a new key on each retry defeats the purpose entirely.
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.
3

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.
POST /v1/air/orders/ord_0000AEdGhpKLMNOPQrsTUv/actions/ticket
Authorization: Bearer <token>
{
    "data": {
    "id": "ord_0000AEdGhpKLMNOPQrsTUv",
    "status": "pending",
    "ticketing_status": "in_progress"
}
}
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.
4

Handle the webhook outcome

Subscribe to order.ticketed and order.failed on your webhook endpoint. These are the authoritative events that determine what the user sees.order.ticketed
{
    "event": "order.ticketed",
    "data": {
    "id": "ord_0000AEdGhpKLMNOPQrsTUv",
    "booking_reference": "TBXQ7F",
    "status": "confirmed",
    "documents": [
{
    "type": "electronic_ticket",
    "url": "https://cdn.travelbase.io/tickets/ET1234567890.pdf"
}
    ]
}
}
order.failed
{
    "event": "order.failed",
    "data": {
    "id": "ord_0000AEdGhpKLMNOPQrsTUv",
    "status": "failed",
    "failure_reason": "airline_rejected",
    "payment_reversed": true
}
}
Always respond to webhook delivery with HTTP 200 before running your fulfilment logic. If your handler throws, Travelbase will retry — but your logic may have partially executed. Acknowledge first, process asynchronously.

Idempotency in depth

Idempotency is your primary defence against duplicate bookings and double charges during network failures or retries. How to generate idempotency keys
// Combine user ID + booking intent timestamp
// Use the same key for every retry of the same booking
const idempotencyKey = `${userId}-${checkoutSessionId}`;

//  Correct — same intent, same key
fetch("/v1/air/orders", {
method: "POST",
    headers: {
    "Idempotency-Key": idempotencyKey,
        "Authorization": `Bearer ${token}`,
},
body: JSON.stringify(orderPayload),
});
Rules
  • 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

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: true in 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
{
    "event": "order.failed",
    "data": {
    "status": "failed",
    "failure_reason": "airline_rejected",
    "payment_reversed": true
}
}
The offer passed its expires_at or was sold out before your order creation request arrived.
  • Prevention: Validate expires_at with a 60-second buffer before booking
  • Recovery: Redirect the user to re-search. Do not attempt to book the same offer ID.
{
    "errors": [
{
    "code": "offer_no_longer_available",
    "message": "The selected offer has expired or is no longer bookable."
}
    ]
}
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.
The account wallet does not have sufficient funds to cover total_amount.
  • Prevention: Check wallet balance against total_amount before the booking flow
  • Recovery: Surface a top-up prompt and resume from the offer validation step
{
    "errors": [
{
    "code": "insufficient_balance",
    "message": "Your balance of $210.00 is insufficient for this booking of $432.50."
}
    ]
}
Webhook delivery can occasionally be delayed due to airline processing times or transient infrastructure issues.
  • At T+30s: Poll GET /v1/air/orders/:id once 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
Never create a second order while a first is still in_progress. You risk holding payment against two orders simultaneously.

Passenger data requirements

Incomplete or incorrect passenger data is the leading cause of airline rejections. Validate all fields client-side before submitting the order.
FieldRequiredNotes
given_nameMust match travel document exactly — no nicknames
family_nameMust match travel document exactly
born_onISO 8601 date (YYYY-MM-DD)
genderm or f — required by most airlines
emailUsed for itinerary and disruption notifications
phone_numberE.164 format (e.g. +2348012345678)
identity_documents✅ for intl.Passport required for international routes
titleRecommendedmr, ms, mrs, dr — some airlines require it
Names must match the travel document exactly, including spacing and hyphenation. Mismatches can result in denied boarding or costly name-change fees post-ticketing.

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

MistakeWhy it’s dangerousFix
Booking without an Idempotency-KeyNetwork retries create duplicate orders and double chargesAlways include a pre-generated, reused key
Using a new key on each retryDefeats idempotency — each retry creates a new booking intentGenerate once per checkout session
Assuming HTTP 200 = confirmedThe order is pending until the webhook arrivesWait for order.ticketed before showing confirmation
Ignoring order.failedUser thinks booking succeeded; support tickets followHandle failure state explicitly with retry UX
Not validating expires_atOrder creation returns 422, wasting a round trip and frustrating the userCheck expiry with a 60s buffer before submitting
Creating a new order during in_progressDual payment holds, possible duplicate ticketBlock 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.