Laver docs
Open Laver
Workspaces and access

API keys#

An API key is a bearer token that lets a script, an integration or an agent work in one of your workspaces. You create keys for yourself — there is no way to mint a key that acts as somebody else. An owner or admin can also create a service account: a key that belongs to the workspace rather than to a person, so it keeps working after that person leaves. See Service accounts below.

You need an active membership of the workspace and a signed-in session. Any role can create a key, viewers included.

For what to do with a key once you have one, see the agent and API quickstart.

What a key is, and is not#

A key acts as its creating user, with exactly their access — no more and no less. Every request made with it re-runs the same membership, role and board-access checks your own session would. There is no separate permission model to keep in step.

That means:

  • A key made by a viewer is read-only. It gets the same 403 Your role in this workspace is read-only. you would.
  • A key made by a member reaches the boards that member reaches, and 404s on the rest.
  • If your role changes, your existing keys change with it, immediately. A key carries your current access, never the access you had when you made it.
  • A key is confined to the one workspace you name when you create it. It carries your access within that workspace only — a request against a board, ticket or workspace it was not made for gets a 403, even for a workspace you are an active member of with your own session. GET /workspaces through a key lists only that one workspace, not every workspace you belong to.
  • A key cannot create a new workspace or see invitations addressed to you — neither names a single workspace to check the key against, so both are refused outright rather than scoped.

Keys work on the board, ticket and wiki API — /workspaces, /boards, /tasks, /wikis, /wiki-pages and /search. They are not accepted on /profile, /admin, /billing, /notifications or /auth, all of which take a session token only. In particular:

  • A key cannot mint further keys. Key creation lives under /profile, which refuses keys, so a leaked key can never be used to manufacture more.
  • A key cannot administer the workspace. No inviting, no role changes, no revoking other people's keys, no billing.

Telling a key's work from your own#

Because a key acts as you, nothing it does is presented as something you did.

A comment it posts is by Laver, under the Laver mark rather than your face, and a ticket's history says the same: Laver moved this ticket from To do → Doing, not your name with a footnote. Hover either and the tooltip names the key and the person whose access it used — "Done by Laver through Board sweeper, acting as Ada Lovelace" — so who could have done this is still one hover away. The audit log, which exists to be read that way round, names the key on every row outright.

The two rows that report a deployment or a merge are the exception: a pipeline did those, not Laver, so they name nobody and carry a (via Board sweeper) marker instead.

Name your keys after the job they do, then. "Test" tells a colleague reading a thread in six months nothing; "Nightly triage" answers them.

Create a key#

The secret is shown once, in the response to the call that creates it. Only a hash is stored, so it cannot be shown again. If you lose it, revoke it and make a new one.

bash
curl -X POST https://api.laver.app/profile/api-keys \
  -H "Authorization: Bearer <your-session-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "workspace_uuid": "11111111-2222-4333-8444-555555555555",
        "name": "nightly triage bot"
      }'
json
{
    "api_key": {
        "uuid": "66666666-7777-4888-8999-000000000000",
        "workspace_uuid": "11111111-2222-4333-8444-555555555555",
        "name": "nightly triage bot",
        "token_hint": "laver_Ab3xQ9",
        "last_used_at": null,
        "created_at": "2026-01-01T09:00:00.000Z"
    },
    "token": "laver_..."
}

Copy token straight into your secret store. token_hint is the first few characters, kept so you can tell your keys apart in a list — it is far too little to use.

Give the key a name that says what it is for. When something goes wrong, the name is what tells you which key to revoke.

List your keys#

bash
curl https://api.laver.app/profile/api-keys \
  -H "Authorization: Bearer <your-session-token>"

Returns your live keys across every workspace you are still an active member of, newest first, each with its workspace, name, hint, last_used_at, last_used_ip and expires_at. Secrets are never returned. last_used_at is what makes a key safe to revoke: a key that has never been used, or has not been used in months, is one you can retire without guessing. last_used_ip answers the other half — whether the thing using it is the thing you think it is.

An expired key stays in this list, flagged "is_expired": true, until somebody revokes it. That is deliberate: the list is where you go to find out why an agent stopped, so it is the last place the answer should disappear from.

About that address#

Only one address is kept per key — the most recent — and the next request overwrites it. No history is accumulated, so there is nothing to age out. It is deleted with the key's row, which goes when the workspace does. Revoking a key does not clear it, because the commonest reason to revoke a key is having just seen an address you did not expect.

Expiry#

A key can be given a date after which it stops working. It is optional, and every key created before this existed has no expiry at all.

bash
curl -X POST https://api.laver.app/profile/api-keys \
  -H "Authorization: Bearer <your-session-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "workspace_uuid": "11111111-2222-4333-8444-555555555555",
        "name": "quarterly migration",
        "expires_at": "2026-12-31T23:59:59.000Z"
      }'

A date in the past is refused with a 400 rather than accepted and immediately dead.

After it expires the key is refused at authentication, with its own message rather than the generic one:

json
{
    "error": "That API key expired on 2026-12-31. Create a new one and give it to whatever is using this.",
    "api_key_expired": true,
    "expires_at": "2026-12-31T23:59:59.000Z"
}

The status is 401 and api_key_expired is there so a client can branch on it without matching prose. A revoked, forged or deactivated key still gets the ordinary That API key is not valid or has been revoked., so the two are never confused.

The warning comes first. Once a key is within 14 days of expiring, every successful response it makes carries:

text
x-api-key-expires-at: 2026-12-31T23:59:59.000Z

Log it, alert on it, or rotate on it. An agent never looks at the admin panel, so the response it is already reading is the only place it can be told. The same 14 days is when Admin → API keys starts drawing the row as a warning.

