API overview
Five services, each with its own HTTP API on its own port. They share a small set of conventions, and every one of them describes itself.
OpenAPI 3.1 is the contract
The specification is not documentation about the API — it is the API's definition, generated from the code that serves it and shipped with every release. When this page and the specification disagree, the specification is right.
Each service also serves its own document at runtime, so a deployed instance can always be asked what it implements rather than what its version was supposed to.
| Service | Specification | Served at | Port |
|---|---|---|---|
| Tokenization | tokenization.json | /v1/openapi.json | 8082 |
| Transaction | transaction.json | /v1/openapi.json | 8081 |
| Indexer | indexer.json | /v1/openapi.json | 8083 |
| Reconciliation | reconciliation.json | /v1/openapi.json | 8100 |
Browse them in the API reference, or download and generate a client in whatever language you work in. The documents are served with CORS open, so tooling can fetch them directly.
Generating a client
We publish the specification rather than a set of hand-written libraries. A generated client is correct by construction; a hand-written one is a second description of the same API that has to be kept in step, and never is.
npx openapi-typescript https://docs.tokenistry.com/openapi/reconciliation.json \ -o reconciliation.d.ts openapi-python-client generate \ --url https://docs.tokenistry.com/openapi/reconciliation.json
These documents are OpenAPI 3.1, which spells a nullable field
as "type": ["string", "null"]. Support for that varies between
generators, and the failure is quiet: optional fields come out untyped rather
than the generator complaining. Check a nullable field in the output before
trusting it. The two generators above read 3.1 natively.
Conventions
Idempotency
Every mutating endpoint accepts an idempotency-key header. Replaying a
request with the same key returns the original result instead of doing the work
again, and reusing a key with a different payload is rejected rather than
silently accepted.
This matters more here than in most APIs: the work behind a request is frequently a chain transaction, where doing it twice costs money and cannot be undone.
curl -s localhost:8082/v1/tokens \
-H 'content-type: application/json' \
-H 'idempotency-key: issue-mpf1-2026-08' \
-d '{ "name": "Meridian Property Fund I", "symbol": "MPF1", "decimals": 0 }'
Cursor pagination
Event listing is cursor-paginated rather than offset-paginated. Offsets shift underneath a reader whenever new blocks arrive, which silently skips or repeats rows; a cursor does not.
Health and licence endpoints
/health and /license sit outside the enforcement path on
every service and keep answering when nothing else does. If the API stops serving,
those two say why — which is the entire reason they are exempt.
Versioning
Two version numbers, doing different jobs.
| Looks like | Changes when | |
|---|---|---|
| API version | /v1/ in the path |
Only for a breaking change. A new major version runs alongside the old one; endpoints are not repurposed under the same prefix. |
| Release version | 1.4.2 on the images |
Every release. All services in a release carry the same number. |
Within /v1/, additions are not breaking: new endpoints, new optional
request fields and new response fields can arrive in any release. Write clients
that ignore response fields they do not recognise.
Five services and five databases share one release number deliberately. Per-service versions buy accuracy about what changed, at the cost of every support conversation starting by collecting five numbers.
Image tags let you choose your own strictness: 1.4.2 never moves,
1.4 follows patches, 1 follows minors. A production
deployment should pin the exact version.
