API reference
The OpenAPI document#
https://api.laver.app/openapi.jsonPublic, no credential needed, generated from the running application — it always matches the deployment that serves it. If a field disagrees with anything written in prose docs, the OpenAPI document is right.
npx openapi-typescript https://api.laver.app/openapi.json -o laver-api.d.tsPin the document, not the URL, in anything you ship: download it, commit it, regenerate deliberately.
Authentication#
Two kinds of bearer token, not interchangeable:
- Session tokens — JWTs the web app holds after sign-in, auto-renewed.
- API keys — begin
laver_, act as their creating person with exactly their access, scoped to the one workspace named at creation.
Service accounts are the exception: an admin can mint a key that belongs to the workspace, given explicit board access, surviving whoever created it.
| Route group | Session token | API key |
|---|---|---|
/workspaces, /boards, /tasks, /wikis, /search |
Yes | Yes |
/sprints, /notifications, /notes, /collab |
Yes | No |
/profile, /admin, /billing, /staff, /auth |
Yes | No |
A key cannot mint another key, and cannot look itself up — read
GET /workspaces/<uuid>/members and match on email instead.
Rate limits#
| Allowance | 900 requests/minute |
| Window | Rolling, one minute |
| Counted per | Authenticated user, individual API key, or client IP |
| Exempt | GET /health, any /events path |
| Over the limit | 429, with retry-after |
Every API key has its own budget — a fleet of agents sharing one key shares its 900/minute; a different key starts a separate budget on the same host.
Pagination#
Most collections return complete, no paging. Two that page:
- Board ticket search (
GET /boards/<uuid>/tasks) —limit1-200 (default 50), keysetcursorover(position, uuid). - Notifications — up to 30 per page,
next_cursor.
Request validation#
Every request body is validated strictly and undeclared fields are rejected, not ignored:
{"statusCode": 400, "code": "FST_ERR_VALIDATION", "error": "Bad Request", "message": "body must NOT have additional properties"}Round-tripping a whole ticket object from a GET into a PATCH will fail —
a response carries fields no request accepts.
Size limits worth knowing before you hit them#
| Thing | Limit |
|---|---|
| Ticket title | 500 characters |
| Comment body | 10,000 characters |
| Column name | 100 characters |
| Labels per ticket | 20 |
| Attachment | 25 MB per file |
| API keys | 10 live per person per workspace |