Guides
Conventions
The handful of rules that hold everywhere, so you only have to learn them once.
Base URL
There is no single API address. Lydira runs one server per residency region, each with its own database, and your account lives on exactly one of them. A key issued on one cell does not resolve on another: you get
Take the host from the address bar of your Lydira tab. Settings → API keys states it next to the key, which is the one place it is certain to be right. Then pick it at the top of this page and every sample here switches to it. A self-hosted install or a custom domain uses its own; locally that is
Under that host, everything lives beneath
401 unauthorized, which reads like a bad key rather than the wrong address, and that is the failure worth recognising before you spend an afternoon on it.
Take the host from the address bar of your Lydira tab. Settings → API keys states it next to the key, which is the one place it is certain to be right. Then pick it at the top of this page and every sample here switches to it. A self-hosted install or a custom domain uses its own; locally that is
http://localhost:3000.
Under that host, everything lives beneath
/api/v1. Unlike the browser URLs there is no locale and no account number in the path, because the key already says which account you are.https://YOUR-CELL.lydira.com/api/v1/customers| Region | Host | Servers in | Account numbers |
|---|---|---|---|
| Türkiye | tr.lydira.com | TR | 10000000–19999999 |
| Europe / Rest of world | eu.lydira.com | DE | 20000000–29999999 |
| Americas | us.lydira.com | US | 30000000–39999999 |
The account number in your browser URL tells you which cell you are on without asking anyone: the bands never overlap.
Envelopes
A list always puts its rows under
data. A list over records carries page beside them; a bounded list does not, because there is nothing to walk: a trip's days, the account's seats, the report catalogue. A few responses put context alongside the collection, like unread_count on the inbox. Two endpoints have no envelope at all, because the object is the payload: GET /api/v1/me and a report result.{
"data": [ … ],
"page": { "next_cursor": "…", "has_more": true, "limit": 25 }
}Paging
Cursors, not page numbers. A page number drifts the moment a row is written above it; a cursor does not. Re-send the same request with
?cursor= while has_more is true. ?limit= takes 1 to 100 and defaults to 25. Cursors are opaque: do not parse them, do not build them.cursor = None
while True:
params = {"limit": 100, **({"cursor": cursor} if cursor else {})}
page = requests.get(url, headers=headers, params=params).json()
for row in page["data"]:
handle(row)
if not page["page"]["has_more"]:
break
cursor = page["page"]["next_cursor"]Idempotency
Any write may carry an
Idempotency-Key header: your own unique string, up to 255 characters. It is optional, and omitting it changes nothing. Send one and a repeat of the same request returns the first call's response verbatim with Idempotent-Replay: true instead of writing again. Only a success is remembered, so a failed write leaves the key free to retry. Reusing a key for a different body or path answers 422 idempotency_key_reuse; repeating while the first call is still running answers 409 idempotency_conflict with a Retry-After. Keys are scoped to your account and kept for 7 days.Idempotency-Key: 8f14e45f-ea2c-4f33-9a3b-6d0c1b7e5a90Rate limits
Every response to a request carrying a resolvable key reports its budget, refusals included.
X-RateLimit-Reset is the UTC second at which the window rolls and Remaining returns to Limit. The figure is the tightest ceiling standing between you and a 429, which on the ingest writes is their own lower limit of 30 a minute rather than your account's rate. A request with no usable key never reached a budget and carries none of these.X-RateLimit-Limit: 600
X-RateLimit-Remaining: 587
X-RateLimit-Reset: 1773483660
# on a 429, the same three plus:
Retry-After: 18Dates, money, nulls
Timestamps are ISO-8601 and dates are
YYYY-MM-DD. Money is a decimal string, never a float, so nothing rounds on the way through JSON. A null on a money or personal-data field can mean two different things and they are worth telling apart: the value is genuinely absent, or your key's role cannot see it. Where it is the second, the record usually says so, as content_masked does on a message.{
"price_amount": "1250.00",
"currency": "EUR",
"start_date": "2026-03-14",
"updated_at": "2026-03-14T09:30:00Z"
}