Laver docs
Open Laver
Agents and API

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#

text
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.

bash
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:

bash
# 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/json on anything with a body — except attachment uploads, which are multipart/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 as due_date are "YYYY-MM-DD" strings.
  • Identifiers are uuids. The API never exposes database row ids.
  • Exact-numeric columns — position on 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 error string; errors raised by schema validation, authentication or the rate limiter carry the framework's statusCode, error, code and message fields. 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:

text
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 /profile and /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>/members and 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 searchGET /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."}.

NotificationsGET /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:

json
{
    "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 GET into a PATCH will 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 a 400 with a message describing the problem, rather than a document that stores fine and then fails to render.