error.code:
error object always has the same three keys: code, message, and an optional doc_url, and it never gains a fourth.
Extra keys sit at the top level, as siblings of error, never inside it. Above, status is a sibling of error, not a field within it. Some failures carry one or two of these, and they are the machine-readable half of the remedy: the deadline that was enforced, the state that refused you, the items that moved. A client that reads only error.code is never affected by any of them, now or when a new one is added. The reference table names them per code.
error.code is a closed enum. The list on this page is the complete set: the platform sends no code that is missing from it, and every code on it has a producer. It is published as the code enum on ErrorResponse in the OpenAPI document, and this page is kept in step with that enum.New codes are added in a minor version and never change meaning. See Versioning & deprecation for what that entitles you to.ApiError (or its subclasses AuthError / ValidationError), see SDK error handling.
Error code reference
Duplicate operations are not an error
There is no duplicate-operation code, because nothing on this surface refuses a repeat:- Creating a trip is never idempotent. Two calls to
POST /v1/tripwith notrip_idmake two trips. That is the contract, not a race you lost. Hold thetrip_idfrom the first response. add_itemandupsert_travelersare idempotent by identity. Adding the same offer twice leaves one item; upserting travelers twice leaves one set. You get a 200 and the current state, not a conflict.- Calling
checkouttwice is not an error either. A repeat call on the same trip returns 200 with the samesession_idandcheckout_urlwhile the quote is still valid.
The price moved: what happens
This is the single most commonly misunderstood behaviour in the API, so read it before you write your checkout path. By default,checkout proceeds at the new price. It does not fail, and there is no tolerance threshold. Live pricing is a moment in time, and by the time the customer reaches checkout the airline or hotel may quote something different. Jinko takes the provider’s current price and tells you it changed.
Two fields on the affected items[] entry carry the signal:
price_changed on every checkout response. If you ignore it, you will charge a customer a price they never agreed to.
Opting out: fail_on_price_change
If you would rather be stopped than re-priced, send fail_on_price_change: true on POST /v1/checkout:
items rides alongside the envelope for a client that wants to show what moved; it is absent when the platform named no specific items. The flag defaults to false, so existing integrations keep the proceed-at-the-new-price behaviour.
A 409 here has no side effects. The trip stays quoted and nothing was charged. Show the new price to your customer, then either call
checkout again without the flag to accept it, or re-run live pricing to look for something cheaper.After a quote expires
A quote is good for 300 seconds, readable asquote.expires_at on get_trip and as expires_at on the checkout response. Past that instant the price is stale, and the platform will not spend your customer’s money on it.
POST /v1/checkout again on the same trip: an expired quote is never reused, so this re-quotes and returns a fresh expires_at, a fresh total, and fresh agent_spt_params. Then mint a new payment token against those new params and submit it. Check price_changed on the new response before you do, because a re-quote is exactly when a price moves.
Two clocks are in play here, and they are not the same clock:
A human on the hosted checkout page is handled for you: the page reads the quote deadline, and once it has passed it offers a Refresh price button that re-quotes in place and re-renders the total before the customer can pay. Nobody is charged a stale price, and nobody has to start over. The 4-hour link lifetime is not a field you can read.
Fulfillment has its own terminal state for this,
expired_quote, which means the quote ran out after the customer paid but before the booking was placed. That is a different moment from the 410 above, which happens before any money moves. See the flight booking guide for the full fulfillment table.A trip that refuses to change
A trip accepts writes while it isdraft, quoting, or quoted. In any other state, add_item, remove_item and upsert_travelers are refused before anything is written, and so is POST /v1/checkout.
Two codes tell you which kind of refusal you hit, and both carry the trip’s status beside the envelope:
TRIP_EXPIREDmeans the trip lapsed after 24 hours with no activity.statusisexpired. It is not recoverable: build a new trip from fresh offers.TRIP_STATE_CONFLICTmeans the trip is alive but busy or finished.statusnames which:quotingin flight,fulfillingorfulfilledbecause the customer already paid, orcancelled/failed.
Lapsing is lazy: a read applies it, a write does not. The 24 hours elapsing changes nothing on its own. The lapse is applied on the next read of the trip, and
GET /v1/trip/{id} returns expired on that same call rather than one call later.Until that read happens the trip still sits at its old status, so a write to a trip nobody has read since it lapsed may still succeed. If you are resuming a trip you have not touched in a while, call GET /v1/trip/{id} first and reconcile on what it tells you, rather than relying on the next write to refuse you.Status code reference
Statuses are a coarser signal than codes, but this is the shape of the space:Reporting issues
Every response includes anX-Request-ID header (also visible in SDK logs as X-Request-ID). Capture it and include it in any bug report; we use it to find the exact request server-side.
Common gotchas
”All my prices are 100x too high”
A 15,977. Monetary amounts come back in minor units:{ "value": 15977, "currency": "USD", "decimal_places": 2 }. Divide by 10 ** decimal_places before displaying, see Money & prices.
The mirror symptom, prices 100x too small, is usually a hardcoded / 100 applied to a zero-decimal currency such as JPY or KRW.
”My key says Active in the dashboard but every call is failing”
If the calls come back 402PAYMENT_REQUIRED, the key is fine and the organization is out of credit. Key status and credit are independent: a key stays active while its organization’s balance is empty, because nothing is wrong with the credential.
Top up and the same key resumes immediately. Do not rotate it, and do not retry the 402 in a loop; no number of retries adds credit. See Limits & credits.
”I get 401 on the MCP endpoint with a token that worked yesterday”
If the token came fromjinko auth login, it is a CLI OAuth token (issuer: Jinko Auth user pool). MCP expects either an API key or an MCP-OAuth token (issuer: AuthKit). They look similar but trip the issuer check. Use a jnk_ key for programmatic MCP access.
”Trip says ‘fulfilled’ but I never got a confirmation”
Confirmation emails go via Resend. If the user’s email is correct on the trip’scontact and the booking status is completed, check spam. If fulfillment.status is stuck on processing or confirming for more than about 6 hours, the upstream provider is slow, common on TravelFusion when the airline is laggy.
Note that fulfilling is a value of the trip’s top-level status, not of fulfillment.status. The two fields have different vocabularies. See the flight booking guide for the full fulfillment table.
”My offer token is rejected as expired”
Offer tokens have a short life (about 30 minutes for flights, shorter for some hotel rates). Adding one after that answers 410OFFER_EXPIRED. If the user takes a long time on traveler entry, re-run live pricing (flight_search / hotel_search) before adding to a trip.