Yarnt API Reference
The Yarnt REST API is the HTTP twin of the MCP server: the same operations and permission model, over plain JSON instead of a tool-call protocol. Reach for it from a script, a webhook, or a CI job where an MCP client doesn't fit.
Base URL: https://api.getyarnt.com. Every endpoint below
hangs off it, e.g. https://api.getyarnt.com/v1/tickets.
Authentication
Every /v1/* request needs an Authorization header carrying an
API key:
Authorization: Bearer yk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Mint a key in Yarnt under Settings → API keys and bind it to a robot: a workspace member representing the script or service using the key. The key acts as that robot: it can see and do exactly what that robot's membership permits, nothing more. Revoke a key at any time from the same settings page; revoked or unknown keys are rejected immediately.
A missing, malformed, invalid, or revoked key gets a 401 with
{ "error": "..." }.
Errors
Non-2xx responses are always { "error": "<message>" }. The status
code tells you what kind of problem it is:
| Status | Meaning |
|---|---|
400 | A required field is missing or invalid (e.g. no title when creating a ticket), or a plan limit rejected the write. |
401 | Missing, invalid, or revoked API key. |
404 | The ticket, project, or member referenced doesn't exist. |
500 | The request was fine and the server failed anyway. Retry. |
Discovery
| Request | Returns |
|---|---|
GET / (no key needed) | A short service description and a pointer to these docs. |
GET /v1 (with a key) | A machine-readable index of every route below, one line each. |
GET /health (no key needed) | { "status": "ok" }, or 503 with "degraded" when the database is unreachable. |
Endpoints
GET /v1/tickets POST /v1/tickets GET /v1/tickets/:id PATCH /v1/tickets/:id POST /v1/tickets/:id/start|finish|drop|unstart GET/POST /v1/tickets/:id/comments GET /v1/tickets/:id/history GET/POST /v1/tickets/:id/attachments GET/DELETE /v1/attachments/:id POST /v1/tickets/:id/uploads GET /v1/users GET /v1/whoami GET /v1/projects
GET/v1/tickets
List tickets in the workspace, filtered by list, assignee, or project.
| Query param | Values |
|---|---|
list | queue (default) | now | next | wishlist | finished | all |
assignee | me | unassigned | a user id (see GET /v1/users) |
project | A project name. |
now and next are the two sublists of the queue:
now is what's in flight, next is what's waiting. all
is every list at once.
Returns. Each ticket reports the list it's on. assignee is a user id (or null), which you can resolve to a name via GET /v1/users:
{ "tickets": [ { "id": "...", "title": "...", "list": "next", "assignee": "dcba4321-..." } ] }
POST/v1/tickets
Create a ticket. title is required.
| Body field | Type |
|---|---|
title | string, required |
project | string, optional. A project name. |
wish | boolean, optional. Files it as a wish instead of the queue. |
201: { "id": "...", "wish": false }
GET/v1/tickets/:id
Full detail for one ticket:
{
"id": "...", "title": "...", "list": "now", "assignee": "dcba4321-...",
"project": "...", "labels": ["..."], "blocked": false, "blockedReason": null,
"createdAt": "...", "updatedAt": "...", "body": "..."
}
PATCH/v1/tickets/:id
Edit any subset of a ticket's fields.
| Body field | Type |
|---|---|
title | string, optional |
body | string, optional |
assignee | a user id to set, null to unassign, omit to leave unchanged |
project | string (name) to set, null to clear, omit to leave unchanged |
Returns { "id": "..." }.
POST/v1/tickets/:id/start
POST/v1/tickets/:id/finish
POST/v1/tickets/:id/drop
POST/v1/tickets/:id/unstart
Move a ticket through its lifecycle. No body. Each returns { "id": "..." }.
unstart is the reverse of start: the ticket returns to state
todo at the top of Next, with blocked and its reason cleared. A
ticket that is not started is left alone.
GET/v1/tickets/:id/comments
Returns { "comments": [ { "createdAt": "...", "authorId": "...", "body": "..." } ] }.
POST/v1/tickets/:id/comments
Body: { "body": "..." }, required. 201 on success.
GET/v1/tickets/:id/history
Returns { "history": [ { "createdAt": "...", "kind": "...", "fromVal": "...", "toVal": "..." } ] }.
GET/v1/tickets/:id/attachments
Returns { "attachments": [ { "id": "...", "name": "...", "size": 1234,
"contentType": "...", "createdAt": "...", "createdBy": "...", "url": "..." } ] },
newest first.
The bucket is private, so url is a signed download link. Fetch the bytes from
it directly. It expires one hour after the response, and a fresh one comes
back on every read, so store the attachment id, never the URL.
POST/v1/tickets/:id/attachments
Body: { "name": "build.log", "data": "<base64>", "contentType": "text/plain" }.
name and data are required; contentType defaults to
application/octet-stream. 201 with the created attachment.
Your plan's caps apply exactly as they do in the app: per-file size, files per ticket, and total workspace storage. A rejected upload leaves nothing behind.
This route carries the file in the JSON body, so it suits small files. For anything larger — and for anything an agent produced — use the two-step upload below and send the bytes straight to storage instead.
POST/v1/tickets/:id/uploads
Body: { "name": "render.png", "contentType": "image/png" }; name is
required. Returns 201 with { "upload": "<handle>", "url": "...",
"expiresIn": 7200, "contentType": "image/png" }. PUT the file's bytes to
url — nothing is recorded until you finish the upload, and an upload you never
finish attaches nothing.
curl -X PUT -H "Content-Type: image/png" --upload-file render.png "$url"
The returned contentType is what the attachment will be stored as: the one you
asked for, or one derived from the filename if you left it out. That is what gets recorded,
not whatever your PUT declared — curl --upload-file sends
application/octet-stream on its own, and an image saved under that type is one
no client will display inline. Send the header anyway so storage and the record agree.
POST/v1/attachments
Body: { "upload": "<handle>" }. Records the file you uploaded and returns
201 with the attachment. The size is read back from storage rather than taken
from you, so your plan's caps are measured against the bytes that really arrived; if a cap
rejects it, the uploaded object is removed.
GET/v1/attachments/:id
One attachment's metadata plus a freshly signed url. The API does not serve the
bytes itself. That URL does.
DELETE/v1/attachments/:id
Removes the attachment and its stored bytes, returning { "id": "...", "name": "..." }.
An id that is not yours, or not there, is a 404. There is no undo — the file is
gone, and re-attaching means uploading it again.
GET/v1/users
List the workspace members you can assign tickets to. Use a member's id as the
assignee value elsewhere. kind is person for people and
integration for bots and service accounts.
{ "users": [ { "id": "dcba4321-...", "name": "Ada", "kind": "person" },
{ "id": "abcd1234-...", "name": "Claude", "kind": "integration" } ] }
GET/v1/whoami
Your own membership row, plus the person who granted you access. Use the returned
id as the assignee value elsewhere instead of guessing which
GET /v1/users row is you.
{ "id": "abcd1234-...", "name": "Claude", "kind": "integration",
"human": { "id": "dcba4321-...", "name": "Ada" } }
GET/v1/projects
List projects in the workspace.
| Query param | Values |
|---|---|
includeArchived | true to include archived projects (default: excluded) |
Returns { "projects": [ { "name": "...", "archived": false } ] }.
Examples
Create a ticket
curl -s https://api.getyarnt.com/v1/tickets \
-H "Authorization: Bearer yk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"title": "Fix the flaky deploy step", "project": "infra"}'
{ "id": "1234abcd...", "wish": false }
List your in-flight tickets
curl -s "https://api.getyarnt.com/v1/tickets?list=now&assignee=me" \
-H "Authorization: Bearer yk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{ "tickets": [ { "id": "1234abcd...", "title": "Fix the flaky deploy step", "list": "now", "assignee": "abcd1234-..." } ] }
Questions about the API: help@getyarnt.com.