API Structure
Request headers, response envelope, pagination, and the query conventions shared by every endpoint
2 min readEvery endpoint follows the same request and response contract. Once you know the envelope, the pagination scheme, and the auth headers, you can call any route without re-reading per-endpoint docs.
Base URL
https://api.example.comReplace the host with your deployment.
Request Headers
| Header | When to send | Value |
|---|---|---|
Content-Type | Any request with a body | application/json |
Cookie | Browser or server acting as a signed-in user | Session cookie (sent automatically) |
X-Api-Key | Personal Access Token (scripts, CI, integrations) | ba_AbCdEf... |
Authorization | Third-party OAuth app on behalf of a user | Bearer eyJhbGciOi... |
The organization id for tenant-scoped routes goes in the URL path (/api/user/organizations/{organizationId}/...), not in a header.
Send one credential, not several. If multiple are present, the session cookie wins, then the PAT, then the OAuth Bearer token. Sending more than one does not combine permissions.
Sending a typical authenticated request
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.example.com/api/user/organizations/${ORG_ID}/projects" \
-H "X-Api-Key: ba_AbCdEf0123456789XyZ..."const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const res = await fetch(
`https://api.example.com/api/user/organizations/${organizationId}/projects`,
{ headers: { 'X-Api-Key': process.env.API_KEY } },
);import os, requests
organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
res = requests.get(
f'https://api.example.com/api/user/organizations/{organization_id}/projects',
headers={'X-Api-Key': os.environ['API_KEY']},
)Request Bodies
Bodies are JSON. Write endpoints reject Content-Type values other than application/json. Request bodies are capped at 10 MiB — anything larger returns 400 Bad Request.
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl -X POST "https://api.example.com/api/user/organizations/${ORG_ID}/projects" \
-H "Content-Type: application/json" \
-H "X-Api-Key: ba_AbCdEf..." \
-d '{
"name": "Acme Studio",
"slug": "acme-studio",
"website": "https://acme.example"
}'const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const res = await fetch(
`https://api.example.com/api/user/organizations/${organizationId}/projects`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.API_KEY,
},
body: JSON.stringify({
name: 'Acme Studio',
slug: 'acme-studio',
website: 'https://acme.example',
}),
},
);organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
res = requests.post(
f'https://api.example.com/api/user/organizations/{organization_id}/projects',
headers={'X-Api-Key': os.environ['API_KEY']},
json={
'name': 'Acme Studio',
'slug': 'acme-studio',
'website': 'https://acme.example',
},
)Response Envelope
Every JSON response — success or error — uses the same envelope.
Success
{
"success": true,
"status": 201,
"code": "OK",
"message": "Project created successfully",
"data": {
"id": "01HZ3KA7V0J9Z8Q2G5B1T7XWDN",
"name": "Acme Studio",
"slug": "acme-studio",
"website": "https://acme.example",
"status": "active",
"createdAt": "2026-04-05T10:00:00.000Z",
"updatedAt": "2026-04-05T10:00:00.000Z"
}
}Error
{
"success": false,
"status": 400,
"code": "VALIDATION_ERROR",
"message": "Validation failed for fields: website, slug",
"meta": {
"errors": [
{ "field": "website", "error": "Invalid url", "location": "body" },
{ "field": "slug", "error": "The slug must be unique", "location": "body" }
]
}
}Each entry in meta.errors has field (the offending field path), error (the message), and location (body, query, or params).
Fields
| Field | Type | Description |
|---|---|---|
success | boolean | true when status < 400, false otherwise |
status | number | HTTP status code, mirrored in the body so clients only parse once |
code | string | Machine-readable code — OK on success, see Error Handling |
message | string | Human-readable summary |
data | object | array | Resource, list, or operation result on success |
meta | object | Pagination, validation error details, or other structured extras |
Pagination
List endpoints use offset-based pagination. The query parameters are offset and limit; the response meta returns total, offset, limit, and hasMore.
| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
offset | number | 0 | — | Number of items to skip |
limit | number | 20 | 100 | Number of items to return |
Some endpoints add their own filters on top — a q search query, a status filter, etc. See the API Reference for endpoint-specific parameters.
ORG_ID="01HZ3K5R4X9Y2V6QF8TJ7W0CDN"
curl "https://api.example.com/api/user/organizations/${ORG_ID}/projects?offset=40&limit=20" \
-H "X-Api-Key: ba_AbCdEf..."const organizationId = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN';
const res = await fetch(
`https://api.example.com/api/user/organizations/${organizationId}/projects?offset=40&limit=20`,
{ headers: { 'X-Api-Key': process.env.API_KEY } },
);
const body = await res.json();
console.log(body.meta.total, body.meta.hasMore);organization_id = '01HZ3K5R4X9Y2V6QF8TJ7W0CDN'
res = requests.get(
f'https://api.example.com/api/user/organizations/{organization_id}/projects',
params={'offset': 40, 'limit': 20},
headers={'X-Api-Key': os.environ['API_KEY']},
)
body = res.json()
print(body['meta']['total'], body['meta']['hasMore'])Paginated response
{
"success": true,
"status": 200,
"code": "OK",
"message": "Projects fetched successfully",
"data": [
{
"id": "01HZ3KA7V0J9Z8Q2G5B1T7XWDN",
"name": "Acme Studio",
"slug": "acme-studio",
"status": "active"
}
],
"meta": {
"total": 156,
"offset": 40,
"limit": 20,
"hasMore": true
}
}Do not use page/pageSize. The API only accepts offset/limit. A request with page
will silently ignore it and return the first page.
HTTP Status Codes
2xx Success — 200 OK for reads and updates, 201 Created for new resources, 204 No Content for deletes that return no body.
4xx Client Errors — 400 validation/bad JSON, 401 missing or invalid credential, 403
valid credential but insufficient scope or wrong organization, 404 resource not found, 409
conflict (duplicate slug, etc.), 429 rate limited.
5xx Server Errors — 500 unexpected failure, 503 dependency unavailable. Both are logged
with a request id you can quote when filing a bug.
Dates and IDs
- Dates — ISO 8601 with UTC (
2026-04-05T10:00:00.000Z). Parse withnew Date(...)in JS ordatetime.fromisoformat(...)in Python. - IDs — UUID v7 strings. They are time-sortable, so ordering by
iddescending roughly matchescreatedAtdescending.
Field Naming
camelCasefor every field.- Booleans use
is,has, orneedsprefixes (isActive,isVerified,hasMore). - Date fields end in
At(createdAt,updatedAt,expiresAt,lastLoginAt). - List endpoints return arrays in
dataand pagination inmeta. Single-resource endpoints return an object indataand omitmetaunless there is something to report.
A minimal API client
class ApiClient {
constructor({ apiKey, organizationId, baseUrl = 'https://api.example.com' }) {
this.apiKey = apiKey;
this.organizationId = organizationId;
this.baseUrl = baseUrl;
}
async request(path, { method = 'GET', body, searchParams } = {}) {
const url = new URL(this.baseUrl + path);
if (searchParams) {
for (const [k, v] of Object.entries(searchParams)) url.searchParams.set(k, v);
}
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': this.apiKey,
},
body: body ? JSON.stringify(body) : undefined,
});
const envelope = await res.json();
if (!envelope.success) {
const err = new Error(envelope.message);
err.code = envelope.code;
err.status = envelope.status;
err.meta = envelope.meta;
throw err;
}
return envelope;
}
listProjects({ offset = 0, limit = 20 } = {}) {
return this.request(
`/api/user/organizations/${this.organizationId}/projects`,
{ searchParams: { offset, limit } },
);
}
}import os, requests
BASE_URL = 'https://api.example.com'
def client():
s = requests.Session()
s.headers.update({'X-Api-Key': os.environ['API_KEY']})
return s
def call(session, method, path, **kwargs):
res = session.request(method, BASE_URL + path, **kwargs)
envelope = res.json()
if not envelope.get('success'):
raise RuntimeError(f"{envelope.get('code')}: {envelope.get('message')}")
return envelope
def list_projects(session, organization_id, offset=0, limit=20):
return call(
session,
'GET',
f'/api/user/organizations/{organization_id}/projects',
params={'offset': offset, 'limit': limit},
)Best Practices
- Check
envelope.success(orcode === 'OK') before readingdata: The HTTP status is also correct, but checking the body is cheaper than re-parsing the status. - Use
hasMore, not arithmetic: Trust the server'shasMoreflag instead of comparingoffset + limitagainsttotal;totalcan shift between paginated requests. - Build URLs from the active organization id: Keep the organization id in one place (e.g., a client wrapper that prepends
/api/user/organizations/{organizationId}/...) so callers cannot forget to scope their requests. - Parse dates as timezone-aware: ISO 8601 with
Zis UTC — do not truncate the trailingZ. - Surface the
codefield in logs: It is stable across versions; the human message is not. - Retry only idempotent methods: Safe to retry on
GET,PUT,DELETE; be careful withPOSTunless the endpoint documents idempotency.
Next Steps
- Handle failures: Read Error Handling for the full code list.
- Plan for limits: See Rate Limits before batching calls.
- Lock down scopes: Pick the right scopes in Scopes & Permissions.
- Configure SSO (optional): Set up Enterprise SSO for domain-based authentication.
- Browse endpoints: Jump to the API Reference and start integrating.
Related Pages
Introduction
How the API is structured, how authentication works, and how multi-tenant requests are scoped
Quick Start
Sign in, issue a Personal Access Token, and make your first authenticated call
Error Handling
Error envelope, full code list, and the retry strategies that actually work
Rate Limits
Per-IP and per-endpoint request limits, the headers that expose them, and how to retry safely