Skip to content

Authentication

One header authenticates every call: Authorization: Bearer <credential>. For anything automated, that credential should be a personal access token.

Mint a personal access token

  1. Open dashboard.lithora.app/settings and select the API Tokens tab.
  2. Click Generate new token.
  3. Give it a name that identifies where it will live — ci-release-bot, laptop-cli. You will read this name months from now when deciding what is safe to revoke.
  4. Tick only the scopes the integration actually needs, and set an expiry. The default is 90 days.
  5. Copy the secret immediately.It is shown once and never again — only a SHA-256 hash is stored server-side, so there is no “show token” button and support cannot recover it for you.

A token is a password with your name on it

A PAT acts as you, including on routes where scope enforcement has not yet been wired (see below). Keep it in a secrets manager or CI secret, never in a repository, a Dockerfile, a URL query string or a screenshot. If one leaks, revoke it — revocation takes effect on the next request.

Token format

Personal access tokens are prefixed lth_pat_ followed by 32 bytes of URL-safe randomness, e.g. lth_pat_Ab3dEf9hJk2LmN4pQr6StU8vWx0YzA1bCd3EfG5hIj7. The prefix is stable and safe to match on if you run secret scanning across your own repositories — add lth_pat_ to your detection rules.

Send it

Put the token in the Authorization header. Nothing else is required — no API key header, no signature, no session cookie.

curl — verify the token
export LITHORA_TOKEN=lth_pat_your_token_here

curl -s https://api.lithora.app/api/auth/me \
  -H "Authorization: Bearer $LITHORA_TOKEN"

A 200 returns your user object. A 401 means the token is missing, malformed, expired or revoked — those four cases are deliberately indistinguishable from outside.

curl — a write, using tasks:write
curl -s -X POST https://api.lithora.app/api/tasks \
  -H "Authorization: Bearer $LITHORA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Ship the API docs",
    "project_id": "8f1c0c2e-4a2d-4f5b-9c31-6b2a7e0d4411",
    "priority": "high"
  }'
Python — plain requests
import os
import requests

BASE = "https://api.lithora.app"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['LITHORA_TOKEN']}"

me = SESSION.get(f"{BASE}/api/auth/me", timeout=30)
me.raise_for_status()
print(me.json()["email"])

For anything beyond a one-off call, use the Python SDK — it handles the header, maps status codes to typed exceptions, and exposes the endpoints as methods.

Scopes

Every token carries at least one scope; a token with none is rejected at creation. Grant the narrowest set that does the job.

ScopeLabel in the UIGrants
tasks:readView work itemsRead tasks and their subtask trees.
tasks:writeCreate & update work itemsCreate, update, reassign and delete tasks. Enforced.
projects:readView projectsRead projects and their metadata.
projects:writeCreate & update projectsCreate, update and delete projects. Enforced.
teams:readView teams & membersRead teams and membership — needed to resolve a team_id.
automations:readView automations & runsRead automation definitions, versions and run history.
automations:writeCreate & update automationsCreate, toggle, execute and roll back automations.
graph:readRead the work graphRead work items, relations and cycle-time analytics.
ai:chatUse the AI workspace (chat)Open sessions and send messages to the agent.
ai:confirmConfirm AI-proposed actionsApprove a staged agent plan. Grant this only if the token should be able to authorise writes the agent proposed.
admin:*Full account access (admin)Wildcard. Satisfies every scope check. Avoid unless you genuinely need it.

Scope enforcement is being rolled out route by route

These endpoints check scopes today and return 403 with “This access token is missing the required scope” when the token lacks one:

  • POST /api/projects — projects:write
  • PUT, DELETE /api/projects/{project_id} — projects:write
  • POST /api/tasks — tasks:write
  • PUT, PATCH, DELETE /api/tasks/{task_id} — tasks:write

Routes that are not yet wired inherit the owning user's full permissions. Until the rollout completes, treat every token as capable of anything you can do, and rely on expiry and revocation rather than scopes as your primary containment.

Manage tokens over the API

