Skip to main content
Sandbox runs against the test systems of real airlines and booking providers. Those systems are often slow, change without notice, and fail for reasons unrelated to your code. A failure path is hard to reach on purpose: you cannot ask a real airline to sell out a seat after payment. Jinko Test Air is a fake airline that exists only in sandbox. It always behaves the same way, and each flight number triggers one documented behaviour, so you can test the normal flow and every failure path on demand. It appears only in live flight search (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 is ZZ, 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 its fares[] 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 with GET /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: true and the quote carries ancillary_recoverable: true; after commit each EMD reads VOIDED. 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: false and the quote ancillary_recoverable: false; the EMDs stay ACTIVE, 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.
Bags and seats are selectable in sandbox now, on the REST API and the Builder MCP. Meals are not listed by GET /v1/trip/{trip_id}/ancillaries yet, so you cannot discover them; they are being added.

Refund, void and exchange

Start every servicing flow with get_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 get support_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:
The change fee is the booked brand’s, and it is waived when you keep the same brand. Prices do not depend on the date. 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_status stays non-terminal, every ticket stays ACTIVE, 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 reads state: "in_progress" for about 20 minutes after commit, then state: "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 in warnings. Refunds and voids follow the brand table.

Recipes

Every recipe searches JFK → LHR for one adult in USD, with include_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)
  1. Search. Assert the ZZ Flex fare on flight 101 costs 1062.00 USD (882.00 + 180.00).
  2. Book. Assert fulfillment.status is completed.
  3. get_booking, then flight_refund_preview. Assert operation_kind: "cancel", refund.amount_money.value 100200 and penalty.amount_money.value 6000 (USD, decimal_places: 2, so 1002.00 and 60.00).
  4. Commit with the preview’s refund as acknowledged, then poll status. Assert state: "succeeded" and every ticket REFUNDED, and keep polling until money.vehicle_state is paid or payout_settled: the tickets and the money move independently, and only those two states mean the customer has the money. Sandbox answers payout_settled.
Void within 24 hours (ZZ101, ZZ Standard)
  1. Search. Assert the ZZ Standard fare on flight 101 costs 789.00 USD.
  2. Book. Assert fulfillment.status is completed.
  3. get_booking, then flight_refund_preview. Assert operation_kind: "void", refund.amount_money.value 78900 (USD, decimal_places: 2, so 789.00) and penalty.amount_money.value 0.
  4. Commit, then poll status. Assert state: "succeeded" and every ticket VOIDED, and keep polling until money.vehicle_state is paid or payout_settled.
Price change (ZZ201, ZZ Standard)
  1. Search, then price-check the fare. Assert both return 789.00 USD.
  2. Add to a trip and call checkout. On the item, assert price_changed: true, original_price_money.value 78900 and price_money.value 86790 (669.90 + 198.00, each 110 % of the search amount). Money fields are objects whose value is in minor units (decimal_places: 2 for USD).
  3. Assert price_change.delta_money.value is 7890 and the checkout total_amount_money.value is 86790.
  4. Pay. Assert fulfillment.status is completed, charged at 867.90.
Sold out after payment (ZZ301, any brand)
  1. Search, add to a trip, call checkout. Assert checkout succeeds.
  2. Pay and poll. Assert fulfillment.status ends failed and the customer is not charged.
Ticketing fails (ZZ401, any brand)
  1. Search, add to a trip, call checkout. Assert checkout succeeds.
  2. Pay and poll. Assert fulfillment.status ends failed and the customer is not charged.
Ticketing delayed (ZZ501, any brand)
  1. Book, polling every few seconds.
  2. Assert fulfillment.status stays non-terminal for at least 2 minutes.
  3. Assert it then reaches completed. Give it up to 5 minutes before failing your test.
Refund needs manual handling (ZZ601, ZZ Flex)
  1. Book. get_booking, then flight_refund_preview. Assert operation_kind: "cancel", refund.amount_money.value 100200 and penalty.amount_money.value 6000 (USD, decimal_places: 2, so 1002.00 and 60.00).
  2. Commit with the preview’s refund as acknowledged.
  3. Poll status. Assert it stays non-terminal and every ticket stays ACTIVE.
Exchange not permitted (ZZ701, ZZ Standard)
  1. Book. get_booking, then exchange shop with a new preferred_departure_date.
  2. Assert support_level: "UNSUPPORTED", a reason in warnings, and no offer you can price.
  3. flight_refund_preview still works: assert operation_kind: "void".
Offer no longer available (ZZ801, ZZ Standard)
  1. Search. Assert the ZZ Standard fare on flight 801 costs 789.00 USD.
  2. Add to a trip and call checkout. Assert HTTP 410 with error.code: "OFFER_EXPIRED".
  3. get_trip. Assert quote.status: "failed", quote.failure_reason: "offer_expired", and no fulfillment.
Asynchronous booking that confirms, then void (ZZ901, ZZ Standard)
  1. Book. For about 90 seconds, assert fulfillment.status stays non-terminal and the booking’s pnr starts with zzo_ (provisional, not an airline reference).
  2. Assert fulfillment.status then reaches completed and pnr is now a 6-character airline reference.
  3. get_booking, then flight_refund_preview. Assert operation_kind: "void" and refund.amount_money.value 78900 (789.00 USD).
  4. Commit, then poll status. Assert state: "succeeded" and every ticket VOIDED, and keep polling until money.vehicle_state is paid or payout_settled.
Asynchronous booking that fails (ZZ1001, ZZ Standard)
  1. Search, add to a trip, call checkout. Assert checkout succeeds.
  2. Pay and poll. Assert fulfillment.status stays non-terminal for about 90 seconds.
  3. Assert it then ends failed with no failure_reason and the customer not charged. pnr still holds the provisional zzo_ value: assert it is not used as an airline reference.
Exchange with a payment outcome (ZZ101, ZZ Standard)
  1. Book. get_booking, then exchange shop with a new preferred_departure_date.
  2. Price the ZZ Flex offer. Assert total_due is {value: 29800, currency: "USD", decimal_places: 2}, which is 298.00: value is in minor units.
  3. Price the ZZ Basic offer. Assert total_refund is {value: 16400, currency: "USD", decimal_places: 2}, which is 164.00. Price the ZZ Standard offer. Assert even.
  4. Commit one offer, then poll exchange status. Assert new ticket numbers and the same airline_locator.
Extra bag (ZZ101, ZZ Standard)
  1. Search, add the ZZ Standard fare, add travelers, then list ancillaries, polling while the call answers 202 pricing. Assert bag-x1-out at 40.00 USD.
  2. Select it for pax_1. Assert total_with_ancillaries is {amount: 82900, currency: "USD", decimal_places: 2}, which is 829.00 (789.00 + 40.00): on this response amount is the integer in minor units.
  3. Book. Assert the charge is 829.00 and the ticket is issued.
Oversize bag fails (ZZ101, ZZ Standard)
  1. Select bag-ov-out (80.00) for pax_1 and book.
  2. 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 sets include_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.