Integrators#
This chapter is for whoever writes something that talks to the tracker: a script, a dashboard, a CI job, a bot, or a language model.
The short version: the API is MantisBT’s. Same paths, same JSON, same status lines, same headers. A script written against MantisBT 2.28’s REST API runs against locust unchanged, which is the point – and that promise is checked, route by route, against a real MantisBT installation.
Two things are locust’s own and are marked where they appear: extra
fields on the issue JSON for features MantisBT has not (estimates,
deadlines, the compliance roles, work packages, meetings), and a
separate namespace, /api/ai, for language models.
Getting in#
Figure 31 API Tokens in the account pages: one token per client, shown once.#
Every request carries an API token in the Authorization header
– the token itself, with no scheme word in front of it:
$ curl -H "Authorization: $TOKEN" https://bugs.example.org/api/rest/users/me
A user mints tokens on the API Tokens tab of their account pages. The
token is shown once, at creation; after that only its name and the day
it was last used. Revoking one is immediate.
A token acts as the user who made it, with that user’s access everywhere: there is no scope, no separate permission system, and nothing a token can do that its owner cannot. An installation that allows anonymous browsing also answers read requests with no token at all, under the anonymous user’s access.
Two base paths work, and are the same API:
https://host/api/rest/...– with MantisBT’s rewrite rule;https://host/api/rest/index.php/...– without it.
Everything else of MantisBT’s URLs has moved: pages have lost their
.php and old links answer 308. The API has not moved, on
purpose.
Conventions#
- JSON
Written as PHP’s
json_encode()writes it, down to the escaping, because clients that compare bytes or hash bodies exist.- ETags
Read requests carry an
ETag; sending it back inIf-None-Matchgets304 Not Modifiedwith no body. Use it for polling: it is far cheaper on both sides than fetching the list again.- Status lines
MantisBT’s, with its reason phrases.
- Errors
{"message": "...", "code": N, "localized": "..."}– MantisBT’s shape, with MantisBT’s error numbers./api/aiuses a different, slug-first shape (below), deliberately: the two namespaces are different promises.- Paging
pageandpage_sizeon the list routes.- Rate limits
An installation may limit requests per client address and per token. Over the limit, the answer is
429withRetry-After; a client that respects that header will never notice anything else.
The routes#
Issues#
- GET /api/rest/issues#
A page of issues. Query parameters:
project_id,filter_id(a saved filter),page,page_size, and MantisBT’sselectto narrow the fields returned.
- GET /api/rest/issues/(int: id)#
One issue, with its notes, attachments, relationships, tags, monitors, custom fields and history as the viewer may see them.
- POST /api/rest/issues#
File one.
summary,description,projectandcategoryat least.
- PATCH /api/rest/issues/(int: id)#
Change it. Only the fields you send.
- DELETE /api/rest/issues/(int: id)#
Delete it, if your account may.
Notes, files, tags, monitors and relationships hang off an issue:
POST /api/rest/issues/{id}/notesandDELETE /api/rest/issues/{id}/notes/{note_id}GET/POST/api/rest/issues/{id}/filesandGET /api/rest/issues/{id}/files/{file_id}POST /api/rest/issues/{id}/tagsandDELETE /api/rest/issues/{id}/tags/{tag_id}POST /api/rest/issues/{id}/monitorsPOST /api/rest/issues/{id}/relationshipsandDELETE /api/rest/issues/{id}/relationships/{relationship_id}
Projects#
GET, POST, PATCH and DELETE on /api/rest/projects
and /api/rest/projects/{id}, plus:
/projects/{id}/versionsand/projects/{id}/versions/{version_id}(GET,POST,PATCH,DELETE)/projects/{id}/subprojectsand/projects/{id}/subprojects/{subproject_id}/projects/{id}/usersand/projects/{id}/users/{user_id}/projects/{id}/handlers– who may be assigned issues here
Users#
GET /api/rest/users/me– who this token belongs to, and what that account may do. The first call a client should make.GET /api/rest/users/{id},GET /api/rest/users/username/{name}POST,PATCH,DELETEon users, andPUT /api/rest/users/{id}/reset, for accounts that may administerPOST/DELETE/api/rest/users/me/tokenand/api/rest/users/{user_id}/token/{token_id}
The rest#
GET /api/rest/configandPATCH /api/rest/config– the configuration options a client may read or setGET /api/rest/filters,GET /api/rest/filters/{id},DELETE /api/rest/filters/{id}– saved filtersGET /api/rest/lang– the tracker’s own strings, for a client that wants to show themGET /api/rest/pages/issues/view/{id}– everything the issue page needs in one response/api/rest/internal/autocompleteand/api/rest/internal/config_display– what MantisBT’s own pages use
locust’s own fields#
An issue’s JSON carries these in addition to MantisBT’s, each only where the project has the feature and the viewer may see it:
effort_estimateHours, where the project estimates.
due_dateThe deadline, under MantisBT’s
due_date_view_threshold– which locust, unlike MantisBT, does not set to “nobody” by default.fast_trackWhether this issue takes the short way, where the project allows it.
work_package{"id": N, "name": "..."}, the contract the hours are charged to. APATCHmay name one byid, bynameor as a bare string, as it may name a version, and the empty string takes it off again. A package of another project, or one that has been switched off, is refused – except the one the issue already carries, which stays nameable after it has been switched off.effort_actualHours, the sum of what the clock measured. Read-only.
meetingWhich meeting the issue came out of, where one did; notes likewise say which meeting they were written in. Read-only.
A read-only field is refused rather than ignored: a PATCH that
names effort_actual or effective_due_date with a value other
than the one the issue has answers 400 naming the field, and writes
nothing else from that request either. Repeating the value the issue
already has passes, so a client may read an issue, change one field and
send the whole object back.
The four compliance roles – implementer, reviewer, verifier,
communicator – the authority notification pairs and the notification
list (notify) travel the same way in a project that runs the
process, and can be set through POST and PATCH like any other
field. Sending notify replaces the list: an addressee the request
does not repeat is taken off.
One place where a compliance project is stricter than MantisBT:
MantisBT’s issue update checks only that the caller may change a status
at all, never which status, so set_status_threshold applies to its
pages and not to its API. In a project that runs the process the
thresholds are the process’s own and hold on every door, so a PATCH
setting a status answers 403 where the page would refuse it. A
project that does not run the process keeps MantisBT’s behaviour
exactly.
Everything here is additive: a client that ignores unknown fields sees exactly MantisBT’s issue.
Other ways out#
Not everything needs the API:
Any filtered issue list exports as CSV, an Excel workbook or a Word document; the columns are the ones the account configured.
Every list is an RSS feed. The URL carries a per-user key – treat it as a credential, and note that it changes when that user changes their password.
GET /healthzanswers200/okwhile the database answers. It needs no token and exposes nothing; poll it from a load balancer.
Agents#
A language model pointed at a bug in a tracker has to work for its answer: the bug is a page of markup, the API that would hand it over as data needs a token nobody can mint without a browser, and nothing on the site says any of this. locust’s answer is a door that is explicit about what is behind it – and not a second, laxer way into the data.
Reading#
Every GET page that shows data also answers JSON when asked, at the
page’s own URL: Accept: application/json, or ?format=json.
$ curl -H 'Accept: application/json' https://bugs.example.org/view?id=42
The shape is locust’s own and deliberately slim: scalars where
MantisBT’s REST API has objects ("status": "assigned" with
"status_code": 50 beside it, a user as their username). Reading
needs no token wherever reading the page needs none, and the visibility
rules are the same ones the HTML obeys – an agent sees exactly what an
anonymous visitor sees, no more.
GET /api/ai/issues and GET /api/ai/issues/{id} are the same data
under a stable path, and full=1 widens the shape to the REST v1
issue for a client that wants everything.
Discovery#
/llms.txtGenerated Markdown: what this tracker is, which projects are readable without a token, the policies (below), how to get a token, what can be read and what can be written, and every error slug.
/.well-known/aiThe same thing as JSON, in the shape of the
draft-aiendpoint-ai-discovery-00Internet-Draft. The draft is an unadopted individual draft and the document says which version it follows, so a client can tell what it is reading.
Every page also carries one comment in its <head> pointing at
/llms.txt and saying that the page answers JSON.
Writing#
Writing needs an account, and an agent’s account is marked as one. The four rules, which the guide states in the second person and the code enforces:
An agent may register its own account, but its username begins with
bot-and its contact address is the human who runs it.That human confirms it. Until they do, the token is inert.
An agent never deletes anything, and never rewrites somebody else’s words – only its own notes.
Everything it does carries its name, and its name is marked as an agent’s wherever it appears.
- POST /api/ai/signup#
username,email(the human’s),real_nameandpurpose. The human is mailed who asked, under what name, for what purpose and from which address, with a link to allow it and a link to reject it; a third link, later, switches it off again. Rejecting removes the account and its token; switching off leaves what it wrote where it is.
- GET /api/ai/me#
What this agent’s account is and what it may do.
- POST /api/ai/issues/tree#
File a whole tree in one statement, all or nothing:
{"request_id": "...", "issue": {...}}where every node may carrychildren, nested as deep as the installation allows. A specification in one project with three implementations in three others, linked as children, is one request.A node may set what the update page can set, at the agent’s level in each project. The status may not be set: it is always
new, and a request that says otherwise is refused rather than quietly corrected – an agent that got a201after asking forassignedwould believe it had assigned the issue.
- POST /api/ai/issues/(int: id)/notes#
Add a note.
- POST /api/ai/issues/(int: id)/files#
Attach a file.
- PATCH /api/ai/notes/(int: id)#
Correct a note the agent itself wrote.
Every write takes a ``request_id``. A timed-out agent retries, and a tree of twenty issues filed twice is the expensive mistake; the tracker remembers the id with a hash of the request and answers the retry with the first result instead of filing again.
Errors on /api/ai are slug-first, so a program can branch on them
without parsing prose:
{"error": "tree-too-large", "message": "...", "ref": "<node>", "field": "..."}
Every slug is listed in /llms.txt.
Limits#
An installation sets how big one tree may be (issues, depth, bytes), how
long a confirmation link lives and how long request ids are remembered,
and can switch the whole door off ([locust-ai] ENABLED = NO), which
removes the discovery documents, the JSON negotiation and /api/ai
and changes nothing else. Signup for agents can be refused separately.
What is deliberately not there: an agent cannot create tags – it
attaches the tracker’s vocabulary, it does not invent it – and there is
no webhook or feed yet, so an agent that wants to know about changes
polls (with If-None-Match, which is what ETags are for here).
A worked example#
Three issues, assigned to you, oldest first, as JSON:
$ curl -s -H "Authorization: $TOKEN" \
'https://bugs.example.org/api/rest/issues?page_size=3&filter_id=assigned'
Add a note to issue 42:
$ curl -s -X POST -H "Authorization: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"text": "Reproduced on 1.2.3, log attached."}' \
https://bugs.example.org/api/rest/issues/42/notes
Poll a list without downloading it twice:
import requests
session = requests.Session()
session.headers["Authorization"] = TOKEN
etag = None
while True:
headers = {"If-None-Match": etag} if etag else {}
r = session.get(f"{BASE}/api/rest/issues?project_id=1", headers=headers)
if r.status_code == 304:
... # nothing changed
else:
etag = r.headers.get("ETag")
handle(r.json()["issues"])
time.sleep(60)