Requests and errors
Formats, how things are named, dates, rich text, paging, status codes, error codes and rate limits.
Requests and answers
Requests and answers are JSON. A GET takes its arguments in the query string; everything else takes a JSON body, sent with Content-Type: application/json. A body in any other format is refused with 422.
Every request is checked against the OpenAPI description before anything happens: a required field that is missing, a value of the wrong type (a string where a list belongs, a limit over 100) or a value outside a list of choices is refused with 422, naming the field:
{"error": {"code": "invalid", "message": "#/labels: Invalid array. Got: string"}}
Fields the description does not name are ignored.
Keys are snake_case. A field with no value is null, and a list with nothing in it is []: fields are never left out of an answer.
Naming things
The API takes the names people use, not internal ids:
| Thing | Named by | Example |
|---|---|---|
| Project | Its key | WEB |
| Ticket | Its key | WEB-142 |
| Board | Its number | 3 |
| Ticket type, priority, resolution, component | Its name | Bug, High, Won't Do, Billing |
| Transition | Its name, its id, or the name of the status it leads to | Start progress, In Progress |
| Board column | Its name or its index from 0 | Doing, 1 |
| Sprint | Its name or its id | Sprint 14 |
| Person | me, their display name or their id; an assignee by email address too |
me, Ada Lovelace |
| Label | Its name, lowercased, with spaces as hyphens | needs-design |
Names match without regard to case or surrounding spaces. A name that matches nothing is refused with 422, and the message lists what would have matched (up to 25), so a program or a model can correct itself:
{
"error": {
"code": "invalid",
"message": "Type \"Bugg\" matches nothing; one of: Task, Bug, Story, Epic, Sub-task."
}
}
People are given as {"id": …, "name": …} and never with an email address. An agent acting for a member finds people as that member would in a form: the search filters offer the workspace's members and customers by name, and an assignee may also be named by email, since GET /projects/{key} lists the people a ticket in the project can be assigned to. A ticket can only be assigned to one of them.
Ids are UUIDs. Keys never change and are never reused, so they are safe to store.
Dates and times
Times are ISO 8601 in UTC with microseconds, such as "2026-09-23T14:05:31.412008Z". Dates without a time are "YYYY-MM-DD", such as a due date "2026-10-01". Sprint dates may be given either way; a date alone means midnight UTC.
Rich text
Descriptions and comments are returned with the format they were written in:
"description": {"format": "markdown", "text": "Steps:\n\n1. Open checkout\n2. …"}
The format is markdown, jira_wiki (imported from Jira), html or plain. Imported text is never converted in bulk. A jira_wiki description also carries markdown, the Markdown the edit form would start from:
"description": {
"format": "jira_wiki",
"text": "h2. Steps\n# Open checkout",
"markdown": "## Steps\n\n1. Open checkout"
}
Everything you write is Markdown with GitHub's extensions (tables, task lists, strikethrough, autolinks). Replacing a jira_wiki description stores Markdown, and the ticket's history keeps the original, as it does when a person edits it. To show a body as HTML, render it yourself or open the ticket's url: the API returns no HTML.
Paging
Lists that can be long (GET /tickets) take limit (1 to 100, default 20) and offset, and return total:
{"total": 213, "limit": 20, "offset": 40, "tickets": […]}
Ask again with offset increased by limit until you have total. Results are ordered, so a page follows on from the last unless tickets change between requests. With a query, the search engine finds at most 250 candidates, so total stops there however many tickets contain the words.
A board's columns and a backlog return at most 500 tickets each. The other lists (projects, boards, sprints, comments, transitions) are returned whole.
Status codes
| Status | Meaning |
|---|---|
| 200 | Done. The answer is the record as it is now. |
| 201 | Created: a ticket, a comment or a sprint. |
| 401 | No token, or one that is unknown, expired or revoked. See Authentication. |
| 403 | The member may not do that, or the token is read-only, or the API, or the module the request is for, is switched off for the workspace. |
| 404 | No such project, ticket or board, or not one the member can see. |
| 409 | A sprint is in the wrong state for that: starting one while another runs, or completing one that is not active. |
| 422 | The request does not make sense: a missing field, a name that matches nothing, a value the workflow refuses. |
| 429 | Too many requests. Wait and retry. |
A ticket that exists but is hidden from the member by a security level answers 403, not 404.
Errors
Every error from /api/v1 has the same shape: a short code for programs and a sentence for people.
{"error": {"code": "forbidden", "message": "You do not have permission to do that."}}
| Code | Status | |
|---|---|---|
invalid_token |
401 | See above |
feature_disabled |
403 | An administrator has the REST API switched off for the workspace, or the module or feature the request is for (boards, sprints, the helpdesk, the knowledge base, time, the CRM); the message says which |
forbidden |
403 | The member's permissions, or the token's access, do not allow it |
not_found |
404 | |
invalid |
422 | A field is missing or wrong; the message names it |
sprint_active |
409 | The board already has an active sprint |
wrong_state |
409 | The sprint is not in a state that allows it |
rate_limited |
429 | |
| others | 422 | A rule of the workspace, such as not_assignable, no_transition, no_such_column, no_such_sprint, transition_not_available, status_not_in_workflow, ticket_type_not_allowed, parent_not_found |
Show message to people; branch on code. New codes may appear, so treat an unknown one by its status.
The OAuth endpoints use OAuth's own error shape instead; see token endpoint errors.
Rate limits
Each token may make 600 requests a minute on each application node. Past that, the answer is 429 with Retry-After: 60. A workspace served by several nodes may allow somewhat more in total; do not rely on it. The limit is there to stop a runaway loop, and a program that pauses for Retry-After never needs to think about it.
The OAuth token endpoint allows 60 requests a minute and registration 30 an hour from one address.
Browsers and CORS
/api, /mcp, /oauth/register, /oauth/token, /oauth/revoke and /.well-known/… answer requests from any origin, preflights included (Authorization, Content-Type, Mcp-Protocol-Version and Mcp-Session-Id are allowed; WWW-Authenticate is exposed). This is safe because they read no cookie: a page elsewhere can do nothing a script with the same token could not. It also means that anything running in a browser holds its token where the page's own scripts can read it; prefer OAuth there, with read access where that is enough.
No response from these paths sets a cookie.
History and notifications
A change made through the API is a change like any other. It is recorded in the ticket's history in the same transaction, under the member's name with the token's name beside it, shows up at once on every open page, is indexed for search and is announced to the ticket's watchers (except the member). There is nothing to call to "save".
There are no idempotency keys: sending a create twice creates two tickets. If a request fails without an answer, search for what you tried to create before trying again.
Versions
The version is part of the path: /api/v1. Within v1, fields and endpoints may be added and new error codes may appear, so ignore fields you do not know. Anything that would break a program written against v1 would come as v2, with v1 kept alongside it.
The OpenAPI 3 description at /api/v1/openapi.json on your workspace (no token needed) describes every endpoint, its arguments and the exact shape of every answer. It is not written by hand: it is built from the same declarations that check each request, and the test suite checks every answer against it, so it cannot drift from what the API does. It names your workspace as the server and its OAuth endpoints, so it can be imported as it is into an API client or an agent tool.
The API reference is built from the same description. On your workspace, choose Authorize, paste a personal token, and try any request. Requests act as you and changes are real, so try them in a project where that does not matter. On albaticket.com itself the reference only reads, since that site cannot send requests to your workspace.