API
With the API, other software can work with the tickets in your
LeanTrack: read projects and tickets, create and change tickets, add
comments and files. It speaks JSON over HTTPS, under
/api/v1/ of your LeanTrack's address.
A key
Make a key under My account → API keys. Copy it straight away: it is shown only once. A key works as you: it sees the same projects and may do the same as you, and everything it changes shows up under your name in the history.
For software, make a separate user (for example "Merxo") that sees only the projects it needs, log in as that user once and make the key there. Admins see each user's keys under Users and can revoke them; deactivating a user stops their keys. The API can be switched off altogether under Settings → Features.
When you make a key, you can limit it to some of your projects: it then
only reaches those, and only as long as you can still see them. Without a
limit it reaches every project you can see, also ones you get later.
GET me tells which projects a key reaches.
Sending the key
Every request sends the key in the Authorization header:
curl -H "Authorization: Bearer ltk_…" https://tickets.example.com/api/v1/me{
"id": 7,
"username": "Merxo",
"role": "user",
"language": "en",
"leantrack_version": "0.20.45",
"key": { "name": "Merxo", "projects": [3, 5] },
"features": { "subtickets": true, "priority": true, "assignee": true, "api": true }
}
Endpoints
All under /api/v1/.
| Request | What it does |
|---|---|
GET me | The key's user: name, role, language, which features are on; and which projects the key reaches ("all", or their IDs). |
GET projects | The projects the key's user can see. |
GET projects/{id} | One project, with the prefixes, statuses (in order), groups, users who can be assigned and tags in use. |
GET tickets | Tickets, 50 per page, newest change first. Filters: project, status, assignee, priority, q, tag, group, page. |
POST tickets | Create a ticket (or a sub-ticket, with parent_id). |
GET tickets/{id} | One ticket with everything: description, comments, attachments, sub-tickets, watchers and history. |
PATCH tickets/{id} | Change a ticket: only the fields you send. |
DELETE tickets/{id} | Delete a ticket (it can be revived on the Deleted page). |
POST tickets/{id}/comments | Add a comment. |
PATCH comments/{id} | Change your own comment. |
DELETE comments/{id} | Delete your own comment. |
POST tickets/{id}/attachments | Attach files (multipart form, field file or files[]). |
GET attachments/{id} | Download an attachment. |
DELETE attachments/{id} | Remove an attachment (yours, or any as admin). |
POST tickets/{id}/watch | Watch a ticket. |
DELETE tickets/{id}/watch | Stop watching a ticket. |
Examples
List tickets
Without status you get the tickets that aren't closed;
status=all gives all of them. assignee takes a
user ID, none or me; q searches like
the search field at the top.
curl -H "Authorization: Bearer ltk_…" "https://tickets.example.com/api/v1/tickets?project=3&assignee=me"{
"tickets": [
{
"id": 42,
"key": "WEB-42",
"label": "Invoice PDF shows the wrong VAT",
"project": { "id": 3, "name": "Webshop" },
"prefix": "WEB",
"status": { "code": "in_progress", "label": "In Progress" },
"priority": "high",
"assignee": { "id": 7, "username": "Merxo" },
"reporter": { "id": 2, "username": "anna" },
"parent_id": null,
"tags": ["invoice"],
"created_at": "2026-10-19T09:12:00Z",
"updated_at": "2026-10-19T14:03:27Z",
"closed_at": null,
"deleted": false,
"url": "https://tickets.example.com/ticket/42"
}
],
"page": 1,
"pages": 1,
"per_page": 50,
"total": 1
} GET tickets/{id} gives the same plus description,
resolution, groups, comments,
attachments (with a download url),
subtickets, watchers and history.
Create a ticket
curl -X POST -H "Authorization: Bearer ltk_…" -H "Content-Type: application/json" \
-d '{"project_id": 3, "prefix": "WEB", "label": "Customer cannot log in",
"description": "<p>Since this morning.</p>", "priority": "high", "tags": ["login"]}' \
https://tickets.example.com/api/v1/tickets The answer is 201 with the whole new ticket, as with
GET tickets/{id}.
Change a ticket
Send only what changes. The same checks as the edit form apply: for example, a ticket can't be closed while one of its blocking sub-tickets is still open.
curl -X PATCH -H "Authorization: Bearer ltk_…" -H "Content-Type: application/json" \
-d '{"status": "review", "assignee_id": null}' https://tickets.example.com/api/v1/tickets/42
Comment, attach, watch
curl -X POST -H "Authorization: Bearer ltk_…" -H "Content-Type: application/json" \
-d '{"body": "<p>Fixed in release 2.4.</p>"}' https://tickets.example.com/api/v1/tickets/42/comments
curl -X POST -H "Authorization: Bearer ltk_…" -F "file=@screenshot.png" \
https://tickets.example.com/api/v1/tickets/42/attachments
curl -X POST -H "Authorization: Bearer ltk_…" https://tickets.example.com/api/v1/tickets/42/watch
Ticket fields
| Field | Value |
|---|---|
label | text, required |
project_id | number, required when creating (not for a sub-ticket) |
prefix | the prefix code, e.g. WEB (or prefix_id), required when creating |
description | HTML |
resolution | HTML |
status | a status code from GET projects/{id}; new tickets start in the first one |
priority | low, mid, high |
assignee_id | a user ID from GET projects/{id}, or null for nobody |
tags | a list of names, e.g. ["bug", "invoice"] |
groups | a list of group IDs of the ticket's project |
parent_id | only when creating: makes it a sub-ticket |
blocks_parent | true/false, sub-tickets only: the parent can't be closed while this one is open |
Fields the API doesn't know are refused (422), so a typo
doesn't go unnoticed. A ticket can't be moved to another parent — on the
pages neither.
Errors
Every error is JSON with a message, and for checks the fields that are wrong:
{
"message": "Give the ticket a label. Choose a prefix.",
"errors": {
"label": "Give the ticket a label.",
"prefix_id": "Choose a prefix."
}
} | Status | Meaning |
|---|---|
400 | The body isn't a JSON object. |
401 | No key, an unknown or revoked key, or the key's user is deactivated. |
403 | Not allowed, e.g. changing someone else's comment. |
404 | Doesn't exist, or the key's user can't see it (the same answer, on purpose). Also when the API is switched off. |
409 | The ticket is deleted: revive it first. |
422 | A check failed; errors says which field and why. |
429 | More than 120 requests in a minute with this key. Retry-After says how many seconds to wait. |
Good to know
- Rate limit: 120 requests per minute per key. Every
answer has
X-RateLimit-LimitandX-RateLimit-Remaining. - HTML: descriptions and comments are HTML and are
cleaned like the editor's:
p, br, strong, b, em, i, u, s, h1–h3, ul, ol, li, blockquote, pre, codeand links stay, anything else is removed. - Dates are UTC, e.g.
2026-10-19T14:03:27Z. - Messages from the API are always in English.
- Watcher emails go out for changes through the API just like for changes on the pages.
- On Apache with PHP as FastCGI the included
.htaccesspasses theAuthorizationheader on to PHP; other web servers must do the same.