Service accounts#

A personal key stops when its owner does — which is right for your own scripts and wrong for the team's build bot. A service account is a key that belongs to the workspace instead.

Create one from Admin → API keys, or:

bash
curl -X POST https://api.laver.app/admin/workspaces/<workspace-uuid>/api-keys \
  -H "Authorization: Bearer <your-session-token>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "release bot",
        "service_account": true,
        "board_uuids": ["<board-uuid>"]
      }'

Only an owner or admin can create one. It has no person to inherit access from, so you list the boards it may reach; an empty list means it reaches none, not all of them.

What a service account can do:

  • Read and write the boards you gave it, and the tickets, comments and wiki pages on them, exactly as a member would.
  • Keep working when the admin who created it is deactivated, removed from the workspace, or deletes their account entirely.

What it cannot do, ever:

  • Hold a session. There is no password, no login and no reset — the only credential that works is the key it was created with.
  • Mint keys, or reach /profile, /admin, /billing, /staff or /notifications. It is refused there exactly as any other key is.
  • Reach a board you did not give it, or any other workspace.
  • Be promoted. It does not appear on Admin → People and the role dropdown refuses it, because an admin role would silently give it every board in the workspace.

A service account uses a seat, on the same terms as the member it replaces: it can write, so it is billable, and creating one is refused when the plan's seat limit is reached. Revoking its key retires the account and frees the seat in one action — which is also the only way to remove one.

If the workspace stops paying, a service account is affected exactly as every member is: a read_only or past_due workspace refuses its writes with a 402 and keeps serving its reads, and a suspended one refuses everything with a 423. Deleting the workspace deactivates every membership, service accounts included, so its keys stop with everyone else's.

Delete a key#

bash
curl -X DELETE https://api.laver.app/profile/api-keys/66666666-7777-4888-8999-000000000000 \
  -H "Authorization: Bearer <your-session-token>"

Revocation is immediate — the very next request made with that key answers 401 {"error": "That API key is not valid or has been revoked."}. It cannot be undone, and it cannot be reversed by recreating a key with the same name.

You can only delete your own keys this way. An owner or admin can revoke any key in their workspace from Admin → API keys, which also lists every key with the person it belongs to.

Ten keys per workspace#

You may hold ten live keys in a single workspace. The eleventh is refused:

json
{
    "error": "You already have 10 keys in this workspace. Revoke one first."
}

The status is 409. Revoked keys do not count, so retiring one frees the slot immediately. The cap is per workspace, so belonging to three workspaces means up to ten keys in each.

What stops a key working#

Event Personal key Service account
You delete the key Dead immediately Dead immediately, account retired
An owner or admin revokes it Dead immediately Dead immediately, account retired
Its expires_at passes Dead, with its own message Same
The person it belongs to is deactivated All of their keys in that workspace stop working Unaffected — that is the point
The person it belongs to is removed from the workspace Same — all of their keys there stop working Unaffected
That person deletes their account Their keys stop Unaffected
The workspace is deleted Memberships are deactivated, so the keys stop Same — it is a membership too
The workspace stops paying Reads work, writes get a 402 Same
That person's role is changed Key keeps working, with the new role's access Has no person, so nothing changes
That person changes their password or signs out everywhere Key keeps working Key keeps working

The deactivation rule is the important one: switching someone off in Admin → People stops their agents with them, in one action, without hunting for keys. It is the offboarding step that matters most.

The last row is the counterpart, and it surprises people: "log out everywhere" ends sessions. A key is not a session, so it survives. Revoke keys explicitly.

Keep the secret secret#

A key is a password that does not expire and belongs to a person. Treat it like one.

Never put a key into:

  • a support ticket, a bug report or an issue description
  • a comment on a ticket, or a wiki page
  • a log line, a stack trace, an error report or an analytics event
  • a screenshot, a screen recording or a shared terminal session
  • a commit, a config file in version control, or a CI job's plain-text variables
  • a chat message, "just for a second"

Instead: put it in a secret manager or an environment variable, pass it only in the Authorization header, and redact anything beginning laver_ before you paste output anywhere. Support will never ask you for a key — if anyone does, that is the whole of the reason not to send it.

Note that requests made with a key share a rate-limit budget by source address rather than getting their own per-user budget, so several noisy agents behind one address can throttle each other.

If a key leaks#

Assume it is being used. Do this in order:

  1. Revoke it nowDELETE /profile/api-keys/<api-key-uuid>, or Admin → API keys if it belongs to someone else or they are unavailable. Revoking is instant and cheap; there is nothing to weigh up.
  2. Issue a replacement and deploy it to whatever was using the old key. Do this after revoking, not before — a leaked key that is still live while you tidy up is the risk.
  3. Check what it could reach. A personal key holds its owner's role and boards, but only inside the one workspace the key names — that is the blast radius, and you work it out from the owner's membership of that workspace. A service account's is narrower and easier: the boards listed against it on Admin → API keys, and nothing else.
  4. Check last_used_ip. Both key lists show the address the key was last seen from. An address you do not recognise is the clearest evidence you will get that somebody else is holding it.
  5. Check the audit log (Admin → Audit log) around the time of the leak. Key creation and revocation are both recorded, so an unfamiliar api_key.created entry is worth investigating.
  6. If you cannot reach the key's owner, deactivate them in Admin → People. That stops every key they hold in the workspace at once — but not a service account, which has no owner to deactivate. Revoke those on Admin → API keys.
  7. Scrub the place it leaked — edit the comment, rotate the log, delete the screenshot. Do this last: it never substitutes for revoking, because you cannot know who already read it.