Same flights from several sellers (journey key)
What you’ll build: a results list that shows each journey once, as “from ₹7,000 · 3 options”, and opens to show every seller’s option.
Kitionz returns every offer every supplier sends. The same flights often come back several times: from different sellers, at different prices, with different seats, baggage and rules. Kitionz never removes a copy and never picks a seller for you.
Instead, every search offer carries a journey key (journeyKey). Offers with the same journey
key are the same flights.
What the key promises
Two offers with the same journeyKey state the same flights at the same start and end times:
- every flight has the same airline and flight number;
- every flight uses the same airports, with every stop on the way;
- every leg leaves and arrives at the same local time.
Critical: the key means “these sellers state the same flights at the same start and end times”. It does not mean “proven to be the same planes”. Kitionz compares what each seller states. It does not check the flights with the airline, and sellers type flight numbers by hand.
A codeshare sold under another airline’s number has a different key.
Inside one group, each offer still has its own price, seats, cabin, baggage, refund and change
rules, and its own way to book (booking). Times at a stop, terminals and aircraft can also differ.
The fields
All five are always present on a search offer. A key is never null. An offer that cannot be
compared gets a key no other offer has, so ordinary grouping code (Map, groupBy, SQL
GROUP BY) keeps it on its own.
Format: at most 64 characters, letters, digits and _ (today always 36). Compare whole values.
Never parse them.
A one-way offer from search, trimmed to the journey key fields:
Step 1 — Group offers by journey key
Offers come sorted by price, cheapest first. So the first offer you meet for a key is the cheapest.
You need no special case for an offer that cannot be compared. Its key is unique, so it gets a group of its own.
Step 2 — Show each group once
Show what every offer in the group shares: airlines and flight numbers, airports and stops, and each leg’s departure and arrival. Then show the lowest price and the number of options.
Step 3 — Open each option
List every option with its own price, seats, cabin, baggage, refund and change rules, and how it is
booked (booking). Times at a stop, terminals and aircraft can also differ: show them from the
option the customer picks.
Critical: never add up
availableSeatsacross a group. Sellers often resell the same seats, so booking one option can sell out another. Show each option’s own seats.
Step 4 — Book one specific offer
The customer picks one option. Send its offerId to /fare-quote, then its quoteToken
to /book. No endpoint accepts a journeyKey.
Required: if one option fails at
/fare-quote, you may offer the next option in the group. It is a different fare: quote it, and show its price and rules again before booking. Never move a customer to another option without showing it.
The rules
- Never add up seats across a group. Sellers often resell the same seats.
- Always book one specific offer, with its own
offerId. - Matching leg keys never split a package or join two one-ways. Equal
legKeys never let you book a round-trip package’s legs separately, or combine two one-way offers into a round trip. A package has one price for both ways. - A key that cannot be compared never groups. When
journeyKeyComparableisfalse, the key is unique to that offer. Show the offer on its own, and do not store its key. - Treat an unknown gap code as opaque. New codes may be added. Branch only on
legKeyComparable, and never fail on a code you do not know.
Gap codes: why a leg cannot be compared
When legKeyComparable is false, legKeyGaps lists why:
New codes may be added later. A code is never reworded or reused.
Storing a key
You may store a comparable key, for example to follow a journey’s price across searches. It is the same in every search, on every day, for every seller and every tenant, as long as the sellers state the same flights at the same times. It changes when:
- an airline retimes a flight (a different start or end minute);
- Kitionz learns a fact a seller left out. For example, the airline confirms that two pieces under one number are one flight with a stop: the shape changes, so the key may change;
- the recipe version changes (
jk2_, announced in advance). Ajk1_key never equals ajk2_key.
Never store a key with journeyKeyComparable: false. It is unique to that offer in that search.
Key your own inventory (recipe v1)
You can compute the same key for flights from your other sources. Then they group with Kitionz offers. Use only the fields below, exactly as the search returns them.
1. Read each leg
For a leg, use its departureTime, arrivalTime and segments[]. For each segment, use
marketingCarrier.iataCode, marketingCarrier.flightNumber, origin.iataCode,
destination.iataCode, technicalStops[].airport.iataCode and unnamedStops. On the leg’s last segment, also use
arrivalTimeUnconfirmed.
Before comparing, trim every text value and put it in upper case.
The leg is comparable only when all of these hold (otherwise it gets the gap codes above):
- it has at least one segment;
departureTimeandarrivalTimeboth start with a valid local date-timeYYYY-MM-DDTHH:mm;- the last segment’s
arrivalTimeUnconfirmedis nottrue; - every airline code is two letters/digits with at least one letter (IATA), or three letters (ICAO);
- every flight number is 1–4 digits with an optional letter, with no
/ , & +in it; - no segment has
unnamedStopsabove 0, and every technical stop names its airport; - every airport (origin, each technical stop, destination) is three letters.
2. Write the leg string
<DEPARTURE>and<ARRIVAL>: the first 16 characters of the leg’sdepartureTimeandarrivalTime(YYYY-MM-DDTHH:mm). These are local clock times. The time-zone offset after them is ignored, so2026-11-07T00:15:00+05:30,2026-11-07T00:15:00and2026-11-07T00:15:00Zall give2026-11-07T00:15.- Hops: for each segment in order, list its points (origin, each technical stop in order,
destination). Write one hop per pair of points, all under the segment’s own airline and number.
So one flight
6E1607BOM→DPS with a stop at MAA, and two rows6E1607BOM→MAA and MAA→DPS, both give6E1607:BOM-MAA,6E1607:MAA-DPS. <NUMBER>: the digits without leading zeros, then the optional letter (0326→326,1607A→1607A).
Never part of the string: seller or supplier, price, seats, cabin, fare or brand, baggage,
terminals, operating carrier, times at stops inside the leg, time-zone offsets, legType, indexes.
Times at stops are never part of the key. Kitionz decides whether the leg’s arrival can be trusted
from the last flight’s own times, and an untrusted arrival makes the leg not comparable. So two
sellers that state different times at a stop still group, as long as each leg’s first departure and
last arrival agree. A seller whose stop time is empty or unreadable gets ARRIVAL_UNCONFIRMED on
that leg, and its offer stands alone.
A stop time that is null (not stated) is never part of the key, so such an offer groups with a copy
that states every time.
3. Hash
Let H(text) be the first 32 lowercase hex characters of SHA-256 over the UTF-8 bytes of text.
- Leg key:
lk1_+H("kitionz-journey-v1\n" + legString) - Journey key (every leg comparable):
jk1_+H("kitionz-journey-v1\n" + legStrings joined by "\n")
A one-way offer’s journey key therefore carries the same 32 characters as its leg key, under jk1_.
In PHP, H is substr(hash('sha256', $text), 0, 32).
4. A leg or offer that cannot be compared
Kitionz builds a key unique to the offer from the offer’s own offerId (the floffer_…
value you book with), never from the seller:
- leg:
lk1_+H("kitionz-journey-v1-unmatched\n" + offerId + "\nleg:" + n), wherenis the leg’s position (0 for the first); - offer (any leg not comparable):
jk1_+H("kitionz-journey-v1-unmatched\n" + offerId + "\noffer").
For your own rows, give a row that cannot be compared any value no other row has (for example your own row id). It must never equal a comparable key.
Worked example: Mumbai to Bali round trip
Outbound MH 195 BOM→KUL + MH 715 KUL→DPS, leaving 7 Nov 00:15 (Mumbai) and arriving 12:35 (Bali). Return MH 714 DPS→KUL + MH 194 KUL→BOM, leaving 14 Nov 13:30 and arriving 22:05. (The times are illustrative.)
Seller A sells it at ₹52,000 with 1 seat. Seller B sells it at ₹52,300 with 1 seat, typing mh and
0195, with other terminals and other times at KUL. Both give:
So a search shows them as one journey: “from ₹52,000 · 2 options”.
A one-way BOM→DPS offer on the same outbound flights has legKey
lk1_8a9bacde0040d6e9cfcc81fb9648fb4a, equal to the package outbound. Its journeyKey is
jk1_8a9bacde0040d6e9cfcc81fb9648fb4a, which is different from the package’s. It is a different
journey, and you can never book the package’s outbound on its own.
Test vectors
Check your code against every vector in the frozen vectors file. Each vector gives the offer fields, the leg strings and the exact keys. The file names the fields as version 1 did: some are old names from the Renamed fields list, with the same values as their new names.
Download journey-key.v1.vectors.jsonA selection:
Test it
Try grouping in Test. A one-way search on Kitionz Test Air returns one journey from two simulated sellers, plus three offers that cannot be compared: a flight with no number, a stop with no airport, and a one-number stop the airline has not confirmed.
Version
This is version 1 (jk1_ / lk1_). The recipe and the vectors are frozen. A change is a new
version, with its own vectors file and keys jk2_ / lk2_, announced as a contract change.