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) → book → user pays → get_trip
Prerequisites
- A Jinko account and an API key (
jnk_...). Get one.
- 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.
import { createJinkoClient } from '@gojinko/api-client'
const client = await createJinkoClient({ apiKey: process.env.JINKO_API_KEY })
const result = await client.findFlight({
origin: 'JFK',
destination: 'CDG',
trip_type: 'roundtrip',
departure_dates: ['2026-06-15'],
return_dates: ['2026-06-22'],
})
const candidate = result.results[0]
console.log(candidate.offer_token, candidate.price)
jinko flight-calendar \
--origins JFK --destinations CDG \
--month 2026-06 \
--format json \
| jq '.results[0]'
Copy the offer_token from the top result.Ask the agent:
“Find me a roundtrip from JFK to CDG leaving June 15, 2026, returning June 22.”
The agent calls flight_calendar and shows you candidates. Pick one.
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.
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
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.
“Yes, book that one.”
The agent calls flight_search and surfaces the available fares (economy, premium, business). Pick one.
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:
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
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'
The agent collects traveler info (names, dates of birth, passport) from you and calls trip(add_item + upsert_travelers).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.
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.)
// 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.jinko select-ancillaries --trip-id "$TRIP_ID" --item-id "$ITEM_ID" --select "$BAG_OFFER_ID"
Ancillary selection happens inside the book widget. The user clicks through the add-ons UI there.
5) Checkout
Create the Stripe checkout session:
const { checkout_url } = await client.book(tripId)
console.log('Open in browser:', checkout_url)
jinko book --trip-id "$TRIP_ID"
# → { "checkout_url": "https://app.gojinko.com/checkout?sid=...", ... }
The agent automatically opens the checkout in a browser window (openLink via MCP Apps).
The checkout_url points at app.gojinko.com/checkout, a Stripe-hosted page Jinko owns.
On REST / CLI / SDK, book is synchronous: it schedules the quote, polls until it’s ready, and returns the full checkout envelope in one call — { session_id, checkout_url, 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–8.
6) User pays
Send the user to checkout_url. They:
- Confirm the itinerary.
- Pick ancillaries (if not pre-selected).
- Enter payment.
- 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:
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))
}
}
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
The agent can poll with get_trip, or the user receives a confirmation email directly from Jinko when the booking lands.
What’s next?
- Add a hotel to the trip: see the Flight + Hotel guide for one trip with two items.
- Hotel-only booking: see the Hotel booking guide.
- Refund a booking: Refund flow,
refund-check then refund-commit then refund-status.
- Exchange dates: Exchange flow, a four-step variant of the booking flow.
- Troubleshooting: Errors has the full status-code reference.