> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dev.gojinko.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Flight booking

> Discover, price, build, and book a real flight, with side-by-side examples for SDK, CLI, and MCP.

This guide walks you through booking a real flight from start to finish. The same flow works regardless of interface, so each step shows three variants: SDK, CLI, and MCP.

The one-line version of the flow:

```
discovery → flight_search → trip(add_item + travelers) → checkout → user pays → get_trip
```

## Prerequisites

* A Jinko account and an API key (`jnk_...`). [Get one](https://dashboard.gojinko.com/developers/keys).
* For the SDK path: Node.js 20 or later, then `npm install @gojinko/api-client`.
* For the CLI path: `npm install -g @gojinko/cli && jinko auth login --key jnk_...`.
* For the MCP path: any MCP client connected to `https://mcp.builders.gojinko.com/mcp`.

## 1) Discover flights

Start with discovery. It is cached, fast, and broad.

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    import { createJinkoClient } from '@gojinko/api-client'

    const client = await createJinkoClient({ apiKey: process.env.JINKO_API_KEY })

    const { destinations } = await client.findDestination({
      origins: ['JFK'],
      destinations: ['CDG'],          // filter discovery to your target airport
      origin_type: 'airport',
      destination_type: 'airport',
      trip_type: 'roundtrip',
      departure_dates: ['2026-06-15'],
      return_dates: ['2026-06-22'],
    })

    // Each destination comes back with itineraries that carry an offer_token
    // you can re-price with flight_search.
    const candidate = destinations[0].flights[0]
    console.log(candidate.offer_token, candidate.total)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    jinko find-destination \
      --origins JFK --destinations CDG \
      --origin-type airport --destination-type airport \
      --trip-type roundtrip \
      --departure-dates 2026-06-15 --return-dates 2026-06-22 \
      --format json \
      | jq '.destinations[0].flights[0]'
    ```

    Copy the `offer_token` from the top result.
  </Tab>

  <Tab title="MCP">
    Ask the agent:

    > "From JFK, what can I book to Paris (CDG) leaving June 15, 2026 and returning June 22?"

    The agent calls `find_destination` and shows you candidates. Pick one.
  </Tab>
</Tabs>

## 2) Live pricing

Discovery returns cached prices. Before booking, confirm them with `flight_search`.

`flight_search` is a **flat** request: `origin`, `destination`, and `departure_date` are required (`return_date` for round-trips). To re-price a specific candidate from discovery, also pass its `offer_token`, but the route and dates are still required alongside it. The response is an `offers[]` list, and each offer carries a `fares[]` array. **A fare's `trip_item_token` is ready to drop straight into a trip, no assembly required.**

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    const priced = await client.flightSearch({
      origin: 'JFK',
      destination: 'CDG',
      departure_date: '2026-06-15',
      return_date: '2026-06-22',
      trip_type: 'roundtrip',
      offer_token: candidate.offer_token, // optional: re-price the discovered offer
    })

    const offer = priced.offers[0]
    const tripItemToken = offer.fares[0].trip_item_token
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    jinko flight-search \
      --origin JFK --destination CDG \
      --departure-date 2026-06-15 --return-date 2026-06-22 \
      --offer-token offer_abc... \
      --format json | jq -r '.offers[0].fares[0].trip_item_token'
    ```

    The printed `trip_item_token` is ready to use as-is.
  </Tab>

  <Tab title="MCP">
    > "Yes, book that one."

    The agent calls `flight_search` and surfaces the available fares (economy, premium, business). Pick one.
  </Tab>
</Tabs>

If the response status is `flight_unavailable`, the flight sold out between discovery and now, and the `alternatives[]` field has replacements. Loop back to discovery or present them to the user.

## 3) Build the trip

Now you have a live `trip_item_token`. Add it to a trip AND set travelers in one call:

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    const trip = await client.trip({
      add_item: { trip_item_token: tripItemToken },
      upsert_travelers: {
        travelers: [{
          first_name: 'Jane',
          last_name: 'Doe',
          date_of_birth: '1990-01-15',
          gender: 'FEMALE',
          passenger_type: 'ADULT',
        }],
        contact: {
          email: 'jane@example.com',
          phone: '+33612345678',
        },
      },
    })
    const tripId = trip.trip_id
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    jinko trip \
      --trip-item-token "$TRIP_ITEM_TOKEN" \
      --travelers '[{"first_name":"Jane","last_name":"Doe","date_of_birth":"1990-01-15","gender":"FEMALE","passenger_type":"ADULT"}]' \
      --contact '{"email":"jane@example.com","phone":"+33612345678"}' \
      --format json | jq '.trip_id'
    ```
  </Tab>

  <Tab title="MCP">
    The agent collects traveler info (names, dates of birth, passport) from you and calls `trip(add_item + upsert_travelers)`.

    <Warning>
      Never let an agent fabricate traveler data. Names, dates of birth, and passport numbers must match the actual passenger's travel documents. Airlines enforce this.
    </Warning>
  </Tab>
</Tabs>

## 4) Quote and select ancillaries (optional)

If you want to preselect bags, seats, or meals before checkout, list what's available with `getAncillaries` (it surfaces the ancillaries **without** generating a checkout URL), then pre-select. (Make sure travelers are set first; ancillaries are priced per passenger.)

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    // List ancillaries: runs a price quote behind the scenes, no checkout URL.
    let ancillaries = await client.getAncillaries(tripId)

    // The quote may still be pricing; poll until it's ready.
    while (ancillaries.status === 'pricing') {
      await new Promise(r => setTimeout(r, (ancillaries.retry_after_seconds ?? 2) * 1000))
      ancillaries = await client.getAncillaries(tripId)
    }

    // Pick an ancillary offer
    const item = ancillaries.items[0]
    const bagOffer = item.available_ancillaries.find(a => a.type === 'bag')
    await client.selectAncillaries({
      trip_id: tripId,
      item_id: item.item_id,
      selections: [{ offer_id: bagOffer.offer_id }],
    })
    ```

    You don't have to pre-select. Your user can pick on the Stripe checkout page. Skip this step if you want the simplest flow.
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    jinko select-ancillaries --trip-id "$TRIP_ID" --item-id "$ITEM_ID" --select "$BAG_OFFER_ID"
    ```
  </Tab>

  <Tab title="MCP">
    Ancillary selection happens inside the `checkout` widget. The user clicks through the add-ons UI there.
  </Tab>
</Tabs>

## 5) Checkout

Create the Stripe checkout session:

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    const { checkout_url } = await client.checkout(tripId)
    console.log('Open in browser:', checkout_url)
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    jinko checkout --trip-id "$TRIP_ID"
    # → { "checkout_url": "https://app.gojinko.com/checkout?sid=...", ... }
    ```
  </Tab>

  <Tab title="MCP">
    The agent automatically opens the checkout in a browser window (`openLink` via MCP Apps).
  </Tab>
</Tabs>

The `checkout_url` points at `app.gojinko.com/checkout`, a Stripe-hosted page Jinko owns.

<Note>
  On **REST / CLI / SDK**, `checkout` is **synchronous**: it schedules the quote, polls until it's ready, and returns the full checkout envelope in one call: `{ session_id, checkout_url, agent_spt_params, status: "ready", total_amount, items }`. (On **MCP**, the booking widget drives this interactively instead of returning the envelope.) Either way, fulfillment still happens asynchronously *after* the user pays. See steps 7 to 8.
</Note>

<Tip>
  **Paying agentically (no browser)?** `checkout` also returns `agent_spt_params`. Mint a Shared Payment Token scoped to those params, then authorize the booking server-side with `submit_agent_payment` (`jinko agent-pay submit --trip-id "$TRIP_ID" --token "$SPT"` / `client.submitAgentPayment(tripId, spt)`). If the card issuer requires 3DS, the response falls back to a `checkout_url`.
</Tip>

## 6) User pays

Send the user to `checkout_url`. They:

1. Confirm the itinerary.
2. Pick ancillaries (if not pre-selected).
3. Enter payment.
4. Stripe holds the authorization.

## 7) Fulfillment is automatic

Once the user pays, Stripe webhooks trigger fulfillment on the API. No client-side confirm step is needed.

Fulfillment states (`get_trip → fulfillment.status`):

| State        | Meaning                                                   |
| ------------ | --------------------------------------------------------- |
| `pending`    | Payment cleared, booking not yet attempted                |
| `fulfilling` | Calling the airline or provider (takes seconds to hours)  |
| `completed`  | Booking confirmed, `pnr` and `booking_ref` populated      |
| `failed`     | Provider rejected (rare). Refund is issued automatically. |

## 8) Watch the booking land

Poll `get_trip` until `fulfillment.status` is terminal:

<Tabs>
  <Tab title="SDK">
    ```typescript theme={null}
    async function waitForBooking(client, tripId: string) {
      while (true) {
        const trip = await client.getTrip(tripId)
        if (trip.fulfillment?.status === 'completed') {
          console.log('Booked! Ref:', trip.bookings[0].booking_ref)
          return trip
        }
        if (trip.fulfillment?.status === 'failed') {
          throw new Error('Booking failed, refund in progress')
        }
        await new Promise(r => setTimeout(r, 5000))
      }
    }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    while true; do
      status=$(jinko trip-status --trip-id "$TRIP_ID" --format json | jq -r '.fulfillment.status')
      echo "Status: $status"
      [[ "$status" == "completed" || "$status" == "failed" ]] && break
      sleep 5
    done
    ```
  </Tab>

  <Tab title="MCP">
    The agent can poll with `get_trip`, or the user receives a confirmation email directly from Jinko when the booking lands.
  </Tab>
</Tabs>

## What's next?

* **Add a hotel to the trip**: see the [Flight + Hotel guide](/guides/flight-hotel-booking) for one trip with two items.
* **Hotel-only booking**: see the [Hotel booking guide](/guides/hotel-booking).
* **Refund a booking**: [Refund flow](/tools/flight-refund), `refund-check` then `refund-commit` then `refund-status`.
* **Exchange dates**: [Exchange flow](/tools/flight-exchange), a four-step variant of the booking flow.
* **Troubleshooting**: [Errors](/concepts/errors) has the full status-code reference.
