API keys
Every request to the REST API is authenticated with an API key. Keys belong to your organization, carry an access level, and act on the API like an owner of the workspace — treat them like passwords.
Creating a key
Only workspace owners can manage keys. In the app, open API keys in the sidebar and create a key with:
- Name — a label that tells you later what the key is for, such as Reporting export.
- Access level — read, write or admin; see the table below.
- Expires in days — optional, 1 to 365; leave it empty and the key never expires.
The full key is shown exactly once, right after creation. Copy it into your secret manager — afterwards only a short prefix remains visible, and a lost key has to be revoked and re-created.
Access levels
A key's level decides which endpoints it may call. A request below the required level is rejected with 403.
read | Query endpoints (GET) — reporting and exports. |
write | Queries plus mutations (POST), except administrative endpoints. |
admin | Everything, including the administrative routers below. |
These routers always require an admin key — even for reads, because they expose organization administration and personnel data such as contracts and absences:
members invitations organization webhooks
apiKeys sso auditLog employees
contracts absences daysOff holidayCalendarsPlatform administration, the feedback relay and onboarding are not part of the public API at all — those routes answer 404 for every key.
Authenticating requests
Send the key with every request, either as a bearer token in the Authorization header or in the x-api-key header:
curl -H "Authorization: Bearer $KEY" \
https://app.example.com/api/v1/customers/listMaking requests
The API lives under /api/v1 on the same origin as the app. Every endpoint is a router and procedure pair; queries are called with GET, mutations with POST.
Query input travels URL-encoded in the input parameter, mutation input as the JSON request body:
# A query with input
curl -H "Authorization: Bearer $KEY" \
--get https://app.example.com/api/v1/timeEntries/list \
--data-urlencode 'input={"from":"2026-01-01","to":"2026-01-31"}'
# A mutation
curl -X POST -H "Authorization: Bearer $KEY" \
-H "content-type: application/json" \
-d '{"name":"Acme Inc."}' \
https://app.example.com/api/v1/customers/createSuccessful responses wrap the result in a data envelope:
{ "data": { "id": "…", "name": "Acme Inc." } }Errors and rate limits
Errors come back as JSON with a message and a machine-readable code — always in English, regardless of any locale settings:
{ "error": "Rate limit exceeded", "code": "TOO_MANY_REQUESTS" }400 | Invalid input — the body or input parameter failed validation. |
401 | Missing, invalid, expired or revoked key. |
403 | The key's level is too low for this endpoint. |
404 | Unknown router or procedure. |
405 | Wrong method — GET for queries, POST for mutations. |
429 | Rate limit exceeded. |
Each key may make 1,000 requests per hour. Beyond that the API answers 429 with a Retry-After header telling you how many seconds to wait.
Key lifecycle
A key acts on behalf of the owner who created it, and audit-log entries for its requests are attributed accordingly.
If that owner leaves the workspace, loses the owner role or is banned, the key stops working immediately with 401 — another owner has to create a replacement.
Revoking a key in the app takes effect immediately.
Next steps
Every endpoint, with schemas and a request playground, lives in the interactive reference: