REST API
How to get a token and make a first call, what each credential and role may do, and how errors, edit conflicts and paging behave across Lific's HTTP API.
Every Lific server answers JSON over HTTP on the same address as its web UI. The web UI is built on this API, so anything you can do in the browser you can script. The API needs a running server; for direct database access without one, use the CLI.
Call it
Paths in this reference are complete: they already start with /api, /public/api, /oauth or /.well-known. Put your server's address in front of them, the same address you open in a browser. There is no version prefix.
Sign in to get a token, then send it as a bearer token:
LIFIC=https://lific.example.com
curl -s -X POST "$LIFIC/api/auth/login" \
-H "Content-Type: application/json" \
-d '{"identity":"alex","password":"correct horse battery"}'
# {"user":{"id":1,"username":"alex",...},"token":"lific_sess_9f2c...","expires_at":"..."}
TOKEN=lific_sess_9f2c...
curl -s "$LIFIC/api/projects" -H "Authorization: Bearer $TOKEN"
curl -s -X POST "$LIFIC/api/issues" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"project_id":1,"title":"Document the REST API","priority":"high"}'GET /api/health answers the plain text ok without a credential, which is enough for a load balancer or an uptime check.
Get a credential
Every credential goes in the same header: Authorization: Bearer <token>.
| Credential | Looks like | How you get one | Lasts |
|---|---|---|---|
| Session token | lific_sess_... | The token field of POST /api/auth/login or signup. | The instance's session lifetime, 30 days unless an administrator changed it. |
| API key | lific_sk-live-... | POST /api/auth/keys from a recent session, or lific key create on the server. | Until revoked. |
| Connected-tool key | lific_sk-live-... | POST /api/auth/bots. It acts as a bot you own, so the activity log names the tool. | Until disconnected. |
| OAuth access token | lific_at_... | The OAuth flow. Normally an MCP client runs it for you. | 30 days. There are no refresh tokens. |
Use a session token for a person at a keyboard and an API key for a script. An agent should normally connect through MCP rather than calling REST directly.
Login also sets an HttpOnly cookie named lific_token holding the same session token. The REST API ignores that cookie everywhere except three places: GET /api/attachments/{id} (so a browser <img> tag can load an attachment, and only when the request has no Authorization header), the realtime socket at GET /api/events/ws, which accepts nothing else, and the OAuth consent pages. Every other request needs the header, including requests from a browser.
These routes need no credential at all: GET /api/health, GET /api/instance, POST /api/auth/signup, POST /api/auth/login, POST /api/auth/auto-login, the public read API and the OAuth and discovery routes.
Two cases act as the instance's first administrator:
- An API key that is not bound to any user. The key printed on a fresh server's first start and keys made with
lific key createwithout a user are like this. Minting one takes shell access to the server, so Lific trusts it. Audit them withlific key list. - A request with no credential on an instance running with
[auth] required = false. A credential that is present but invalid is still refused with401, so a broken client configuration shows up as an error.
Which credentials an endpoint accepts
Most endpoints accept any valid credential and then check the caller's role. Three rules narrow that.
A recent browser session. Endpoints that hand out lasting access require a session token in the Authorization header that was created in the last 15 minutes. An API key, an OAuth token, an older session and the cookie all get 403 recent authentication required, and nothing is written. The endpoints are:
POST /api/auth/keysandPOST /api/auth/botsPATCH /api/instance/settingsPOST /api/users,POST /api/users/{id}/promoteandPOST /api/users/{id}/reactivatePOST /api/projects/{id}/members, andPATCH /api/projects/{id}/members/{user_id}when it raises a rolePUT /api/projects/{id}when it setslead_user_idto a userPOST /api/projectswhen it names somebody other than the caller as lead
Approving an OAuth connection in the browser follows the same rule. A tab that has been open for a while can get a fresh session for the same account from POST /api/auth/me/refresh without signing out.
A browser session of any age. POST /api/auth/me/refresh and the three project archive routes accept only a session token. API keys, unbound operator keys and OAuth tokens get 403.
Not an OAuth token. An OAuth token belongs to a tool acting for a person, so it cannot manage credentials or accounts. Lific answers these with 403 and a plain-text reason: API key and connected-tool routes (/api/auth/keys, /api/auth/bots and everything under them), PATCH /api/auth/me, POST /api/auth/me/password, POST /api/auth/me/refresh, DELETE /api/auth/me/sessions, POST /api/users and everything under /api/users/. GET /api/auth/me and GET /api/users stay open to it.
Roles
A role is held on one project. Each role includes everything below it. The projects concept page describes them in full.
| Role | In short |
|---|---|
viewer | Read the project, write comments, keep personal saved views. |
maintainer | Create, edit, delete and restore issues, pages and plans; link issues; manage modules, labels and folders. |
lead | Project settings, membership, publishing, repository bindings, GitHub import, deleting the project. |
| Administrator | A flag on the account, not a project role. Everything in every project, plus workspace pages and instance settings. |
A connected tool or OAuth client acts with the access of the person who owns it and never more. An operation that touches two projects, such as linking issues across them, needs the role in both.
Each endpoint in this reference names the minimum role it checks. That is the rule when the instance setting authz_enforced is on, which is the default for new installs. With it on, a caller without the role gets 403, and a project they cannot read is simply missing from lists, search and cross-project reads rather than appearing as an error.
With authz_enforced off, which is how an instance upgraded from a version before project membership starts:
- Endpoints that need
viewerormaintaineraccept any authenticated caller. - Endpoints that need
leadaccept the project's primary lead, anyleadmember or an administrator. A project with no lead can only be changed by an administrator. - Module, label and folder changes need
leadinstead ofmaintainer. - Deleting a project needs an administrator.
- Workspace pages (pages with no project) are open to every caller. With enforcement on they are administrator-only, for reads as well as writes.
GET /api/projects/{id}/my-role tells a client which role it holds and whether enforcement is on.
Requests
Send JSON bodies with Content-Type: application/json. Unknown fields are ignored. Request bodies are capped at 2 MiB unless an endpoint says otherwise; the attachment upload, project archive upload, GitHub import and OAuth routes each set their own limit.
Every fixed size, rate and concurrency limit is collected on Limits.
In update bodies, some fields can be cleared. For those, leaving the field out keeps the current value and sending null clears it. Each endpoint marks which fields work this way.
Numeric ids appear in paths. Issues, pages, plans and projects also have readable identifiers such as APP-42, APP-DOC-3, APP-PLAN-2 and APP; the resolve endpoints turn one into the full record. Identifiers are matched without regard to case.
Most timestamps are UTC strings in SQLite's format, such as 2026-07-14 12:00:00.
Concurrent edits and expected_seq
Issues, pages and comments carry seq, a counter shared by every issue, page and comment on the instance. Every write to a row moves its seq above every value handed out so far. A new comment also moves its issue's or page's seq, because it changes what a reader of that issue or page sees.
By default the last write wins. To avoid overwriting somebody else's change, send the seq you last read as expected_seq in PUT /api/issues/{id} or PUT /api/pages/{id}. If the row has changed since, Lific writes nothing and answers 409 with the record as it now stands:
{
"error": "APP-42 has changed since seq 118 (current seq 121); re-read it, merge your change, and retry",
"code": "update_conflict",
"current": { "id": 42, "identifier": "APP-42", "seq": 121, "title": "...", "...": "..." }
}current has the same shape as the GET response, so a client can merge and retry without another request.
Errors
Handlers answer failures with a JSON object carrying one error string, and a status that says what kind of failure it was:
{ "error": "issue 999 not found" }| Status | Meaning |
|---|---|
400 | The request is invalid: a bad value, a missing field, an unknown status. Rate-limited sign-in, signup, password change, session refresh and repository binding changes also answer 400, with a message that says how many seconds to wait. |
401 | No credential, or one that is invalid, expired or revoked. Comes from the authentication layer as plain text, not JSON, with a WWW-Authenticate: Bearer resource_metadata="..." header that points OAuth clients at the discovery document. |
403 | Authenticated but not allowed, including recent authentication required. The refusal of an OAuth token on an account route is plain text. |
404 | The record does not exist, is in the trash, or is hidden from you in a way that must not reveal that it exists. |
405 | The path exists but not with that method. |
409 | A conflict: a duplicate name, removing the last lead, a GitHub import already running, or an expected_seq mismatch (see above). |
413 | The request or its result is larger than Lific accepts. The message names the limit. |
415, 422 | The body is not JSON, or does not have the expected shape. These come from the request parser as plain text. |
429 | A shared slot is taken: exports, project archives, the public read API's rate limit. Carries Retry-After: 30. The realtime socket's connection limit also answers 429, without the header. |
500 | Something broke on the server. The body says only internal server error; the detail is in the server log. |
503 | Temporarily unable, and the same request will work shortly. Carries Retry-After: 2. Seen when a backup or restore holds the attachment store, when the public read API is busy, and when a trusted proxy's client-identity headers do not verify. |
The OAuth endpoints follow the OAuth specifications instead: {"error": "invalid_grant", "error_description": "..."}, where error is a machine-readable code and error_description is optional. The consent pages under /oauth are HTML for a person, not JSON.
Lists and paging
Most list endpoints return a bare JSON array with no total and no cursor. A few return an object instead; each endpoint says which.
| Endpoint family | Default limit | Maximum | Notes |
|---|---|---|---|
| Issues, plans, project files | 50 | 500 | limit and offset. |
| Search | 20 | 500 | offset stops at 100,000. |
| Activity feeds | 50 | 200 | Returns items and has_more. |
| Comments | 50 | 500 | Paging information is in response headers. See comments. |
| Pages | everything | 500 | Paginated only when limit or offset is given. |
| Delta sync changes | 5,000 | 50,000 | Cursor-based. See delta sync. |
A limit below 1 counts as 1 and a negative offset counts as 0. GET /api/plans also supports keyset paging with before_id when order_by=id.
Shared shapes
These records appear across many endpoints. Other shapes are described with the endpoint that returns them.
Project
id, name, identifier, description (Markdown), emoji (a literal emoji, a lucide:Name reference, or null), lead_user_id (integer or null), sort_order, is_public, created_at and updated_at. In a project list, sort_order is the project's position in the caller's own sidebar order.
{
"id": 1,
"name": "Lific",
"identifier": "APP",
"description": "",
"emoji": null,
"lead_user_id": 1,
"sort_order": 0,
"created_at": "2026-07-14 12:00:00",
"updated_at": "2026-07-14 12:00:00",
"is_public": false
}Issue
id, project_id, sequence (the number in the identifier), identifier, title, description, status (backlog, todo, active, done or cancelled), priority (urgent, high, medium, low or none), module_id, sort_order (a number), start_date, target_date, created_at, updated_at, seq and labels (label names).
source appears only on an issue that came from an import, such as github:owner/repo#12. Single-issue responses add relation arrays when they have entries: blocks, blocked_by, relates_to, duplicates and duplicated_by, each a list of identifiers. An empty relation array is left out. An issue that waits on a person or a date carries waits, an array of Wait objects, in lists and single reads; it too is left out when empty.
{
"id": 7,
"project_id": 1,
"sequence": 3,
"identifier": "APP-3",
"title": "Document the REST API",
"description": "",
"status": "active",
"priority": "high",
"module_id": null,
"sort_order": 1000.0,
"start_date": null,
"target_date": null,
"created_at": "2026-07-14 12:00:00",
"updated_at": "2026-07-15 09:30:00",
"seq": 118,
"labels": ["docs"],
"blocked_by": ["APP-2"]
}Page
id, project_id (null for a workspace page), sequence, identifier (APP-DOC-1, or DOC-1 for a workspace page), folder_id, title, content (Markdown), sort_order, status (draft, active, complete or archived), pinned, created_at, updated_at, seq and labels. Workspace pages have no labels.
Comment
id, issue_id or page_id (exactly one is set), user_id, author (username), author_display_name, content (Markdown), created_at, updated_at, seq and kind. kind is comment, or verification for the evidence an agent recorded while closing an issue through MCP update_issue. A verification comment otherwise behaves like any other: its author can edit or delete it.
Module, label and folder
- Module:
id,project_id,name,description,status(backlog,planned,active,paused,doneorcancelled),emoji,created_at,updated_at. - Label:
id,project_id,name,color(a hex string such as#6B7280). - Folder:
id,project_id,parent_id,name,sort_order.
Delete responses
Unless an endpoint says otherwise, a successful delete answers {"deleted": true}.
Reference
| Page | Covers |
|---|---|
| Accounts and access | Instance settings, signing in, sessions, API keys, connected tools, user administration. |
| Projects | Projects, sidebar groups, membership, saved views, board and insights, project activity, GitHub import. |
| Issues, pages and plans | Issues, relations, waits and the server clock, the trash, comments, pages, plans, modules, labels and folders. |
| Attachments | Uploading, downloading, previews and the project file list. |
| Search, sync and realtime | Search, the delta sync endpoints and the realtime WebSocket. |
| Exports and archives | Markdown, JSON and ZIP exports, and whole-project archives. |
| Repositories and commits | Repository bindings and closing issues from commit messages. |
| Public read API | The anonymous, read-only routes for a published project. |
| OAuth and discovery | Client registration, authorization, device login, tokens and metadata. |
The MCP endpoint, /mcp, is served by the same process and takes the same bearer credentials. It is documented under MCP.