# API update, September 2026

**Travelgenix Contracting API.** What changed, and what you need to do about it.

Everything here is live on `https://contracts.travelify.io/api/v1`. The spec is
now **1.9.0**. Nothing was removed and no endpoint changed shape, so a working
integration keeps working. There is **one** change that needs action, and it is
small.

| # | Change | Version | Action needed |
|---|---|---|---|
| 1 | `ship` is a strict filter, not a loose search | 1.8.3 | **Yes, if you send `ship`** |
| 2 | Cruise names and ship names were corrected in the data | data, 15 Sep | **Yes, if you cache `sailing_id`** |
| 3 | Search a cruise feeder by ship | 1.8.3 | Optional |
| 4 | Every sailing carries its cruise line | 1.8.4 | Optional |
| 5 | Transfer journey direction worked out for you | 1.8.5, **1.8.7** | Optional, removes an error you may be hitting |
| 6 | Transfer breakdown itemises its supplements | 1.8.6 | Optional |
| 7 | Breakdown lines say which `leg` they are | 1.8.8 | Optional, stop parsing labels |
| 8 | The voucher names any supplement that applied | 1.8.8 | **Nothing to do**, it is our document |
| 9 | Flight searches accept IATA city codes | 1.9.0 | **Yes, if you send a code that is both a city and an airport** |

---

## Do these two

### 1. If you send `ship`, check what you meant by it

`ship` used to be a silent alias for `sailing`: a loose "contains" match across
the cruise name, the ship and the voyage reference. **It now matches the ship
field only, and exactly** (case and surrounding spaces ignored, never a
substring).

```
"ship": "Marina"        → matches the ship Marina
"ship": "Marin"         → matches nothing
"ship": "Celtic Dreams" → matches nothing. That is a cruise name
```

**What to do.** If you were using `ship` as a search box, switch that call to
`sailing`, which keeps the loose behaviour and now searches the cruise line too.
If you were using it to mean an actual ship, you are already correct and there is
nothing to do.

### 2. If you cache `sailing_id`, refresh your cache

On 15 September we corrected spelling in the cruise data: eleven cruise names
and one ship name, across 74 departures. For example `Northen City Escape`
became `Northern City Escape`, and the ship `freedom of the seas` became
`Freedom of the Seas`.

A `sailing_id` pins the cruise name, ship, voyage reference and return date. Any
id minted **before 15 September for one of the corrected cruises** no longer
resolves. You get:

```json
{ "error": { "code": "not_found",
  "message": "This sailing is no longer on that coach. Search the sailings again." } }
```

This is deliberate. The alternative is quietly selling a different cruise.

**What to do.** Treat that `404` as "re-run the search", not as a hard error, and
drop any stored `sailing_id` older than 15 September. If you fetch sailings fresh
on each search you are already fine.

Bookings already taken are **not** affected. They hold departure ids, not sailing
ids.

---

## The rest is additive

### 3. Search a cruise feeder by ship

`POST /v1/coaches/availability` takes `ship`. Send it **without a date** and you
get that ship's sailings instead of one day's search: one row per outbound coach
date, in date order, each with the cruise, the day the coach brings them home,
the seats left and the `sailing_id`.

```json
{ "ship": "Freedom of the Seas", "passengers": { "adults": 2 } }
```

Send it **with** a date and it narrows that date instead.

### 4. Every sailing carries its cruise line

`line` now sits beside `ship` on `sailings[]` and on every sailing row.

```json
{ "sailing_id": "sail_…", "line": "Oceania Cruises", "ship": "Insignia",
  "name": "Bountiful British Isles", "reference": "IN2705",
  "return_date": "2027-05-27" }
```

`POST /v1/coaches/availability` takes `line` the same two ways `ship` works: on
its own it lists that line's sailings, with a date it narrows that date. The
alias `cruise_line` is accepted.

**Lead your picker with `line` and `ship`.** That is what a passenger
recognises; many do not know the cruise's marketing name at all. Keep `name`
though: it is the only thing that separates two cruises on the same ship on the
same day, which happens.

> `operator` on a route is the **coach company**, not the cruise line. On the
> live data it reads `Port Cruise Transfers` on every cruise route. Use `line`.

**Naming the line did not change any `sailing_id`.** The line is deliberately
outside the id, so ids you already hold still resolve. Only item 2 above affects
those.

The full walkthrough is at
`https://contracts.travelify.io/contracting-api/cruise-coach-handover.md`.

### 5. Transfer direction worked out from the two ends

If you have been getting this:

```json
{ "error": { "code": "invalid_request",
  "message": "Set direction to arrival or departure when you send a flight time…" } }
```

it should now be gone. On **both** `POST /v1/transfers/availability/search` and
`POST /v1/transfers/{transfer_id}/availability`, naming an airport at one end and
a place at the other settles the direction on its own. (The per-product endpoint
was missed in 1.8.5 and fixed in 1.8.7, so if you tested it and it still failed,
try again.) An
`origin` of `GVA` with a `destination` near Chamonix is an `arrival`, and the
reverse is a `departure`. The response tells you what was used:

```json
{ "data": [...], "direction": "arrival", "direction_source": "inferred_from_endpoints" }
```

An explicit `direction` always wins, so if you already send one nothing changes.

