Zenith Public API
Read-only access to a company's financial data: bank transactions, expenses, partners, bank accounts, and yearly report aggregates.
Authentication
Every request needs BOTH:
Authorization: Bearer zth_live_…— an API token created in Settings → API tokens. Shown once at creation; treat it as a secret.X-Company-Key: <company key>— the company key shown next to the token. One token is permanently bound to one company; the header is an explicit guard against cross-company mistakes.
Tokens are secrets for server-to-server use. The API sends no CORS headers — do not call it from browser JavaScript, where the token would be exposed.
Conventions
- Field names are
snake_case; dates are ISO-8601 (YYYY-MM-DDfor calendar dates, RFC 3339 for timestamps). - A field that has no value is an explicit
null— keys are never omitted. - Transaction and expense ids are random strings (partners and bank accounts use integer ids). Always use ids exactly as the API returned them.
- Amounts are flat numeric fields next to a
currencyfield, like a bank statement.*_eurfields carry the EUR equivalent — use those when summing across currencies. - List endpoints return
{ data, next_cursor }. To get the next page, passnext_cursorback as?cursor=exactly as received;nullmeans you have everything. A page can be smaller thanlimiteven when more pages exist — always checknext_cursor.
Rate limiting
60 requests/minute per token (token bucket refilled at 1/sec). Every
response carries X-RateLimit-Limit, X-RateLimit-Remaining, and
X-RateLimit-Reset (unix seconds until fully refilled). A 429 also
carries Retry-After (seconds).
Errors
Every non-2xx response has the same body:
Code
Codes: missing_token, invalid_token, token_revoked, token_expired
(401) · invalid_company_key, invalid_request (400) · not_found (404)
· rate_limited (429) · internal_error (500). Codes are stable contract;
messages are not — branch on the code. request_id also appears on the
X-Request-Id response header; include it when contacting support.
Invalid parameters always fail with 400 invalid_request — filters are
never silently ignored.
Versioning
This is V1. Additive changes (new fields, new endpoints) may happen at any
time — tolerate unknown fields. Breaking changes ship under /api/public/v2
with a documented sunset period for V1.
is_split: true and carries the pieces in children. The pieces
always add up to the transaction amount — so when you sum amounts,
sum only the transactions themselves; the children are a breakdown,
not additional money.id to filter transactions by account.