Skip to main content
This guide walks you through booking a hotel from start to finish. Hotels skip the separate price-check step you see in flight booking: each rate returned by hotel_search is already a live offer with an htl_* token you can drop straight into a trip. 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) Search hotels

Search live inventory for the destination, dates, and occupancy. Hotel rates returned here are already priced and bookable.
The destination field accepts five shapes (free-text query, city plus country code, lat/lng with optional radius, place ID, or a list of hotel IDs). Use whichever matches what you have.

2) Build the trip

Add the chosen rate to a trip and set travelers in one call. The hotel offer_id (the htl_* token) goes into trip(add_item) exactly the same way a flight trip_item_token does.

3) 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.
The response also carries expires_at, the deadline on the quoted price (5 minutes, the same instant as quote.expires_at on get_trip). That is a different clock from the checkout link itself, whose signed token lasts 4 hours. The link outlives the price deliberately, so a customer who opens the page later still reaches it. See the flight guide for the full envelope. Past expires_at the price is stale and payment is refused, not re-priced: POST /v1/agent_payment/submit answers 410 QUOTE_EXPIRED and creates no payment object, and the hosted page offers the customer a Refresh price button instead of charging them. To recover from an agent, call checkout again on the same trip (this re-quotes) and mint a new token against the new agent_spt_params. Full detail in Errors, after a quote expires.

Rate terms on the quoted item: hotel_terms

Once the trip is quoted, every hotel item on the checkout response and on get_trip carries a hotel_terms object: the cancellation ladder, the tax breakdown, board, and room, as the platform holds them. It is present on hotel items only.
included decides whether a tax is already in the price. included: true means the amount is inside the item’s price; included: false means the guest pays it at the property on top. Adding an included line to the total double-charges, and ignoring a non-included one under-quotes the trip. Read the flag on every line.
Every hotel cancellation deadline you receive is zoned. A deadline with no timezone is omitted, not guessed. free_cancellation_until and cancellation_schedule[].cancel_time are RFC 3339, and every value present carries the supplier’s UTC offset (2026-08-18T10:00:00+02:00), so it is an unambiguous instant you can compare directly against your own clock. This holds on both hotel shapes, the rates in a hotel_search result and the hotel_terms on a quoted item. It says nothing about flight datetimes, which follow their own conventions.When the supplier declared no timezone for the property, the platform drops the value rather than inventing an offset: free_cancellation_until is absent, and a cancellation-schedule step with no timezone is left out of the ladder. Guessing a zone on a cancellation deadline is how a refundable booking silently becomes non-refundable.An absent deadline is not a free-cancellation window. It means the platform could not place the deadline on a clock, not that no deadline exists. Say the terms are unavailable rather than implying there is nothing to miss.
An empty or absent list is not a statement that there is nothing. No taxes_breakdown means no tax is known, not that the stay is untaxed. No cancellation_schedule means no ladder could be published, either because the provider gave none or because its steps carried no timezone (see above), not that cancelling is free. Say “not available from the provider” in your interface rather than “none”.
This is the distinction to build against, because the two answer different questions. The price is re-shopped with the provider when the trip is quoted. The terms are not: they are what the provider published when you searched, carried forward on the item. That is a real window, since a property can change its cancellation policy between your search and your checkout. So: quote price to your customer as final, and present hotel_terms as the rate’s published conditions rather than as a guarantee of what the provider will charge at booking. Branch on terms_as_of rather than assuming. It is the field that will tell you when the platform starts re-confirming terms at quote time, and a client that reads it needs no change on the day that happens.
The canonical name for the tax array is taxes_breakdown, on hotel_terms and everywhere else in the public contract. Some older platform-internal payloads carried the same data as taxes_and_fees; that name is not part of the public contract and you should not read it.

4) User pays

Send the user to checkout_url. They:
  1. Confirm the hotel, dates, and room.
  2. Enter payment.
  3. Stripe holds the authorization.

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

6) 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 confirmation lives in bookings[], and the field is booking_reference, not booking_ref:
pnr is optional and normally absent on hotels. bookings[] lists failed items too, so read provider_status per entry rather than assuming every entry is confirmed. (booking_ref is a real field name elsewhere, 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 flight to the trip: see the Flight + Hotel guide for one trip with two items and one Stripe checkout.
  • Flight-only booking: see the Flight booking guide.
  • Search by location, chain, or amenities: the hotel_search tool reference covers every filter (star rating, hotel type, chain, facilities, geo radius).
  • Lookup a booking after the fact: get_booking finds a booking by reference and last name without needing a login.
  • Troubleshooting: Errors has the full status-code reference.