Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

The contract and how it evolves

What contract version 1 guarantees, how one file keeps two languages in step, and which gaps are known and open.

ExplanationLast updated

On this page

Two independent libraries, in two languages, talking to a server that ships on its own schedule. The ordinary outcome is drift: a header the Python side truncates at a different boundary, a cost that renders with one fewer digit, a field one binding never learned about.

This page is how that is avoided, and what it means for you when you upgrade.

The short version: one machine-readable file describes every mechanical surface, a gate binds it to the shipped server, a generator emits both languages’ constants from it, and a shared corpus of executable vectors is run by both bindings on every build.

The version you have

contract_version is 1. Both packages implement it, and the number is not the package version — the packages are at 1.0.0rc1 and 1.0.0-rc.1, and those will move independently.

Ask the SDK rather than trusting a page:

>>> tt.__version__
'1.0.0rc1'

The contract version answers one question: can this binding and this server understand each other’s bytes? It bumps only on a breaking wire change. Adding a response header does not bump it, for reasons the evolution section below makes concrete.

How one file keeps everything in step

One contract file, checked against the server and executed by both bindingsAt the centre sits contract.json, the machine-readable contract. To its left, a drift gate compares it against the shipped server's own constants, so the contract cannot describe header names or caps the server does not use. To its right, a code generator emits per language constant files for the Python and TypeScript bindings, so neither hand-maintains the names. Below, a shared corpus of seventy conformance vectors is executed by both bindings, covering emission, receipt parsing, money rendering, outcomes and feedback. A behaviour that differs between the two languages fails the build. Evolution is additive: a new response header is a new contract row plus regenerated constants plus new vectors, and older bindings already surfaced the value in the receipt's extra mapping.Shipped serverheader names, capsvocabulariesdrift gatecontract.jsoncontract_version 1the one sourcegenerateconstant filesone per language,never hand-editedPythontokentriageTypeScripttokentriage70 conformance vectorsboth bindings execute every oneemission 27receipt 19render 12outcomes 7feedback 5

Follow it left to right, then down. The server’s own constants are compared against the contract file by a gate that runs in the server’s test suite, so the contract cannot describe a header name or byte cap the server does not use. From the contract, a generator emits the constant files for both bindings, which is why neither hand-maintains a list of header names. Both bindings then execute the same 70 vectors, and a behaviour that differs between them fails the build.

The corpus splits like this:

Kind Vectors Pins
emission 27 the header map, byte for byte, including caps and sort order
receipt 19 the parsed receipt, as a canonical nine-key mapping
render 12 the money strings, character for character
outcomes 7 the outcome request body
feedback 5 the feedback request body

Each vector names the requirements it witnesses. The runner asserts that the number of vectors it executed equals the number found on disk, rather than checking against a hard-coded total, so a vector that fails to load is a failure rather than a silent skip.

$ pytest tests/conformance_test.py -q
71 passed in 0.17s

That is the 70 vectors plus the structural assertion.

What the gate cannot check mechanically, and what covers it instead

The drift gate compares the contract against exported server constants. Four refusal header names exist only as unexported string literals in the server, so they cannot be compared that way. They are covered by a source scan plus a behavioural test that drives a genuine refusal and asserts the names that were stamped.

Two more sit outside both: retry-after, which is an IANA-standard name that cannot drift, and two vocabularies whose values live as unexported literals. Those are covered behaviourally only.

That distinction is published rather than smoothed over, because “mechanically enforced” and “covered by a test that drives the real path” are different strengths of guarantee, and a reader deciding how much to trust the contract deserves to know which applies where.

Upgrading

Evolution is additive, and the mechanism is worth understanding because it is what makes upgrades boring.

When the server starts stamping a new response header, an SDK built before that header existed already surfaces its value: unknown x-tt-* headers are captured verbatim into Receipt.extra. Nothing crashes and nothing is lost. A later SDK release adds a contract row, regenerates the constants, and adds vectors, which promotes the value from extra to a typed field.

So there are two upgrade shapes, and neither breaks you:

  • Stay on your SDK version. New server headers arrive in extra. You can read them by name if you need them.
  • Upgrade the SDK. Values that were in extra become typed fields. Code reading them from extra keeps working, since the raw capture is not removed for fields it knows.

The same rule covers new dimensions with no SDK change at all. A new business dimension is a tags={"cost_center": "r7"} argument, because the tag family is open on both sides.

Watch items

Three gaps are known, documented, and open. They are listed here rather than only in the specification, because each one is something you might otherwise spend an afternoon looking for.

RateLimit and RateLimit-Policy are not captured. They appear on some 429 responses. They are not typed fields, and because they are not x-tt-* names they are not routed into extra either, so in version 1 they are unreachable through the receipt. Promotion is an additive contract row if and when the underlying IETF draft settles. Retry-After is typed, as refusal.retry_after_s.

There is no cost response header, and there cannot honestly be one. Pricing runs after the last byte of the body, so nothing can stamp a cost on the forward path. Receipt.cost is reserved for a future server that discloses one, or for a ledger hydrate; the dollar today comes from the ledger fetch.

Success-side x-ratelimit-* headers are not captured either, for the same reason as the first item: only x-tt-* names enter extra in version 1. The corpus pins that behaviour today, so it is a decision rather than an oversight, and broadening the capture rule is an additive change.

One asymmetry between the bindings' own guards

The rule that no source file embeds a currency literal or a price table is enforced by a source-scanning test in the TypeScript binding only. Python’s side of that rule is covered behaviourally, by tests asserting that unpriced costs never render as zero and that no client-side arithmetic happens.

Both bindings satisfy the rule. Only one of them has a gate that would catch a future edit introducing a price table. That is stated here because “checked by a gate” and “checked by behaviour tests” are different, and the difference is not visible from the outside.