API¶
This page documents the HTTP API in Traq 3.9, based on the routes and controllers in the 3.9.0 source.
Traq has no separate API application. There are two groups of JSON endpoints:
- Dedicated endpoints under
/api/…that always return JSON. - Regular pages that also return JSON when you ask for it (the same URLs the web UI uses).
Warning
The API is currently being refactored to be RESTful. Endpoints, request formats and response shapes described here may change in a future release.
Note
Examples use https://traq.example.com. If Traq is installed in a
subdirectory, include it in the base URL.
Authentication¶
Generate an API key from your User CP (/usercp, "Create API key"). Send it in
the X-API-Key header, optionally prefixed with Bearer:
curl -H "X-API-Key: YOUR_KEY" https://traq.example.com/api/auth
curl -H "X-API-Key: Bearer YOUR_KEY" https://traq.example.com/api/auth
Requests made with a valid key act as that user, with that user's group and project permissions. Without a key (or session cookie) you are the guest user, so public projects are readable without authentication.
Requests authenticated with an API key are treated as API requests: JSON is returned for every controller action, and some actions (ticket create, update and move) return JSON instead of redirecting.
Requesting JSON¶
A regular (non-/api) URL returns JSON if any of these is true:
- The path ends in
.json, e.g./projects.json - The
Acceptheader is exactlyapplication/json - The request is authenticated with an API key
Request bodies¶
POST data is read from $_POST, so send form-encoded
(application/x-www-form-urlencoded or multipart/form-data) bodies. Query
string parameters are also accepted by most actions.
Two endpoints are the exception and read a raw JSON body:
POST /api/search and POST /{project}/tickets/{id}/related-tickets.
Responses and errors¶
- Responses are JSON with
Content-Type: application/json. - The keys
password,login_hash,api_keyandprivate_keyare stripped from every JSON response. - Validation failures when creating a ticket return
422with{"errors": …}. - Permission failures return
403({"error": "…"}) when the request hasAccept: application/json, otherwise an HTML error page. Some legacy paths return401. - A missing project returns
404; a project you can'tviewreturns403. - Dates are
YYYY-MM-DD HH:MM:SSstrings as stored in the database.
{project} below is the project slug.
Dedicated /api endpoints¶
All are GET, all return JSON. Endpoints with a project slug require the view
permission for that project.
| Endpoint | Description |
|---|---|
/api/auth |
Current user, or false if not authenticated |
/api/auth/{project} |
Same, plus the user's permissions in that project |
/api/types |
All ticket types |
/api/types/{id} |
One ticket type |
/api/statuses |
All ticket statuses |
/api/priorities |
All ticket priorities |
/api/{project}/components |
Project components |
/api/{project}/custom-fields |
Project custom fields |
/api/{project}/members |
Project members and roles |
/api/search |
Search tickets and milestones (POST, JSON body) |
GET /api/auth¶
{
"id": 1,
"username": "jack",
"name": "Jack",
"group_id": 1,
"locale": "enUS",
"group": { "id": "1", "name": "Admin", "is_admin": true },
"permissions": {}
}
permissions is only populated on /api/auth/{project}, and only contains
permissions that are granted, as { "action": true }. Returns false when
there is no authenticated user.
Types, statuses, priorities¶
// /api/types
[{ "id": 1, "name": "Defect", "bullet": "-", "changelog": 1, "template": "" }]
// /api/statuses
[{ "id": 1, "name": "New", "status": 1, "changelog": true }]
// /api/priorities
[{ "id": 1, "name": "Highest" }]
For statuses, status is the status type: 0 closed, 1 open, 2 started.
GET /api/{project}/components¶
[{ "id": 1, "name": "Core", "project_id": 1 }]
GET /api/{project}/custom-fields¶
Each item contains the custom field columns: id, name, slug, type
(default text), values, multiple, default_value, regex, min_length,
max_length, is_required, project_id, ticket_type_ids.
GET /api/{project}/members¶
[{ "id": 2, "username": "sam", "name": "Sam", "role": "Developer" }]
POST /api/search¶
Body is raw JSON.
| Field | Type | Description |
|---|---|---|
query |
string | Required. Matched with LIKE %query% against ticket summaries and milestone names |
project |
string | Optional project slug to limit the search |
Results are limited to 25 tickets and 25 milestones, only from projects the user can view.
curl -X POST -H "X-API-Key: KEY" -d '{"query":"crash","project":"traq"}' \
https://traq.example.com/api/search
{
"tickets": [
{ "ticket_id": 12, "summary": "Crash on save", "project": { "name": "Traq", "slug": "traq" } }
],
"milestones": [
{ "name": "3.9", "codename": null, "slug": "3.9", "project": { "name": "Traq", "slug": "traq" } }
]
}
Projects and milestones¶
| Method | Path | Returns |
|---|---|---|
| GET | /projects |
Array of all projects |
| GET | /{project}.json |
The project |
| GET | /{project}/roadmap.json |
Array of milestones (default: active ones) |
| GET | /{project}/roadmap/{completed\|all\|cancelled}.json |
Filtered milestones |
| GET | /{project}/milestone/{slug}.json |
One milestone |
/projects needs .json or Accept: application/json (or an API key) to
return JSON.
Project fields: id, name, slug, codename, info, enable_wiki,
default_ticket_type_id, default_ticket_sorting, displayorder.
Milestone fields: id, name, slug, codename, info, changelog, due,
completed_on, status, is_locked, project_id, displayorder.
Tickets¶
GET /{project}/tickets¶
Requires view_tickets. Returns a page of tickets.
{
"page": 1,
"total_pages": 3,
"tickets": [
{
"ticket_id": 12,
"summary": "Crash on save",
"user_id": 2,
"assigned_to_id": null,
"votes": 0,
"created_at": "2026-01-01 12:00:00",
"updated_at": null,
"is_closed": 0,
"owner": "Sam",
"assignee": null,
"type": "Defect",
"milestone": "3.9",
"milestone_slug": "3.9",
"version": null,
"version_slug": null,
"component": "Core",
"status": "New",
"status_id": 1,
"priority": "Normal",
"priority_id": 3,
"severity": "Normal",
"severity_id": 4,
"project_id": 1,
"project_slug": "traq"
}
]
}
Custom fields are added to each ticket under their slug. Page size is the
tickets_per_page setting.
Query parameters
| Parameter | Description |
|---|---|
page |
Page number (default 1) |
order_by |
field.direction, e.g. created_at.desc. Defaults to the project's default sorting |
| any filter field | Filter, see below |
Sortable fields: ticket_id, summary, votes, created_at, updated_at,
owner (or user), assigned_to, milestone, component, type, status,
priority, severity, plus custom field slugs. Direction is asc or desc.
Filterable fields:
| Field | Matching |
|---|---|
q |
Search ticket summary and ticket ID (contains) |
summary, description, owner |
Contains |
milestone, version |
Exact, by slug |
status, type, component, priority, severity, assigned_to |
Exact, by name |
status_type |
Status type: 0 closed, 1 open, 2 started |
| custom field slugs | Contains |
Multiple values are comma separated (status=New,Open); prefix the value with
! to negate (type=!Task). * is a wildcard in "contains" filters. For
status the special values allopen, allclosed and allstarted match by
status type. Unknown parameters are ignored.
curl -H "X-API-Key: KEY" \
"https://traq.example.com/traq/tickets?status=allopen&milestone=3.9&order_by=updated_at.desc"
POST /{project}/tickets¶
Creates a ticket (same as /{project}/tickets/new). Requires create_tickets.
Returns {"ticket": {…}} for API-key requests, or 422 with {"errors": …}.
Form fields:
| Field | Notes |
|---|---|
summary |
Required |
description |
Ticket body |
type |
Type ID (default 1) |
milestone, version, component, assigned_to |
IDs; 0 for none |
severity (default 4), priority (default 3), status (default 1) |
IDs |
tasks |
JSON-encoded array of {"task": "…", "completed": false} |
time_proposed, time_worked |
Strings |
custom_fields[{field_id}] |
Custom field values |
Each property beyond summary, description, type and privacy requires the
matching ticket_properties_set_* permission, otherwise it is ignored.
Non-admin users are subject to the ticket creation delay.
GET /{project}/tickets/{ticket_id}¶
Requires view_tickets.
{
"ticket": { "id": 12, "ticket_id": 12, "summary": "…", "body": "…", "type": {"id": 1, "name": "Defect"}, "…": "…" },
"attachments": [ { "id": 1, "name": "log.txt", "type": "text/plain", "size": 120, "user_id": 2, "ticket_id": 5, "created_at": "…" } ],
"ticketHistory": [ { "id": 1, "user_id": 2, "ticket_id": 5, "changes": [], "comment": "…", "created_at": "…" } ]
}
Ticket objects have project, user, assigned_to, milestone, version,
component, status, priority, severity and type expanded to
{id, name} objects (users also include username), plus votes, tasks,
extra, time_proposed, time_worked, is_closed,
created_at, updated_at. Attachment contents are not included.
POST /{project}/tickets/{ticket_id}/update¶
Requires update_tickets. Returns {"ticket": {…}}.
Fields: summary, type, milestone, version, component, severity,
priority, status, assigned_to, tasks, time_proposed, time_worked,
custom_fields[…], comment, and a file upload attachment.
Each property needs the matching ticket_properties_change_* permission;
omitted fields keep their current value. Attachments need add_attachments.
POST /{project}/tickets/{ticket_id}/move¶
Requires move_tickets and create_tickets in the destination project. API
requests must be POST (otherwise 400 {"error": "POST required"}).
| Field | Description |
|---|---|
project_id |
Destination project ID |
milestone_id, version_id, component_id, assigned_to_id |
Optional values in the destination project |
Returns {"success": true, "project": "new-slug", "ticket_id": 7}.
POST /{project}/tickets/mass-actions¶
Requires update_tickets and perform_mass_actions. Fields: tickets (array
of ticket IDs, or JSON string), type, milestone, version, component,
severity, priority, status, assigned_to (value -1 or omitted means
"don't change"), and comment. Returns {"success": true}.
Related tickets¶
GET /{project}/tickets/{ticket_id}/related-tickets
{ "related_tickets": [], "relation_types": [{ "id": 1, "name": "Relates to", "inverse_type_id": 1 }] }
POST with a JSON body {"related_tickets": [{"ticket_id": 3, "relation_type_id": 2}]}
replaces the set of related tickets (requires
ticket_properties_change_related_tickets; existing relations missing from the
list are removed) and returns the same structure as GET.
Built-in relation types: 1 Relates to, 2 Blocks, 3 Is Blocked By, 4 Duplicates, 5 Is Duplicated By, 6 Contains, 7 Is Contained By.
Other ticket actions¶
| Method | Path | Notes |
|---|---|---|
| POST | /{project}/tickets/{id}/tasks/{task_id} |
Toggle a task. Needs ticket_properties_complete_tasks. Returns {"success": bool} |
| POST | /{project}/tickets/{id}/vote |
Vote. Returns an HTML/JS fragment, not JSON |
| POST | /{project}/tickets/{id}/delete |
Needs delete_tickets. Redirects |
Wiki¶
Requires the project to have the wiki enabled.
| Method | Path | Returns |
|---|---|---|
| GET | /{project}/wiki |
{"page": {…}} (main page) |
| GET | /{project}/wiki/_pages |
{"pages": […]} |
| GET | /{project}/wiki/{slug} |
{"page": {…}} |
| GET | /{project}/wiki/{slug}/_revisions |
{"page": {…}, "revisions": […]} |
| GET | /{project}/wiki/{slug}/_revisions/{n} |
{"page": {…}, "revision": {…}} |
| POST | /{project}/wiki/_new |
Create. Fields title, slug, body. See note below |
| POST | /{project}/wiki/{slug}/_edit |
Edit. Fields title, slug, body. See note below |
| POST | /{project}/wiki/{slug}/_delete |
Returns {"success": true} |
Warning
The create and edit actions save the page, but in 3.9 they build the JSON
response without returning it, so the response body is not the
{"page": …} JSON you'd expect. Re-fetch the page with GET to confirm.
Page fields: id, project_id, title, slug, main, revision_id.
Revision fields: id, wiki_page_id, revision, content, user_id,
created_at, updated_at. Create, edit and delete need the corresponding wiki
permissions.
Admin¶
These need an admin user (or appropriate permissions).
Ticket types¶
| Method | Path | Fields | Returns |
|---|---|---|---|
| GET | /admin/tickets/types |
{"types": […]} |
|
| POST | /admin/tickets/types/new |
name, bullet, changelog, template |
{"type": {…}} |
| POST | /admin/tickets/types/{id}/edit |
same, all optional | {"type": {…}} |
Priorities¶
| Method | Path | Fields | Returns |
|---|---|---|---|
| GET | /admin/priorities |
{"priorities": […]} |
|
| POST | /admin/priorities/new |
name |
{"priority": {…}} |
| POST | /admin/priorities/{id}/edit |
name |
{"priority": {…}} |
| POST | /admin/priorities/{id}/delete |
{"success": true} |
Statuses¶
| Method | Path | Fields | Returns |
|---|---|---|---|
| GET | /admin/tickets/statuses |
{"statuses": […]} |
|
| POST | /admin/tickets/statuses/new |
name, status (0 closed, 1 open, 2 started), changelog |
{"status": {…}} |
| POST | /admin/tickets/statuses/{id}/edit |
same, all optional | {"status": {…}} |
| POST | /admin/tickets/statuses/{id}/delete |
{"success": true} |
Severities¶
| Method | Path | Fields | Returns |
|---|---|---|---|
| GET | /admin/severities |
{"severities": […]} |
|
| POST | /admin/severities/new |
name |
{"severity": {…}} |
| POST | /admin/severities/{id}/edit |
name |
{"severity": {…}} |
| POST | /admin/severities/{id}/delete |
{"success": true} |
See Ticket Properties for what these mean.
Other JSON endpoints¶
| Method | Path | Notes |
|---|---|---|
| GET | /_ajax/autocomplete/username?term=ab |
Usernames starting with term (* is a wildcard) |
| POST | /{project}/timeline/{id}/delete |
Needs delete_timeline_events. Returns {"success": false} with 403 when denied |
Caveats¶
- Traq 3.9 is in development, so details here may change before release.
- JSON support on the non-
/apiroutes is per-action. Only the endpoints listed above were found to return JSON; others return HTML regardless of the.jsonextension. - Many actions still use browser-style conventions (form-encoded bodies, redirects on success when not using an API key).
- Several other admin and project settings controllers (projects, users, groups, project roles, milestones, components, custom fields, members and project settings) return JSON from their create, edit and delete actions, but only for requests authenticated with an API key. They aren't documented here yet.