API Conventions
Conventions that apply to every endpoint — the response envelope, scoping, pagination, filtering, and errors. Read this once and the rest of the reference reads the same way.
Base URLs & environments
| Environment | Base URL | Use it for |
|---|---|---|
| Staging | https://api.staging.lexabit.com | All development, testing, and integration work. |
| Production | https://api.lexabit.com | Production workloads only — applications running against the live environment. |
Code examples throughout these docs show the production host. While you are developing or testing — including building a new integration — substitute api.staging.lexabit.com. The two environments expose the same API but hold entirely separate data: accounts, tenants, connectors, client credentials, and tokens are not shared between them, so staging credentials never work against production (and vice versa).
Each environment publishes its own RFC 8414 discovery document at /.well-known/oauth-authorization-server, so OAuth clients can resolve the correct endpoints from the base URL alone.
The response envelope
Every response — success or error — is a JSON object that always contains a top-level meta.
On success (2xx) the payload is under data:
{
"data": { "id": "…", "name": "Acme AS" },
"meta": { "requestId": "3f1c…" }
}
For a list, data is an array and meta also carries pagination (see below).
On failure (4xx/5xx) the payload is under error:
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"details": { "email": ["The email field is required."] }
},
"meta": { "requestId": "3f1c…" }
}
datais present only on success;erroronly on failure. Both always includemeta.meta.requestIduniquely identifies the request — include it when contacting support.
Whatever language you use, unwrap data on success and error on failure. Don't assume the resource is at the top level — it never is.
Authentication
All endpoints (except login, registration, and password reset) require a bearer token:
Authorization: Bearer <token>
See Authentication for how to obtain one.
Scope — the X-Scope header
Most endpoints operate within a scope — the tenant, team, or other entity you're acting in. Send its scopeId with every scoped request:
X-Scope: <scopeId>
You get the scopeId from GET /scopes/v1, which lists the scopes you can act in. Treat scopeId as an opaque token — send it back exactly as received; don't build or parse it, and note it's not the same as the entity's own id.
If you omit X-Scope on a scoped endpoint, the request fails with 400 and error.code = "bad_request" ("scope not set"). The scope also determines what you can see: list endpoints only return records visible from the current scope. How to list your scopes and how they nest is covered in Concepts → Scopes & Workspaces.
Pagination
List endpoints are paginated. Set the page size with perPage and navigate with page:
GET /clients/v1?perPage=25&page=2
List responses include a pagination block in meta:
"meta": {
"requestId": "3f1c…",
"pagination": {
"total": 137,
"perPage": 25,
"currentPage": 2,
"lastPage": 6,
"links": { "first": "…", "prev": "…", "next": "…", "last": "…" }
}
}
A few high-volume endpoints (company search, account transactions, counterparts) use cursor pagination instead of page numbers: pass perPage and a cursor, and follow the nextCursor value from meta until it's null. The reference marks which style each endpoint uses.
Sorting
sort takes a comma-separated list of fields; prefix a field with - for descending:
GET /clients/v1?sort=-createdAt,name
Each endpoint allowlists which fields can be sorted.
Filtering
Filter with filter[field]=value:
GET /clients/v1?filter[status]=active
Each endpoint allowlists which fields can be filtered.
Idempotency
POST requests can be made safely retryable by sending a unique Idempotency-Key:
Idempotency-Key: 1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed
Retrying with the same key returns the original result instead of creating a duplicate; reusing a key with a different body returns 409. See Error Handling for a retry pattern that uses this.
Errors & status codes
Errors use the error envelope above. error.code is a stable machine-readable string:
| Status | error.code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request (e.g. missing X-Scope) |
| 401 | unauthenticated | Missing or invalid token |
| 403 | forbidden | Authenticated, but you lack the required permission |
| 404 | not_found | The resource doesn't exist or isn't visible in your scope |
| 409 | conflict | State conflict (including idempotency mismatches) |
| 422 | validation_failed | Invalid input — error.details maps each field to its messages |
| 429 | rate_limited | Too many requests |
| 500 | server_error | Unexpected server error |
Always branch on error.code (not the message text, which may change). For 422, read error.details for per-field validation messages. The Error Handling guide shows this in code.
Putting it together
A complete, scoped, authenticated call looks the same in every language — set two headers, then read data. Here we list the first page of clients:
- curl
- Python
- JavaScript
curl "https://api.lexabit.com/clients/v1?perPage=2&sort=-createdAt" \
-H "Authorization: Bearer $LEXABIT_TOKEN" \
-H "X-Scope: 42"
import os, requests
resp = requests.get(
"https://api.lexabit.com/clients/v1",
params={"perPage": 2, "sort": "-createdAt"},
headers={
"Authorization": f"Bearer {os.environ['LEXABIT_TOKEN']}",
"X-Scope": "42",
},
)
resp.raise_for_status()
body = resp.json()
clients = body["data"] # the list
page = body["meta"]["pagination"] # total, currentPage, lastPage, links
const params = new URLSearchParams({ perPage: "2", sort: "-createdAt" });
const resp = await fetch(`https://api.lexabit.com/clients/v1?${params}`, {
headers: {
Authorization: `Bearer ${process.env.LEXABIT_TOKEN}`,
"X-Scope": "42",
},
});
const body = await resp.json();
const clients = body.data; // the list
const page = body.meta.pagination; // total, currentPage, lastPage, links
A successful 200 response:
{
"data": [
{
"id": "9b1d…",
"type": "company",
"displayName": "Acme AS",
"countryCode": "NO",
"registrationNumber": "922020175",
"status": "active",
"createdAt": "2026-05-14T09:12:03Z"
},
{
"id": "7a44…",
"type": "person",
"displayName": "Kari Nordmann",
"countryCode": "NO",
"status": "prospect",
"createdAt": "2026-05-13T16:40:55Z"
}
],
"meta": {
"requestId": "3f1c8e2a-…",
"pagination": {
"total": 137,
"perPage": 2,
"currentPage": 1,
"lastPage": 69,
"links": { "first": "…", "prev": null, "next": "…", "last": "…" }
}
}
}
From here, the whole API is variations on this shape. The Quick Start walks the very first call step by step, and each guide applies these conventions to a real workflow.