Yarnt Start free

Yarnt API Reference

A REST API for working with tickets, projects, comments, and history.

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:

StatusMeaning
400A required field is missing or invalid (e.g. no title when creating a ticket), or a plan limit rejected the write.
401Missing, invalid, or revoked API key.
404The ticket, project, or member referenced doesn't exist.
500The request was fine and the server failed anyway. Retry.

Discovery

RequestReturns
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 paramValues
listqueue (default) | now | next | wishlist | finished | all
assigneeme | unassigned | a user id (see GET /v1/users)
projectA 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 fieldType
titlestring, required
projectstring, optional. A project name.
wishboolean, 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 fieldType
titlestring, optional
bodystring, optional
assigneea user id to set, null to unassign, omit to leave unchanged
projectstring (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 paramValues
includeArchivedtrue 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.