API reference#
There is no hand-maintained endpoint list here, because a hand-maintained list goes stale. The canonical, machine-readable contract is the OpenAPI document served by the API itself. This page tells you where it is, how to turn it into a client, and the handful of cross-cutting rules — authentication, rate limits, paging, validation — that a schema cannot express on its own.
The OpenAPI document#
https://api.laver.app/openapi.json
It is public, needs no credential, and describes every route, parameter, request body and response the API serves. Fetch it from the host you actually call: it is generated from the running application, so it always matches the deployment that serves it.
curl -sS https://api.laver.app/openapi.json -o laver-openapi.json
If a field or an endpoint disagrees with anything written in these docs, the OpenAPI document is right and the prose is out of date. Please tell us.
Generate a client from it#
Any OpenAPI 3 toolchain works. Point it at the URL or at the file you just downloaded:
# TypeScript types
npx openapi-typescript https://api.laver.app/openapi.json -o laver-api.d.ts
# A full client, any supported language
npx @openapitools/openapi-generator-cli generate \
-i https://api.laver.app/openapi.json \
-g python \
-o ./laver-client
Pin the document, not the URL, in anything you build and ship: download it, commit it, and regenerate deliberately. Generating at build time against a live host makes your build depend on our uptime and lets a deployment change your types without a code review.
For agents that call the API directly rather than through a generated client, the document is also a usable tool schema — the parameter and body definitions translate straight into tool definitions.
Base URL and response conventions#
Every request goes to the API host, https://api.laver.app throughout these
docs, over HTTPS. There is no version prefix in the path; the OpenAPI document
is the contract.
- Request and response bodies are JSON. Send
Content-Type: application/jsonon anything with a body — except attachment uploads, which aremultipart/form-data. - Successful responses are objects with a single named key:
{"task": …},{"tasks": […]},{"boards": […]}. Nothing returns a bare array, so a response can gain a field without breaking your parser. - Timestamps are RFC 3339 in UTC:
"2026-08-01T09:20:00.512Z". Date-only fields such asdue_dateare"YYYY-MM-DD"strings. - Identifiers are uuids. The API never exposes database row ids.
- Exact-numeric columns —
positionon tickets, boards, columns and groups — are returned as decimal strings ("3000.000000") so no precision is lost in transit. Send them back as JSON numbers. - Errors are objects. Errors raised by a handler carry a single human-readable
errorstring; errors raised by schema validation, authentication or the rate limiter carry the framework'sstatusCode,error,codeandmessagefields. See Errors. - Browser-based cross-origin calls are restricted to the Laver web app's own origin. Integrations run server-side.
Authentication#
Every authenticated request carries a bearer token:
Authorization: Bearer <token>
There are two kinds, and they are not interchangeable.
Session tokens are what the web app holds after a person signs in. They are JWTs, they expire, and they are renewed automatically while in use. Every route group accepts one.
API keys begin laver_ and do not expire — they last until revoked, until
their owner is deactivated, or until their owner leaves the workspace the key is
booked against. A key acts as the person who created it and inherits exactly
their access: their role, their board memberships, and any change to either
takes effect on the next request. There is no separate permission model for
keys. See API keys.
The workspace named when a key is created is the only workspace the key can
reach. Within that workspace it acts with its owner's exact access — their
role, their board memberships — but a request against any other workspace,
including ones its owner is an active member of with their own session, gets a
403. The key stops working entirely the moment its owner stops being an
active member of the workspace it is booked against.
Which route groups accept an API key:
| Route group | Session token | laver_... API key |
|---|---|---|
/workspaces |
Yes | Yes |
/boards |
Yes | Yes |
/tasks |
Yes | Yes |
/wikis, /wiki-pages |
Yes | No |
/notifications |
Yes | No |
/profile, /platform |
Yes | No |
/admin |
Yes | No |
/billing |
Yes | No |
/auth |
Yes (where relevant) | No |
/public, /health |
Not required | Not required |
Two consequences:
- A key cannot mint another key. Key management lives on
/profileand/admin, which take session tokens only, so a leaked key cannot bootstrap more credentials. Creating a key is therefore always a human, interactive act. - A key cannot look itself up. There is no endpoint accepting a key that
reports which user it acts as. If you need your own user uuid, read
GET /workspaces/<workspace-uuid>/membersand match on the email address of the account that owns the key.
Sending a laver_ token to a route that does not accept keys fails
authentication with 401, because the route only knows how to verify a session
JWT. That is not a permissions failure and no amount of role granting will fix
it.
Endpoint catalogue#
Grouped by resource. Each group is a plugin with its own authentication and its own rules; the OpenAPI document has the individual routes.
Workspaces — /workspaces#
The tenant boundary. Lists the workspaces you belong to and your role in each; lists a workspace's boards, members and labels; creates workspaces, boards and labels; accepts or declines invitations addressed to you. Accepts API keys. Renaming and deleting a workspace are owner-only.
Boards — /boards#
A board and its furniture: the full board read, the narrow ticket search, the
columns and swimlanes, board ordering, per-user card display preferences, ticket
creation, and the live event stream at /boards/<uuid>/events. Accepts API
keys. Sprint management is also addressed under /boards/<uuid>/sprints, but
belongs to the sprint routes and takes session tokens only.
Tickets — /tasks#
One ticket at a time: read, update, move between columns, move between boards, duplicate, archive. Also its comments, its success-criteria checklist and its attachments. This is where optimistic concurrency lives — see Concurrency and polling. Accepts API keys.
Wiki — /wikis, /wiki-pages#
The workspace's nested documentation: the page tree, full-text search, page creation and editing, duplication, moving, archiving, version history and restore, and page attachments. Session tokens only.
Notifications — /notifications#
Mentions and assignment notices addressed to the calling user, with an unread count, and the calls to mark one or all as read. Session tokens only, and always scoped to the caller — there is no way to read anyone else's.
Admin — /admin#
Workspace administration, restricted to owners and admins: the people list,
role changes and deactivation, invitations, the workspace's API keys (including
revoking someone else's), and the audit log. Session tokens only. A non-admin
gets 403 {"error": "Admin access required."}.
Billing — /billing#
The workspace's plan, subscription state, seat count and renewal date, plus the calls that open a Stripe Checkout session or the Stripe billing portal. Reading the summary is open to any member; changing anything is owner-only. There is also a Stripe webhook endpoint, authenticated by Stripe's signature rather than by a bearer token — it is not for you to call.
Profile and account — /profile, /platform#
Your own account: name and profile image, password and email changes, a full
data export, account deletion, workspace transfer, and your own API keys
(GET, POST, DELETE /profile/api-keys). /platform carries terms-acceptance
state. Session tokens only.
Auth — /auth#
Sign-up, email verification, login, two-factor, password reset, token refresh, logout and log-out-everywhere. The credential-issuing routes are the only unauthenticated ones; the rest need a session.
Public — /public, /health, /#
No credential required. Signed links to the privacy and cookie policies, and a
health probe used by the load balancer. /health is exempt from rate limiting.
Support — /staff#
Reserved for Laver support accounts and refused to everyone else. Listed for completeness; there is nothing here for an integrator.
Rate limits#
| Allowance | 900 requests per minute |
| Window | Rolling, one minute |
| Counted per | Authenticated user for sessions; individual API key; client IP otherwise |
| Exempt | GET /health, and any path ending in /events |
| Over the limit | 429, with retry-after |
Responses carry x-ratelimit-limit, x-ratelimit-remaining and
x-ratelimit-reset; a 429 adds retry-after, in seconds. Read
x-ratelimit-remaining and slow down before you hit the wall rather than after.
The detail that matters for agents: every API key has its own budget. Agents sharing one key share its 900 requests per minute, but a different key starts a separate budget even when it runs on the same host. Invalid or unauthenticated traffic is counted by IP address. Plan a fleet's polling accordingly, and do not run a tight retry loop.
The allowance is an abuse ceiling rather than a fair-use quota. If you are close to it in normal operation, the fix is almost always to poll deltas instead of re-reading boards — see Concurrency and polling.
Pagination and limits#
Most collections in Laver are naturally small and are returned complete: a
workspace's boards, a board's columns and members, a ticket's comments, its
checklist, its attachments, a workspace's labels. There is no paging on those
and no limit parameter to pass.
Two endpoints do page:
Board ticket search — GET /boards/<uuid>/tasks
| Parameter | Rule |
|---|---|
limit |
1–200, default 50 |
cursor |
The previous response's next_cursor, verbatim |
next_cursor |
Present only when the page came back full |
The cursor is a keyset over (position, uuid), so a ticket being added, moved
or archived mid-walk cannot make the walk skip or repeat a row. A malformed or
foreign cursor is 400 {"error": "Invalid cursor."}.
Notifications — GET /notifications
Returns up to 30 notifications per page alongside unread_count, has_more and
next_cursor. Pass next_cursor back as cursor for the following page.
unread_only=1 narrows it to unread.
Size limits worth knowing before you hit them#
| Thing | Limit |
|---|---|
| Ticket title | 500 characters |
| Comment body | 10,000 characters (and at least 1) |
Search query q |
200 characters |
| Column name | 100 characters |
| Board and workspace name | 200 characters |
| Label name | 50 characters |
| Labels per ticket | 20 |
| Assignees per ticket | 50 |
| Attachment | 25 MB per file, allowed types only |
| API keys | 10 live keys per person per workspace |
Plan limits — seats, boards and total storage — are a separate matter and are
enforced with 402. See Errors.
Request validation#
Every declared request body, path parameter and query string is validated
strictly, and undeclared fields are rejected rather than ignored. A typo in
a field name, or a field copied from a response into a request, is a 400 and
the request is not partially applied:
{
"statusCode": 400,
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "body must NOT have additional properties"
}
This is deliberate. Silently dropping a field you meant to set is a far worse failure than refusing the call, particularly for a client that never reads the response body.
Practical consequences:
- Send only the fields the endpoint declares. Round-tripping a whole ticket
object from a
GETinto aPATCHwill fail — a response carries fields no request accepts. - Unknown query parameters fail too, including cache-busting parameters some HTTP clients append by default.
- Rich-text fields (
description_json,body_json,content_json) are validated against the editor's document schema wherever they appear. An unrecognised node type is a400with a message describing the problem, rather than a document that stores fine and then fails to render.