**Two things worth knowing while you are here:**

- **You only ever send ONE direction**, for the outbound leg. The leg home runs
  the other way and is flipped for you. There is no second direction field.
- **It is still refused where it genuinely cannot be worked out**: airport to
  airport, or neither end naming an airport. A flight time cannot become a
  pick-up time until the direction is known, and reading it backwards is
  expensive. On a real Geneva to Chamonix booking, a 09:00 flight home means a
  02:30 pick-up on a supplier who collects 6h30 early, which catches a night
  supplement. Read as an arrival it would look like a 09:00 pick-up and the
  supplement would be missed. Send `pickup_time` instead if you already know the
  pick-up.

### 6. Transfer breakdown itemises its supplements

Each `breakdown` line can now carry `pickup_time`, a `surcharges` array naming
every rule that applied to that leg, and `surcharge_total`.

```json
"breakdown": [
  { "label": "Outbound 2027-03-13: 300.00 EUR per vehicle", "amount": 300,
    "pickup_time": "14:30" },
  { "label": "Return 2027-03-20: 350.00 EUR per vehicle", "amount": 350,
    "pickup_time": "02:30",
    "surcharges": [
      { "name": "Night surcharge", "amount_type": "fixed", "amount": 50,
        "start_time": "19:00", "end_time": "06:00" }
    ],
    "surcharge_total": 50 }
]
```

**Two traps, both easy to hit:**

- **These fields explain the line, they are not another line.** The `amount`
  already includes the supplement. Sum every `amount` and you get the price. Add
  `surcharge_total` on top and you have charged it twice.
- **`amount` inside `surcharges` is the rule. `surcharge_total` is its effect.**
  They differ whenever there is a markup, because a supplement lifts the base
  before markup. A fixed 50 on a net rate marked up 20% reads `amount: 50` and
  `surcharge_total: 60`. Show the customer the 60. On a per-person basis
  `surcharge_total` covers the whole party.

Both keys are **absent** when nothing applied, so an unsurcharged journey is
shaped exactly as it was before.

### 7. Breakdown lines say which leg they are

On a dated return each line now carries **`leg`**, `"outbound"` or `"return"`.
Use it instead of reading the label text. It is absent on a one-way, and on a
return priced without its own return date.

### 8. The voucher names the supplement

Nothing for you to do. The voucher your customer receives now has an **"Included
in your price"** section naming any supplement that applied, what it added and
the pick-up that triggered it:

```
Night surcharge          +€50.00 (return, pickup 02:30)
```

It shows what the traveller actually paid, not the operator's rule, so it
reconciles against the total on the same page. A booking with no supplement, and
any booking taken before this shipped, looks exactly as it did.

### 9. Flight searches accept IATA city codes

`LON` now searches LHR, LGW, STN, LTN, LCY and SEN. `NYC` searches JFK, EWR, LGA
and SWF. Before this a city code matched nothing at all, because contracted
inventory is always loaded against the airport a flight really uses, so no
contract ever carries `LON`.

Origin and destination resolve independently, so `LON` to `NYC` is 24 airport
pairs in one call, and **every result names the real airport**. A traveller is
never told they fly from `LON`.

Cities whose code is not an airport at all now work for the first time: `TCI`
(Tenerife), `YEA` (Edmonton), `MJV` (Murcia), `ANK` (Ankara) and the rest.

**The one thing to check.** Many city codes are also an airport code. `IST` is
Istanbul and Istanbul Airport; `LAX` is Los Angeles and LAX itself. The city
wins, so **`IST` now also returns Sabiha Gokcen (SAW)** and **`LAX` also returns
Ontario (ONT)**. If you meant the airport alone:

```json
{ "origin": "IST", "dest": "LON", "airport_only": true }
```

or `"origin_airport_only"` / `"dest_airport_only"` for one side. It does nothing
to a code that is not a city code, so it is safe to send every time.

`"include_secondary_airports": false` searches a city's main airports only:
Paris without Beauvais, Los Angeles without Ontario.

An unrecognised code still matches nothing. There is no fuzzy or nearest-airport
matching.

---

## What has not changed

- No endpoint was removed, renamed or moved.
- No field was removed or renamed.
- Quote, hold, confirm and cancel are untouched on every product.
- Every `rate_reference` and `quote_id` already issued still prices the same.
- Send none of the new fields and you get exactly what you got before, with the
  single exception of `ship` in item 1.

## Where the docs are

| | |
|---|---|
| Interactive reference | `https://contracts.travelify.io/contracting-api/reference.html` |
| OpenAPI spec (1.9.0) | `https://contracts.travelify.io/contracting-api/travelgenix-contracting-api.openapi.yaml` |
| Written guide, with a changelog | `https://contracts.travelify.io/contracting-api/travelgenix-contracting-api.md` |
| Cruise coach handover | `https://contracts.travelify.io/contracting-api/cruise-coach-handover.md` |
| This update | `https://contracts.travelify.io/contracting-api/api-update-september-2026.md` |

Transfer time-of-day supplements have their own note, `TRANSFER-TIME-SURCHARGES-DEV-NOTE.md`,
which has been brought up to date: the airport pick-up lead is a **per-supplier**
setting, not a fixed three hours.