The token endpoints exist, with one deliberate restriction: a token cannot manage tokens. Creating and revoking both return 403 when the caller authenticated with a PAT, so a stolen token cannot mint a never-expiring replacement or delete your other tokens to hide the compromise. Use a browser session or a login JWT for these calls. Minting is also blocked while an admin is impersonating you.

Create

POST /api/auth/tokens
# Requires an interactive session cookie or a JWT — a PAT cannot mint a PAT.
curl -s -X POST https://api.lithora.app/api/auth/tokens \
  -H "Authorization: Bearer $LITHORA_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-release-bot",
    "scopes": ["tasks:read", "tasks:write", "projects:read"],
    "expires_in_days": 90
  }'

The response is the only time token appears:

201-style response body
{
  "token_id": "pat_9f2c1b7a4d3e6510",
  "name": "ci-release-bot",
  "scopes": ["tasks:read", "tasks:write", "projects:read"],
  "prefix": "lth_pat_Ab3dEf9h…",
  "token_prefix": "lth_pat_Ab3dEf9h…",
  "created_at": "2026-08-01T09:14:22.481Z",
  "expires_at": "2026-10-30T09:14:22.481Z",
  "last_used_at": null,
  "revoked": false,
  "status": "active",
  "token": "lth_pat_Ab3dEf9hJk2LmN4pQr6StU8vWx0YzA1bCd3EfG5hIj7"
}

expires_in_days accepts 0–3650; 0 or null means it never expires, which we discourage. You may hold 50 active tokens; past that, creation returns 400 and you must revoke something first.

List

GET /api/auth/tokens
curl -s https://api.lithora.app/api/auth/tokens \
  -H "Authorization: Bearer $LITHORA_JWT"

Returns { "tokens": [ ... ] }, masked. Each entry carries a derived status of active, expired or revoked, plus last_used_at — the fastest way to find a token nothing is using any more. GET /api/auth/tokens/scopes returns the grantable scope catalogue as { key, label } pairs.

Revoke

DELETE /api/auth/tokens/{token_id}
curl -s -X DELETE https://api.lithora.app/api/auth/tokens/pat_9f2c1b7a4d3e6510 \
  -H "Authorization: Bearer $LITHORA_JWT"

Immediate and irreversible. Pass the token_id, not the secret. Anything still using that token starts failing with 401 on its next request.

The JWT alternative

POST /api/auth/login returns a short-lived JWT for the same Authorization: Bearer header. It is the right credential for an interactive session and the wrong one for automation.

POST /api/auth/login
curl -s -X POST https://api.lithora.app/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your-password"}'

Login does not always return a token. If the account has 2FA enabled, or the request comes from a device we have not seen before, you get a challenge instead and must complete it before a token is issued. Client code has to check for the absence of token rather than assuming it is there:

Challenge responses (no token field)
// 2FA enabled on the account — no token issued:
{ "requires_2fa": true, "requires_login_otp": false, "email": "you@example.com" }

// Untrusted device — a one-time code was emailed:
{ "requires_2fa": false, "requires_login_otp": true, "email": "you@example.com" }

This is why automation wants a PAT

Neither challenge can be satisfied headlessly — one needs an authenticator app, the other needs a code from an inbox. A PAT has no challenge step, so a CI job using one keeps working when someone turns on 2FA.

There is no POST /api/auth/register. Direct registration was removed on purpose; accounts are created only through the email-OTP signup flow in the web app. If you are looking at an older client that calls it, that call has been failing.

Troubleshooting

You seeMost likely cause
401The header is missing the literal word Bearer, the shell did not expand $LITHORA_TOKEN, the token expired, or it was revoked. Also fires if the owning account was suspended or deprovisioned — deactivation closes the PAT door too.
403Either the token is missing a required scope (the message names it), you are not a member of the owning team, you hold a read-only guest seat, or you tried to manage tokens with a token.
404The id is wrong, or the resource is in the trash. Trashed items are invisible to the API.
400On token creation: an unrecognised scope name, an empty scope list, or the 50-token limit.