Motir

Getting started

From an account to a working call, in five steps. Everything below uses the same token you mint in step 1.

1Mint a token

Every call is authenticated with a personal access token. Mint one in Settings → Account → Tokens, choose the workspace it is bound to, and grant it the permissions it needs.

A token GRANTS permissions — the same resource:action names the Roles & permissions screen shows: project:browse for every read in this guide, work_item:edit to create or change work items, comment:add to comment, sprint:manage for sprint lifecycle and membership, work_item:archive to archive or restore, work_item:delete for the irreversible subtree delete, and ai:plan for a planning submit that spends your AI credits. Grant the narrowest set that does the job — a grant narrows your own role and never widens it, so a token cannot do something you could not.

The secret is shown ONCE, when the token is created. Copy it then; there is no way to read it again, and a lost token is replaced rather than recovered.

2Your first authenticated call

Make this call first. It answers who the token is, which workspace it is bound to, and exactly which permissions it carries — so you learn what your own credential may do without probing endpoints and collecting 403s.

curl
curl https://app.motir.co/api/v1/me \
  -H "Authorization: Bearer motir_pat_<your-token>"
200 · application/json
{
  "user": { "id": "usr_…", "name": "Ada", "email": "ada@example.com" },
  "workspaceId": "wsp_…",
  "permissions": ["project:browse"]
}

A missing, malformed, unknown, revoked or expired token all return the same 401 with the same message. That is deliberate: distinguishing them would turn the endpoint into an oracle that answers “does this secret exist?”.

3Paginate a collection

Collections are cursor-paged. Ask for a page size with limit (the default is 50 and anything larger is clamped to 100, not rejected), then send the previous response’s nextCursor back as cursor. A nextCursor of null is the last page.

curl · the first page
curl "https://app.motir.co/api/v1/projects/MOTIR/work-items?limit=2" \
  -H "Authorization: Bearer motir_pat_<your-token>"
200 · application/json
{
  "items": [ { "key": "MOTIR-1", … }, { "key": "MOTIR-2", … } ],
  "nextCursor": "eyJjIjoid29ya0l0ZW1zIiwicCI6…"
}
curl · the next page
curl "https://app.motir.co/api/v1/projects/MOTIR/work-items?limit=2&cursor=$CURSOR" \
  -H "Authorization: Bearer motir_pat_<your-token>"
The cursor is OPAQUE and signed. Do not parse it, construct one, or carry it between collections — a cursor issued elsewhere is a 422, never a silently wrong page. Send back exactly what you were given.

One asymmetry surprises people, so it is worth knowing before you meet it: some collections also report a totalCount and most deliberately do not. The backlog, a sprint’s members and a work item’s comments carry one, because the read behind them already computes it as a bounded aggregate. Projects, sprints, workspaces and the ready set omit the field ENTIRELY — absent, never null and never 0, so a client can always tell “no total was promised” from “the total is zero”.

4Read an error

Every failure returns the same body: a machine code and a human error. Branch on code — it is stable, and changing one is a breaking change. Never parse error; it is a sentence for a developer reading a terminal and is reworded freely.

404 · application/json
{ "code": "WORK_ITEM_NOT_FOUND", "error": "Work item not found." }

A 404 means the resource does not exist or is outside the workspace your token is bound to — the same answer on purpose, so the API cannot be used to enumerate another tenant’s data. A 403 means the opposite kind of refusal: your token is valid, and its grant lacks the permission this operation requires — the response names the key. A 422 is a request you can fix, and its code names which part.

A 500 is the one failure with NO code. An unexpected fault has no stable contract, so the body carries a message and nothing else — do not branch on it.

5Read the response headers

The budget is per TOKEN, and the headers ride EVERY response — a success, a 403, a mapped error and a 500 alike. You never have to make a request to find out where you stand; the last one already told you.

response headers
X-RateLimit-Limit:     600
X-RateLimit-Remaining: 594
X-RateLimit-Reset:     1785312000
X-Request-Id:          c7771231-e18c-48bc-90c9-c1a9720436a4
X-Motir-Api-Version:   1.22.0
On a 429, back off until X-RateLimit-Reset — a Unix timestamp in SECONDS. There is no Retry-After header, deliberately: an absolute instant cannot go stale in transit the way a relative duration can.

X-Request-Id is on every response too. Quote it if you ever need to ask us about a specific call — it is the one identifier that finds it.

X-Motir-Api-Version is the version of the CONTRACT that served the response — the same MAJOR.MINOR.PATCH as the specification’s info.version, not our release number. Read it off any response, including a failure, to check for version skew: you never have to fetch the specification just to learn which contract you are talking to. A MAJOR you do not recognise means a /api/v2 exists; a higher MINOR means the contract grew, additively, and your client is still correct.

Next: API reference · Stability & deprecation