flight_search in search mode) on sandbox. It never appears in production, and never in cached search: flight calendar, destination discovery and price monitoring do not return it.
Use the sandbox URLs and a sandbox key, as described in Sandbox keys:
https://api.sandbox.gojinko.com for REST, the SDK and the CLI (--env sandbox), and https://mcp.builders.sandbox.gojinko.com/mcp for the Builder MCP. To pay without a browser, follow the sandbox walkthrough.Find the test airline
Jinko Test Air flies both directions of seven city pairs, 14 routes in total. The city codes
NYC, LON and PAR resolve to JFK, LHR and CDG.
Each search on one of these routes returns 10 flights × 4 brands = 40 fares from Jinko Test Air, one-way or round trip. A round trip always pairs an outbound and a return flight of the same scenario (for example ZZ201 out, ZZ202 back; ZZ1001 out, ZZ1002 back).
The 40 fares arrive as 10 offers, one per flight, each with 4 fares.
On a Jinko Test Air segment,
airline and operating_carrier are ZZ and flight_number is the numeric part (101 for ZZ101). To keep only the test airline in the results, send include_carriers: ["ZZ"] and check that include_carriers_widened is not true: when no Jinko Test Air itinerary matches the route, date and other filters you sent (a route it does not fly, today’s date, or a time window, cabin, stop or price filter that excludes all ten flights), the search widens to other carriers and sets include_carriers_widened: true.
Flights and scenarios
The flight number isZZ, then the scenario, then one digit for the city pair, then one digit for the direction. Read it from the right:
Scenarios 1 to 9 give three-digit flight numbers (ZZ101 to ZZ962) and scenario 10 gives four-digit ones (ZZ1001 to ZZ1062). Any other number, for example ZZ1101, is not a Jinko Test Air flight.
So ZZ101 is JFK → LHR in the normal scenario, ZZ102 is LHR → JFK, ZZ111 is CDG → JFK, ZZ362 is LHR → SIN in the sold-out scenario, and ZZ1001 is JFK → LHR in scenario 10.
Arrival is departure plus block time, in local time at the destination.
What you observe in each scenario
Steps not listed behave as in the normal scenario.Brands
Every flight is sold in four brands. In search results each offer is one flight, and itsfares[] are the four brands in this order, cheapest first: ZZ Basic, ZZ Standard, ZZ Flex, ZZ Business. brand_name shows the cabin (Economy or Business), not the brand, so tell the three economy brands apart by position, price, or is_refundable, is_changeable and checked_bag_included:
Every brand includes one 7 kg carry-on bag. Fees and penalties are defined in USD and charged once per booking, not per passenger.
Prices
Base fare and tax per adult, one way, in USD:
For each passenger:
- Base = sum of the legs’ base fares × brand factor × passenger factor.
- Tax = sum of the legs’ taxes. It is the same for every passenger, infants included.
All Jinko Test Air amounts are defined in USD and rounded to the cent. A search in another
currency returns amounts that Jinko converts from USD at its current exchange rate, so they change from day to day. Search with currency: "USD" when you assert exact amounts.
Example. ZZ Standard, JFK → LHR, one adult:
A round trip adds the two legs: ZZ Basic JFK → LHR → JFK for one adult is (420 + 430) + (180 + 160) = 1190.00 USD.
The checkout total equals the airline total: ZZ101 ZZ Standard checks out at 789.00 USD.
Extra bags, seats and meals
Jinko Test Air sells extra bags, seats and meals on every flight and brand. After the trip is quoted, list them withGET /v1/trip/{trip_id}/ancillaries (or read items[].available_ancillaries on the trip). While the trip is still being priced that call answers HTTP 202 with status: "pricing"; poll the same URL after Retry-After until it answers 200. Select with POST /v1/select_ancillaries. Each call replaces the item’s whole selection, so send every bag, seat and meal you want in the same call; a second call with only a seat drops the bag. A bag or meal selection is {offer_id, pax_ref_id, quantity: 1}, with pax_ref_id the traveler’s pax_<N>. A seat selection is one per traveler per flight and also needs seat_number, picked from the offer’s seat_map.seats with available: true, and segment_ref_ids with the offer’s one segment, for example {offer_id: "seat-seg-1", pax_ref_id: "pax_1", segment_ref_ids: ["seg-1"], seat_number: "14F", quantity: 1}. The item’s total_with_ancillaries is the fare plus the selections, and that is the amount charged at checkout.
Each bag and meal can be selected once per traveler. Labels name the journey (“Extra bag 23 kg (outbound)”) so the two bags of a round trip are told apart. Seat maps list only the seats still available, free and paid: some seats are taken, always the same ones for a given flight and date, and taken seats are left out of the map.
After ticketing, each paid extra is issued on its own document, an EMD whose number starts with
999, separate from the ticket. What happens next:
- A selection the airline cannot honour never stops the booking. Examples: a second unit of a bag, a child meal for an adult, a seat for a lap infant, or a seat another traveler already holds. The flight is booked and ticketed; that extra alone is marked failed with the airline’s reason.
- Row 13 seat: the ticket is issued, the seat fails with “airline refused the seat”, and Jinko refunds the seat’s price.
- Oversize bag: the ticket is issued and the bag fails with “airline could not add the oversize bag”.
- Void of the booking (ZZ Standard or ZZ Business within 24 hours): the extras’ EMDs are voided with the tickets and the whole charge comes back, extras included. On the refund preview every EMD document carries
recoverable: trueand the quote carriesancillary_recoverable: true; after commit each EMD readsVOIDED. This is the agency void of a whole order: a void that does not cover every ticket and EMD of the booking is not offered. - Refund of the booking (ZZ Flex, or ZZ Business after 24 hours): only the flight is refunded. The preview marks each EMD
recoverable: falseand the quoteancillary_recoverable: false; the EMDs stayACTIVE, and the refund excludes what was paid for them. - Exchange: the extras move to the new booking with the same EMD numbers. A seat moves only if the same seat is free on the new flight; otherwise it is marked failed. An exchange from an economy brand to ZZ Business is one example.
GET /v1/trip/{trip_id}/ancillaries yet, so you cannot discover them; they are being added.
Refund, void and exchange
Start every servicing flow withget_booking, then follow the refund flow or the exchange flow. Tickets are issued right after payment, so a new booking is inside its 24-hour void window.
Refund and void
What you see on the refund calls:
In the preview,
refund and penalty are objects: read the exact figure from refund.amount_money.value and penalty.amount_money.value, integers in minor units (1002.00 USD is value: 100200 with decimal_places: 2).
A void retires the extras’ EMDs with the tickets, so it returns everything charged. A refund covers the flight fare only: paid bags, seats and meals keep their EMDs and their price is not returned (see “Extra bags, seats and meals” above), so on a refunded booking with extras the refund is smaller than the amount charged.
You cannot ask for a void or a refund: the platform picks the one the table gives. ZZ Business inside 24 hours is voided, not refunded.
Exchange
Exchange shop offers the booked flight number on the new date(s), once per brand. It leaves out the booked brand on the booked dates, because that would change nothing. ZZ Basic and ZZ7xx bookings getsupport_level: "UNSUPPORTED" with the reason in warnings.
get_booking reports the same before you shop: on a ZZ7xx booking can_exchange is false and exchange_support_level is "UNSUPPORTED"; on an exchangeable brand they are true and "AUTO".
Exchange price returns payment_outcome and the amounts. Both fares are the booked prices of all passengers:
All three outcomes can be priced on sandbox. Committing an
EVEN exchange is tested end to end; committing an ADD_COLLECT or REFUND exchange is not covered by this guide yet.
One shop on a new date therefore reaches every outcome. Moving to another date in the same brand is always even. From ZZ Standard or ZZ Flex, a dearer brand means paying more and a cheaper brand means a refund. From ZZ Business, every other brand means a refund.
Examples, JFK → LHR, one adult, USD. The amounts below are shown in dollars; on the wire total_due and total_refund are money objects whose value is in minor units (298.00 is value: 29800).
After commit, the exchange is confirmed at once.
new_ticket_numbers lists new tickets, and airline_locator keeps the same airline reference. Keep using the same booking_ref and item_id. A booking that is not ticketed, already refunded, has a refund in progress or was already exchanged cannot be exchanged.
On a ZZ Basic or ZZ7xx booking, exchange shop returns no offers, so there is nothing to price or commit.
ZZ6xx and ZZ7xx
- ZZ6xx, refund needs manual handling. The preview is the brand’s normal answer, so you can commit as usual. The airline accepts the request but a person has to settle it:
flight_refund_statusstays non-terminal, every ticket staysACTIVE, and no money moves. The test airline never settles it. Use it to test that your integration keeps polling and does not tell the customer the refund failed or completed. On sandbox the operation readsstate: "in_progress"for about 20 minutes after commit, thenstate: "attention_required", waiting for a Jinko operator. - ZZ7xx, exchange not permitted. Every brand, ZZ Business included, refuses exchanges: exchange shop answers
support_level: "UNSUPPORTED"with the reason inwarnings. Refunds and voids follow the brand table.
Recipes
Every recipe searches JFK → LHR for one adult in USD, withinclude_carriers: ["ZZ"], on a date from tomorrow. “Book” means: add the fare’s trip_item_token to a trip with travelers and contact, call checkout, pay (see the sandbox walkthrough), and poll get_trip.
Amounts in the recipes are written in dollars for readability. On the wire every amount is a money object: assert the integer in minor units, from the *_money.value field where the response has one (for example total_amount_money.value 78900 for 789.00 USD), as the Prices and refund sections show.
The asynchronous scenarios (ZZ9xx, ZZ10xx) take about 90 seconds at the airline, plus the interval at which Jinko polls the airline. Allow several minutes before your test gives up.
Normal: book, then refund with a penalty (ZZ101, ZZ Flex)
- Search. Assert the ZZ Flex fare on flight
101costs 1062.00 USD (882.00 + 180.00). - Book. Assert
fulfillment.statusiscompleted. get_booking, thenflight_refund_preview. Assertoperation_kind: "cancel",refund.amount_money.value100200 andpenalty.amount_money.value6000 (USD,decimal_places: 2, so 1002.00 and 60.00).- Commit with the preview’s
refundasacknowledged, then poll status. Assertstate: "succeeded"and every ticketREFUNDED, and keep polling untilmoney.vehicle_stateispaidorpayout_settled: the tickets and the money move independently, and only those two states mean the customer has the money. Sandbox answerspayout_settled.
- Search. Assert the ZZ Standard fare on flight
101costs 789.00 USD. - Book. Assert
fulfillment.statusiscompleted. get_booking, thenflight_refund_preview. Assertoperation_kind: "void",refund.amount_money.value78900 (USD,decimal_places: 2, so 789.00) andpenalty.amount_money.value0.- Commit, then poll status. Assert
state: "succeeded"and every ticketVOIDED, and keep polling untilmoney.vehicle_stateispaidorpayout_settled.
- Search, then price-check the fare. Assert both return 789.00 USD.
- Add to a trip and call
checkout. On the item, assertprice_changed: true,original_price_money.value78900 andprice_money.value86790 (669.90 + 198.00, each 110 % of the search amount). Money fields are objects whosevalueis in minor units (decimal_places: 2for USD). - Assert
price_change.delta_money.valueis 7890 and the checkouttotal_amount_money.valueis 86790. - Pay. Assert
fulfillment.statusiscompleted, charged at 867.90.
- Search, add to a trip, call
checkout. Assert checkout succeeds. - Pay and poll. Assert
fulfillment.statusendsfailedand the customer is not charged.
- Search, add to a trip, call
checkout. Assert checkout succeeds. - Pay and poll. Assert
fulfillment.statusendsfailedand the customer is not charged.
- Book, polling every few seconds.
- Assert
fulfillment.statusstays non-terminal for at least 2 minutes. - Assert it then reaches
completed. Give it up to 5 minutes before failing your test.
- Book.
get_booking, thenflight_refund_preview. Assertoperation_kind: "cancel",refund.amount_money.value100200 andpenalty.amount_money.value6000 (USD,decimal_places: 2, so 1002.00 and 60.00). - Commit with the preview’s
refundasacknowledged. - Poll status. Assert it stays non-terminal and every ticket stays
ACTIVE.
- Book.
get_booking, then exchange shop with a newpreferred_departure_date. - Assert
support_level: "UNSUPPORTED", a reason inwarnings, and no offer you can price. flight_refund_previewstill works: assertoperation_kind: "void".
- Search. Assert the ZZ Standard fare on flight
801costs 789.00 USD. - Add to a trip and call
checkout. Assert HTTP 410 witherror.code: "OFFER_EXPIRED". get_trip. Assertquote.status: "failed",quote.failure_reason: "offer_expired", and nofulfillment.
- Book. For about 90 seconds, assert
fulfillment.statusstays non-terminal and the booking’spnrstarts withzzo_(provisional, not an airline reference). - Assert
fulfillment.statusthen reachescompletedandpnris now a 6-character airline reference. get_booking, thenflight_refund_preview. Assertoperation_kind: "void"andrefund.amount_money.value78900 (789.00 USD).- Commit, then poll status. Assert
state: "succeeded"and every ticketVOIDED, and keep polling untilmoney.vehicle_stateispaidorpayout_settled.
- Search, add to a trip, call
checkout. Assert checkout succeeds. - Pay and poll. Assert
fulfillment.statusstays non-terminal for about 90 seconds. - Assert it then ends
failedwith nofailure_reasonand the customer not charged.pnrstill holds the provisionalzzo_value: assert it is not used as an airline reference.
- Book.
get_booking, then exchange shop with a newpreferred_departure_date. - Price the ZZ Flex offer. Assert
total_dueis{value: 29800, currency: "USD", decimal_places: 2}, which is 298.00:valueis in minor units. - Price the ZZ Basic offer. Assert
total_refundis{value: 16400, currency: "USD", decimal_places: 2}, which is 164.00. Price the ZZ Standard offer. Assert even. - Commit one offer, then poll exchange status. Assert new ticket numbers and the same
airline_locator.
- Search, add the ZZ Standard fare, add travelers, then list ancillaries, polling while the call answers 202
pricing. Assertbag-x1-outat 40.00 USD. - Select it for
pax_1. Asserttotal_with_ancillariesis{amount: 82900, currency: "USD", decimal_places: 2}, which is 829.00 (789.00 + 40.00): on this responseamountis the integer in minor units. - Book. Assert the charge is 829.00 and the ticket is issued.
- Select
bag-ov-out(80.00) forpax_1and book. - Assert the ticket is issued and the bag is marked failed, “airline could not add the oversize bag”.
Limits
- Extras. Bags and seats are selectable in sandbox; meals are not listed yet (see Extra bags, seats and meals). Included baggage follows the brand.
- Live search only. Flight calendar, destination discovery and price monitoring never return Jinko Test Air.
- No same-day departures. Jinko Test Air returns nothing for today (UTC) or earlier. With
include_carriers: ["ZZ"]the search does not fail: it widens to other carriers and setsinclude_carriers_widened: true. Search from tomorrow. - 90-day memory. The test airline forgets a booking’s tickets, refunds and exchanges 90 days after its last change. After that, refunding or exchanging it is refused.
- No reset. Test data is cleared only by that 90-day expiry. Make a new booking for each test run.
