Skip to main content
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:

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.

2) Live pricing

Discovery returns cached prices. Before booking, confirm them with flight_search. On REST, the SDK and the CLI, flight_search is a flat request with two mutually exclusive modes. Pick one per request: 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.
offer_token replaces the route, it does not accompany it. Price-check re-prices one offer that already knows its own route, so sending route or filter fields beside offer_token is a contradiction and returns 422, naming the offending field:
The MCP tool surface shapes the same operation differently, as a nested price_check: { offer_token } object. That shape is MCP-only and does not apply to the flat REST request here.
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:
POST /v1/trip and GET /v1/trip/{id} return different item shapes. This divergence is real, long-standing, and not going away, so read trip_id off the create response and read items off the get.Write your item parsing against the GET shape. Treat the create response as a receipt: read trip_id and actions_performed from it, then call get_trip for anything about the items themselves.
An unpriced item now has no price key at all. It used to serialize as price: 0 with currency: "", which reads as “this is free” to anything doing arithmetic. Absence now means not priced yet, and a price appears once the trip is quoted. Check for the key before reading it, and never treat a missing price as zero.

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.)
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.

5) Checkout

Create the Stripe checkout session:
The checkout_url points at app.gojinko.com/checkout, a Stripe-hosted page Jinko owns. The query string carries a signed capability token (?t=<token>).
Never build a checkout URL yourself. Open the exact string the API returned. The older ?sid=<cart_id> form is a fallback that is being removed, and a URL you assemble from a trip id will not authenticate.
On REST / CLI / SDK, checkout is synchronous: it schedules the quote, polls until it’s ready, and returns the full envelope in one call:
On MCP, the booking widget drives this interactively instead of returning the envelope. Either way, fulfillment happens asynchronously after the user pays. See steps 7 to 8.
payment_type tells you how this cart will actually be paid, and is one of intent, checkout or agent. It reflects the live authorization, not what you asked for when you created the trip.

Two clocks, don’t confuse them

expires_at is the price deadline, not the link deadline.The link outlives the price, deliberately: a customer who opens the page 20 minutes later still reaches it. Only expires_at is on the wire; the 4-hour link TTL is not a field you can read.

After expires_at: refused, not re-priced

Once the quote’s expires_at has passed, the platform will not spend your customer’s money on that price. What happens next depends on who is paying. Paying agentically. POST /v1/agent_payment/submit answers 410 QUOTE_EXPIRED and creates no payment object at all. Nothing is authorized, nothing is captured, and the trip and its quote are untouched. The response carries the deadline it enforced:
Recover in two calls: checkout again on the same trip (an expired quote is never reused, so this re-quotes and hands you a fresh expires_at, total, and agent_spt_params), then mint a new Shared Payment Token against those new params and submit that. Check price_changed on the new response before you do, because a re-quote is exactly when a price moves. A human on the checkout page. The page handles it without you. It reads the quote deadline, and once it has passed it offers a Refresh price button that re-quotes in place, re-renders the total with any moved item highlighted, and then lets the customer pay. They never see a stale price and never have to start over.
A late payment is never quietly re-priced. Older versions of this guide said a customer arriving after the deadline was simply re-quoted at whatever the provider then charged. That was not true on any path, and refusing at the money choke point is what makes sure it never becomes true.

If the price moved

Airlines re-price constantly, and a quote is only good for 5 minutes. By default checkout proceeds at the airline’s current price. It does not fail, and there is no tolerance threshold. It tells you instead, on the affected items[] entry:
Check price_changed on every checkout response. Ignoring it means charging a customer a price they never agreed to. original_price is what they saw; price is what they will pay.
Prefer to be stopped instead? Send fail_on_price_change: true in the checkout request body. A moved price then returns 409 price_changed listing the affected items, with nothing charged and the trip still quoted:
Branch on error.code === "PRICE_CHANGED". It is the standard three-key error envelope this API uses everywhere, so your generic handler already understands it, and a price refusal is always a 409, never a 502. fail_on_price_change is an ordinary request-body field, so a generated SDK client exposes it like any other checkout parameter. It defaults to false; leave it out and you keep the proceed-at-the-new-price behaviour. On a 409, show the new price to your customer, then call checkout again without the flag to accept it, or re-run live pricing to look for something cheaper. Full detail in Errors → The price moved.
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.

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):
fulfillment is absent until the customer pays. POST /v1/checkout does not create a fulfillment record. It quotes the cart and hands you a checkout_url. The record appears, at awaiting_payment, when the customer presses Pay on that page. Before then get_trip returns quote but no fulfillment and no bookings, so read trip.fulfillment?.status defensively.Two other values you may see elsewhere and should not confuse with these. pending is an item-level status and never appears as a trip’s fulfillment.status. fulfilling is a value of the trip’s own top-level status field, not of fulfillment.status, despite what older versions of this page said.

8) Watch the booking land

Poll get_trip until fulfillment.status is terminal:
Break on every terminal state, not just completed and failed. A loop that waits for those two alone spins forever on partial, cancelled, expired_quote and exchange_partial_failure. The list above is the complete set.
Each entry in bookings[] looks like this. Note the field is booking_reference, not booking_ref, and pnr is optional even at completed, because not every provider issues one:
Each entry also echoes the handle you submitted (trip_item_token and offer_id on a flight; offer_id and hotel_id on a hotel), so you can reconcile a finished booking against your own search results without a side map. See the identity echo for the caveats. bookings[] lists failed items too, so read provider_status per entry rather than assuming every entry is a confirmed booking. (booking_ref is a real field name elsewhere in the API, on the guest-authenticated post-booking endpoints such as get_booking and hotel_cancel. It is just not the name used inside bookings[].)

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.