Appearance
Versioning
The API is versioned by date train. One train is served today:
Version2026-11currentHow a request picks its train
The key's own train. A key stores the train that was current when it was minted and answers on it, so an integration written against 2026-11 keeps getting the shapes it was written against after a later train opens. Today every key is on 2026-11, and a request sends nothing to say which train it wants.
Tightly-Version is the header a request will use to name a train other than the key's, once there is a second train to name. Until then the header is not read: sending it changes nothing, and there is no refusal for an unknown value. It is a declared optional parameter on every operation in the reference all the same, so a generated client offers you the argument and a client built today names its train from the start rather than being retrofitted with one. The changelog entry that opens a second train says how to name it.
The promise
A breaking change never lands on a published train. It opens a new one, and the old train keeps answering.
| A change of this kind | Is it breaking? | What it does |
|---|---|---|
| A new operation | No | Ships to every train. |
| A new optional field on a response | No | Ships to every train. |
| A new optional parameter | No | Ships to every train. |
| A field removed or renamed | Yes | Opens a new train. |
| A field's type changed | Yes | Opens a new train. |
| A refusal tightened | Yes | Opens a new train. |
| A default changed | Yes | Opens a new train. |
This is enforced by CI, not by good intentions: every change to the spec is diffed against the published artefact and a breaking diff to a published operation fails the build. That is also why there is no response-transform layer in the API: the contract is held by the gate rather than by code paths, so nothing accumulates behind it.
What the gate reads is the spec. A response typed field by field is covered by it: rename or remove one of those fields and the build fails. A handful of operations declare their success as a plain object and describe it with an example instead, and inside one of those the gate cannot tell a renamed key from an unchanged one. The three finance reports and the cash gate are in that set. There the changelog entry is the notice rather than the gate, which is the other reason every change carries one. Read the entry, and read fields by name.
What "published" means
An operation is part of the contract once it appears in the published spec. Anything not in that file is not part of the contract, whatever a route happens to answer today.
That distinction matters more than it sounds. Tightly has hundreds of internal routes; the public surface is the enumerated subset in the spec, and the enumeration is the promise. If you build against something you found by watching the app's own network traffic, nothing here applies to it.
Additive changes still get an entry
The changelog carries one entry per change, additive ones included, each naming the operation by the name a reader sees and saying what a caller has to do, which is usually nothing. A reader's question is "what is new", not only "what broke", and a release with no changelog entry cannot move the spec: the gate requires the entry.
Writing a client that survives a train
- Read fields by name and ignore the ones you do not know. New optional fields arrive on every train, and a client that fails on an unexpected key fails on an additive release.
- Branch on
message.code, never on a status or on a sentence. Codes are stable within a train; sentences are written for people and can be improved. - Let the key carry the pin. A key takes its own train, which is exactly the pin most integrations want, and it needs nothing in your code to do it.
Upgrading
When a second train opens, the changelog entry names it and says what changed. To move:
- Read the entry.
- Mint a key after the train opens: a new key takes the current train. Run your suite against it in a staging environment.
- Replace the production key with one on the new train, with the old one stopping on the schedule you choose.
There is no forced migration date today. When one becomes necessary it will be announced in the changelog with the date on it, well ahead of it.
The machine-readable contract
Download OpenAPI 3.1 for 2026-11. It is generated from the code, its info.version is the train, and it is the same file our own drift job checks: download it, run the generator at that commit, and you get an identical file. Use it for codegen, for Postman, or to diff two trains yourself.