REST API
Everything Lithora does in the browser it also does over HTTP. JSON in, JSON out, one Bearer credential, no SDK required.
Base URL
Every path is prefixed with /api. Requests and responses are JSON; send Content-Type: application/json on anything with a body.
| Environment | Base URL |
|---|---|
| Production | https://api.lithora.app |
| Local development | http://localhost:8000 |
The API host is api.lithora.app even though the product lives on lithora.app. That is deliberate and stable — point your integrations at it.
Your first call
Mint a personal access token at Settings → API Tokens with the teams:read scope, export it, and confirm it works. GET /api/auth/me is the cheapest way to prove a credential is live.
curl -s https://api.lithora.app/api/auth/me \
-H "Authorization: Bearer $LITHORA_TOKEN"A 200 with your user object means you are in. A 401 means the token is missing, malformed, expired or revoked. Full walkthrough on Authentication.
Where to go next
Authentication
Mint a personal access token, pick scopes, send it, revoke it. Start here.
Endpoint reference
Teams, projects, tasks, the work graph and the confirmation-gated agent — with real request and response bodies.
CLI
Install lithora-cli, authenticate, and script your workspace from a terminal or CI job.
Python SDK
pip install lithora — a typed client with resource namespaces and real exception classes.
GitHub webhooks
HMAC verification, the normalized github.* event names, and every payload shape.
OpenAPI 3.1 spec
The curated machine-readable contract. Feed it to your generator of choice.
Conventions
- Ids are opaque strings. Read
team_id,project_id,task_idback from the API; never construct or parse them. - Timestamps are ISO 8601 UTC strings, e.g.
2026-08-01T09:14:22.481Z. Date-only fields such asdue_dateaccept either a date or a full timestamp. - Everything is tenant-scoped. A project belongs to a team and a task belongs to a project. If you are not a member of the owning team you get a
403, and if the resource is in the trash you get a404— trashed items never appear in list responses. - Guests are read-only. A guest seat can read shared work and will be refused with
403on any write. - Collection responses are not paginated.
GET /api/tasksreturns a nested subtask tree as a bare array, hard-capped at 10,000 tasks per response. Filter withproject_idorteam_idrather than fetching everything.
Errors
Every non-2xx response uses one structured envelope. The correlation_id is also returned in the X-Correlation-ID response header — include it when you contact support and we can find the exact request in our logs.
{
"success": false,
"verified": false,
"status": "failure",
"error_code": "PERMISSION_DENIED",
"message": "You don't have access to this project",
"detail": "You don't have access to this project",
"correlation_id": "1f0e9b6a-3c25-4f8b-9a0d-2b71c4e8d5a3",
"timestamp": "2026-08-01T09:14:22.481Z"
}On a 422, detail is an object listing each field that failed validation instead of a string. Parse defensively.
{
"success": false,
"status": "failure",
"error_code": "VALIDATION_ERROR",
"message": "Request validation failed",
"detail": {
"errors": [
{ "field": "body -> title", "message": "String should have at least 1 character", "type": "string_too_short" }
]
},
"correlation_id": "8c31d0f4-27ab-4f19-b3ec-6a5d2b904177",
"timestamp": "2026-08-01T09:16:40.102Z"
}Status codes
| Status | error_code | What it means |
|---|---|---|
| 400 | INVALID_REQUEST | The request was understood but rejected — an unknown scope, a bad date, a limit reached. |
| 401 | AUTHENTICATION_REQUIRED | Missing, malformed, expired or revoked credential. |
| 403 | PERMISSION_DENIED | Authenticated, but not allowed: not a team member, a guest attempting a write, or a token missing a required scope. |
| 404 | RESOURCE_NOT_FOUND | No such resource, or it is in the trash. |
| 409 | RESOURCE_CONFLICT | The write collided — most often a task_id already in use. |
| 422 | VALIDATION_ERROR | The body failed schema validation. detail.errors lists the offending fields. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. Back off and retry. |
| 5xx | INTERNAL_ERROR | Our fault. message and detail are deliberately generic; quote the correlation id. |
Rate limits
Limits are declared per endpoint and keyed by client IP. There is no global request cap today, so treat the numbers below as the enforced floor rather than a budget to spend — write clients that back off on 429 regardless.
| Endpoint group | Limit |
|---|---|
| POST /api/auth/login | 10 / minute |
| OTP request and verify routes | 5 / minute |
| File uploads | 10 / minute |
| AI and model-backed routes | 30 / minute |
| POST /api/github/webhooks | 120 / minute |
| Core CRUD (tasks, projects, teams, work items) | No declared per-route limit |
AI usage is metered separately
402 or 403 naming the limit — not a 429. Retrying will not help; see Billing & Plans.Interactive docs are off in production
FastAPI's Swagger UI, ReDoc and runtime schema are disabled on api.lithora.app on purpose, so the full internal endpoint catalogue cannot be enumerated by outsiders. This documentation and the curated OpenAPI 3.1 spec are the public contract. If you run the backend locally, http://localhost:8000/api/docs works as usual.