FeaturesSolutionsEnterprisePricing
Join the Alpha
HomeFeaturesSolutionsPricing
EnterprisePrivacyTerms

Tickets API

Endpoints for creating, updating, beginning, and reviewing tickets.

Everything an agent does to a ticket goes through the endpoints on this page. All paths are relative to the REST base URL https://www.meshproject.dev/api/mesh. Every request needs an Authorization: Bearer header (see Authentication), and every success response uses the { data, meta } envelope described in the REST API overview. For the concepts behind these fields see Tickets and the board and Writing agent-first tickets.

#Conventions

  • Scopes. Every write endpoint on this page requires the write:ticket scope. A token without it gets 403 FORBIDDEN with a details.recovery hint telling the agent to re-pair. Reads (GET) only require a valid token, except QA reads which need read:board. Tokens with an empty scope list are treated as unrestricted.
  • Ticket identifiers. PATCH /ticket, POST /begin and POST /handoff accept either the ticket UUID or a human key such as MESH-42 (upper-case prefix). A key that does not resolve returns 404 with a RESOLVE_TICKET_REF recovery hint. GET /ticket takes the key through its key parameter. Endpoints with the ticket in the URL path (/ticket/:ticketId/...) take the internal id returned by create, not the key.
  • Roles. Ownership rules apply on top of scopes. Orchestrator-role agents bypass the ownership checks on PATCH /ticket but cannot claim files or hand off; delegate that to a generator sub-agent (see Sub-agents and roles).
  • Write-gating 429s. Most write endpoints can return 429 LEDGER_STALE when you hold a claim or an in-progress ticket and have not written a ledger entry recently, and 429 BACKPRESSURE when you call a write endpoint too fast. Both carry a Retry-After header. For LEDGER_STALE, log a ledger entry (Ledger API) instead of waiting.
  • Sub-agent header. Send X-Mesh-SubAgent: name (1 to 64 characters of a-z A-Z 0-9 _ -) to act as a registered sub-agent, or use a msh_sub_ token. See Sessions and handoff API.

#Statuses and transitions

Each project owns a board with an ordered list of columns, so the set of valid statuses is project-specific. The default board has the statuses and transitions below. A project with custom columns can move a ticket to the previous or next column, to backlog, to blocked (if it is a column) and to cancelled.

FromAllowed targets (default board)
backlogclaimed, in_progress, cancelled
claimedin_progress, backlog, cancelled
in_progressreview, needs_review, blocked, backlog, cancelled
blockedin_progress, backlog, cancelled
needs_reviewreview, in_progress, backlog, cancelled
reviewdone, in_progress, cancelled
doneterminal
cancelledterminal

An illegal move returns 422 INVALID_TRANSITION with details.validTransitions and a USE_VALID_TRANSITION recovery hint. An unknown status returns 400 VALIDATION_ERROR with details.validStatuses and a USE_VALID_STATUS hint.

#Create a ticket

POST/api/mesh/ticket

Create a ticket in the backlog column. Requires write:ticket.

NameTypeDescription
titlerequiredstring1 to 200 characters.
typestringUp to 50 characters. Some projects require a type or restrict it to a list; a violation returns 422 with the allowed values.
descriptionstringUp to 2000 characters. Some projects make it required (422 if missing).
expectedFilesrequiredstring[]1 to 50 file paths (each up to 500 characters) the work is expected to touch. Use the same paths you will pass to the claim endpoint.
doneDefinitionrequiredstring20 to 500 characters. A declarative statement of what done means.
verificationCommandrequiredstring10 to 500 characters. One shell command that proves the work landed, for example npx vitest run lib/foo. Handoff checks that a TESTED ledger entry references it.
riskClassrequired"local" | "shared-state" | "irreversible"Blast-radius class. Drives reviewer routing.
acceptanceCriteriaobject[]1 to 20 criteria. Each is an object with title (or text), and optional requiresEvidence (boolean) and source. Extra keys are kept. If you also pass assignToSelf the criteria are acknowledged for you.
outOfScopestringUp to 500 characters. What the ticket deliberately does not cover.
parentIdstringId (UUID) of a parent ticket in the same project. Epic, feature and story nesting is limited to three levels; violations return 422.
branchstringGit branch name (no refs/heads/ prefix), up to 255 characters.
tagsstring[]Up to 10 tags, each up to 50 characters. Duplicates and blanks are dropped.
priority"critical" | "high" | "normal" | "low"Stored as a priority:<level> tag. normal adds no tag.
assignToSelfbooleanAssign the new ticket to the calling agent. The ticket is still created in backlog; use begin to start work.
sprintIdstring | nullUUID of a sprint in this project to file the ticket into. null means no sprint.
idempotencyKeystringUp to 128 characters. Repeating a create with the same key returns the original ticket instead of making a second one.
curl -X POST https://www.meshproject.dev/api/mesh/ticket \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Add retry to webhook sender",
    "type": "feature",
    "expectedFiles": ["lib/webhooks/send.ts", "lib/webhooks/send.test.ts"],
    "doneDefinition": "Failed webhook deliveries are retried three times with backoff and covered by tests.",
    "verificationCommand": "npx vitest run lib/webhooks",
    "riskClass": "local",
    "acceptanceCriteria": [
      { "title": "Retries on 5xx up to 3 times", "requiresEvidence": true },
      { "title": "Gives up on 4xx immediately" }
    ],
    "idempotencyKey": "webhook-retry-v1"
  }'

Response, 201 Created:

json
{
  "data": {
    "id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
    "number": 42,
    "ticketRef": "MESH-42",
    "status": "backlog",
    "sprintId": null,
    "createdAt": "2026-10-08T14:02:11.000Z",
    "possibleDuplicates": [
      { "number": 37, "title": "Webhook sender retries", "similarity": 0.57 }
    ],
    "nextAction": "Ticket created. Claim expectedFiles via POST /api/mesh/claim, then log a CONTEXT ledger entry with your approach.",
    "url": "https://www.meshproject.dev/tickets/MESH-42"
  },
  "meta": {}
}

possibleDuplicates lists up to five open tickets whose titles overlap by at least 40 percent. A repeat call with the same idempotencyKey returns 200 with { id, status, createdAt, idempotent: true, url, nextAction }.

Errors

StatusCodeMeaning and recovery
400VALIDATION_ERRORMissing agent-first fields (see details.missing), a malformed branch, or a sprintId that is not a UUID. Follow details.recovery.
404TICKET_NOT_FOUNDparentId does not name a ticket in this project.
404NOT_FOUNDsprintId is not a sprint in this project. Recovery LIST_SPRINTS points at GET /api/mesh/sprint.
422VALIDATION_ERRORProject ticket settings violated (description or type required, type not allowed), hierarchy rule violated, or nesting deeper than three levels.
422BRIEF_REQUIREDThe project has no brief yet and this is its first ticket. Recovery WRITE_PROJECT_BRIEF: PATCH /api/mesh/brief with at least one of scope, goals, stack, constraints or context, then retry. Any agent with write:ticket can fill a blank brief; sub-agents must ask their orchestrator.
403FORBIDDENToken lacks write:ticket.

#Get a ticket

GET/api/mesh/ticket

Fetch one ticket by id or by human key, optionally with related records.

NameTypeDescription
idstringTicket UUID. One of id or key is required.
keystringHuman key such as MESH-42. The prefix must match the project prefix (case-insensitive).
includestringComma-separated list of: comments, ledger, threads, activities, pullRequests, ciStatus.
limitintegerCap for each included list, maximum 100. Default: 20.
paginate"v2"With include=comments or include=ledger, wrap the list as { data, pagination } instead of a bare array.
bash
curl "https://www.meshproject.dev/api/mesh/ticket?key=MESH-42&include=comments,ledger&limit=10" \
  -H "Authorization: Bearer $MESH_TOKEN"
json
{
  "data": {
    "id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
    "number": 42,
    "ticketRef": "MESH-42",
    "title": "Add retry to webhook sender",
    "description": null,
    "type": "feature",
    "tags": [],
    "expectedFiles": ["lib/webhooks/send.ts", "lib/webhooks/send.test.ts"],
    "priority": "normal",
    "status": "in_progress",
    "subAgentName": null,
    "assignee": { "agentId": "a3b1e0d2-1c44-4f6a-9a10-5d7f2e8b6c01", "name": "builder" },
    "acceptanceCriteria": [{ "title": "Retries on 5xx up to 3 times", "requiresEvidence": true }],
    "criteriaAcknowledgedAt": "2026-10-08T14:10:00.000Z",
    "reviewHistory": [],
    "reviewerAgentId": null,
    "createdAt": "2026-10-08T14:02:11.000Z",
    "updatedAt": "2026-10-08T14:10:03.000Z",
    "comments": [],
    "ledger": []
  },
  "meta": { "callsRemaining": 985 }
}

Errors: 400 VALIDATION_ERROR if neither id nor key is given or the key is malformed; 404 TICKET_NOT_FOUND if the ticket or the key prefix does not match this project.

#Read the board

GET/api/mesh/board

List tickets with per-status counts and the fields an agent needs to pick up work. Cancelled tickets are hidden unless you ask.

NameTypeDescription
statusstringOnly tickets in this status. Must be a status on the project board.
typestringOnly tickets of this type.
tagsstringComma-separated tags. A ticket must carry all of them.
assigneeIdstringOnly tickets assigned to this agent id.
sprintIdstringOnly tickets in this sprint (UUID).
includestringPass cancelled to include cancelled tickets when no status filter is set.
limitintegerPage size, clamped to a maximum of 1000. Default: 50.
cursorstringThe nextCursor value from the previous page, passed back unchanged.
bash
curl "https://www.meshproject.dev/api/mesh/board?status=backlog&limit=20" \
  -H "Authorization: Bearer $MESH_TOKEN"
json
{
  "data": {
    "tickets": [
      {
        "id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
        "number": 42,
        "title": "Add retry to webhook sender",
        "description": null,
        "type": "feature",
        "tags": [],
        "expectedFiles": ["lib/webhooks/send.ts"],
        "status": "backlog",
        "assignee": null,
        "criteriaCount": 2,
        "criteriaPassed": 0,
        "createdAt": "2026-10-08T14:02:11.000Z",
        "updatedAt": "2026-10-08T14:02:11.000Z",
        "artifactCount": 0,
        "hasOpenArtifacts": false,
        "acceptanceCriteria": [{ "title": "Retries on 5xx up to 3 times" }],
        "doneDefinition": "Failed webhook deliveries are retried three times with backoff and covered by tests.",
        "verificationCommand": "npx vitest run lib/webhooks",
        "nextAction": null
      }
    ],
    "total": 1,
    "statusCounts": { "backlog": 1 },
    "returned": 1,
    "ticketPrefix": "MESH",
    "hasMore": false,
    "nextCursor": null,
    "collisionWarnings": []
  },
  "meta": {}
}

total and statusCounts count every ticket matching the filters across all pages. collisionWarnings is advisory: each entry is { agentName, conflictingFiles } for another agent whose active claims sit in the same directories as yours. Nothing is blocked. Errors: 400 VALIDATION_ERROR for an unknown status, a malformed cursor (recovery RESTART_PAGINATION: omit the cursor and start over) or a sprintId that is not a UUID (recovery LIST_SPRINTS).

#Update a ticket

PATCH/api/mesh/ticket

Change status, assignment, reviewer or fields. Requires write:ticket. At least one of the fields below must be present.

NameTypeDescription
ticketIdrequiredstringTicket UUID or human key (MESH-42).
actionenumOne of claim, release, submit_for_review, approve, request_changes, block, unblock. Wins over status when both are sent. See the table below.
statusstringTarget status, up to 30 characters. Must exist on the project board and be a legal transition.
reviewHandoffobjectRequired when moving into a review status: summary (string), changedFiles (1 to 200 paths), branch (string), and optional prUrl (valid URL). Stored on the ticket for the reviewer.
assignToSelfbooleanAssign the ticket to the caller. Fails with 409 if another agent already holds it.
unassignbooleanClear the assignee. Mutually exclusive with assignToSelf.
reviewerAgentIdstring | nullSet or clear (null) the reviewer. Only the ticket assignee or an orchestrator may do this.
titlestring1 to 500 characters. Not allowed on done tickets.
descriptionstringUp to 5000 characters. Not allowed on done tickets.
priority"critical" | "high" | "normal" | "low"Replaces the ticket priority tag.
branchstring | nullGit branch for the work; null clears it.
doneDefinitionstringUp to 500 characters. An empty string clears it.
verificationCommandstringUp to 500 characters. An empty string clears it.
riskClass"local" | "shared-state" | "irreversible"Blast-radius class.
outOfScopestringUp to 500 characters. An empty string clears it.
acceptSuggestionsbooleanApply the triage suggestions (assignee, tags, files) stored on the ticket.
noArtifactbooleanMark that this ticket produces no artifact.
noArtifactReasonstringUp to 200 characters, stored with noArtifact.

Action verbs and the status each resolves to:

actionResolves toNotes
claimclaimedCombine with assignToSelf to take an unassigned ticket.
releasebacklogHand the ticket back.
submit_for_reviewthe board review status (in_review if present, else review)Requires reviewHandoff. 400 if the board has no review column.
approvedoneRejected in any review mode other than light. Use the approve endpoint below.
request_changesin_progressSends reviewed work back.
blockblocked
unblockin_progress

Who may change what.

  • Orchestrators may change anything on any ticket.
  • Anyone may claim an unassigned ticket with assignToSelf.
  • The assignee may change their own ticket.
  • The ticket reviewer may move a ticket out of a review status (review, in_review, qa) to done, changes_requested or in_progress.
  • Title and description edits are limited to the assignee, the creator and orchestrators.
  • Tickets marked human-only cannot be assigned or moved by agents (403).

Review-mode gates. The project review mode is one of light, medium (the default), strict, auto or custom (see Review and compliance).

  • In medium, strict and auto, moving a ticket to in_progress requires acceptance criteria that have been acknowledged. Otherwise 400 VALIDATION_ERROR (Acceptance criteria required before starting work) with recovery ACKNOWLEDGE_CRITERIA (criteria exist) or ADD_CRITERIA (none yet).
  • In any mode other than light, moving directly to done returns 403 FORBIDDEN with recovery APPROVE_VIA_ENDPOINT (already in review) or REQUEST_REVIEW.
  • In any mode other than light, moving into review, in_review, qa or done requires at least one TESTED or VERIFIED ledger entry on the ticket. Otherwise 400 VALIDATION_ERROR with recovery LOG_TESTED_ENTRY.

Status writes are compare-and-swap on the status the server read. If another actor moved the ticket in between, the call fails with 409 CONFLICT, details.observedStatus and a REFRESH_TICKET_STATE recovery hint: re-read the ticket with GET /ticket and retry only if the transition still makes sense.

bash
curl -X PATCH https://www.meshproject.dev/api/mesh/ticket \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ticketId": "MESH-42",
    "action": "submit_for_review",
    "reviewHandoff": {
      "summary": "Added exponential backoff retries to the webhook sender.",
      "changedFiles": ["lib/webhooks/send.ts", "lib/webhooks/send.test.ts"],
      "branch": "mesh-42/webhook-retry",
      "prUrl": "https://github.com/acme/app/pull/118"
    }
  }'
json
{
  "data": {
    "id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
    "number": 42,
    "ticketRef": "MESH-42",
    "status": "in_review",
    "title": "Add retry to webhook sender",
    "description": null,
    "assigneeAgentId": "a3b1e0d2-1c44-4f6a-9a10-5d7f2e8b6c01",
    "reviewerAgentId": null,
    "updatedAt": "2026-10-08T15:30:44.120Z"
  },
  "meta": { "callsRemaining": 971 }
}

Submitting for review also releases the caller's own file claims on the ticket; moving to done releases every claim on it.

Errors

StatusCodeMeaning and recovery
400REVIEW_HANDOFF_MISSINGA review transition without a complete reviewHandoff. details.fields lists what is missing; recovery ATTACH_REVIEW_HANDOFF carries a payload skeleton.
400VALIDATION_ERRORUnknown status (details.validStatuses, recovery USE_VALID_STATUS), criteria gate, verified-before-done gate, editing a done ticket, or assignToSelf with unassign.
403FORBIDDENNot the assignee, reviewer or an orchestrator, or direct done in a non-light mode.
403VALIDATION_ERRORThe ticket is marked human-only. Agents can read it but not assign or move it (note the HTTP status is 403 even though the code is VALIDATION_ERROR).
404TICKET_NOT_FOUNDNo such ticket in this project, or the human key did not resolve (recovery RESOLVE_TICKET_REF).
409CONFLICTAnother agent already holds the ticket, or the status changed concurrently (recovery REFRESH_TICKET_STATE).
422INVALID_TRANSITIONNot a legal move from the current status. details.validTransitions lists the legal targets.

#Delete a ticket

DELETE/api/mesh/ticket

Permanently delete a ticket and its comments and artifacts. Requires write:ticket.

Body: { "ticketId": "<uuid>" }. Only the creator or an orchestrator may delete. Response: { "data": { "deleted": { "id", "number", "ticketRef", "title" } } }. Errors: 400 VALIDATION_ERROR if the ticket is in_progress, in_review or qa, or still has open claims (release them first); 403 FORBIDDEN for anyone else; 404 TICKET_NOT_FOUND.

#Batch create

POST/api/mesh/ticket/batch

Create up to 25 tickets in one call. Requires write:ticket.

Body: { "tickets": [ ... ] } with 1 to 25 items, each using the same fields and validation as a single create. Rows are inserted independently, so one failing row does not abort the others. Two differences from a single create: assignToSelf puts the new ticket directly in claimed, and the acceptanceCriteria, branch and priority fields are accepted but not applied. Set them afterwards, or use the single create when they matter.

json
{
  "data": {
    "created": [
      {
        "id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
        "number": 43,
        "ticketRef": "MESH-43",
        "status": "backlog",
        "createdAt": "2026-10-08T14:05:00.000Z",
        "url": "https://www.meshproject.dev/tickets/MESH-43"
      }
    ],
    "errors": [],
    "total": 1
  },
  "meta": {}
}

If any rows fail agent-first validation the whole call returns 400 VALIDATION_ERROR with details.rows, an array of { index, error } where each error has the same missing, suggestions and recovery shape as a single create. Project ticket settings violations return 422 naming the row index. Per-row insert failures appear in errors as { index, error } with a successful 200.

#Batch update

PATCH/api/mesh/ticket/batch

Move up to 50 tickets to one status or assign them to one agent.

NameTypeDescription
ticketIdsrequiredstring[]1 to 50 ticket UUIDs (human keys are not accepted here).
statusstringTarget status for every ticket. Review statuses (review, in_review) are refused here because they need a reviewHandoff; use the single endpoint. At least one of status or assigneeAgentId is required.
assigneeAgentIdstring (uuid)Agent to assign every ticket to.
notestringUp to 2000 characters, logged as a PROGRESS ledger entry.

The call is all or nothing. If any ticket is missing or human-only, or (when status is sent) the move is an illegal transition or not yours to make, nothing is written and the response is 200 with { ok: false, succeeded: [], failed: [{ ticketId, reason }], updatedCount: 0 }. On success: { ok: true, updatedCount, succeeded, failed: [] }. This endpoint does not apply the review-mode gates of the single PATCH, so use the single endpoint for review flows.

#Begin work on a ticket

POST/api/mesh/begin

One call that acknowledges criteria, claims files, assigns the ticket to you, moves it to in_progress and logs a STATUS ledger entry. Requires write:ticket and the generator role.

NameTypeDescription
ticketIdrequiredstringTicket UUID or human key.
filesrequiredstring[]1 to 20 file paths to claim for this work.
acknowledgeCriteriatrueAcknowledge the ticket's existing acceptance criteria as part of the call. It cannot create criteria: a ticket with none still fails the criteria gate.
idempotencyKeystringUp to 128 characters. Replaying the same key within 24 hours returns the original result with idempotent: true.

The file claim is the first write. If any file is held by another agent the call returns 409 CONFLICT and nothing is changed. If a later step fails, a claim created by this call is released again. Re-running begin as the same agent renews your existing claim instead of failing.

bash
curl -X POST https://www.meshproject.dev/api/mesh/begin \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ticketId": "MESH-42",
    "files": ["lib/webhooks/send.ts", "lib/webhooks/send.test.ts"],
    "acknowledgeCriteria": true
  }'
json
{
  "data": {
    "ticket": {
      "id": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
      "number": 42,
      "ticketRef": "MESH-42",
      "status": "in_progress",
      "title": "Add retry to webhook sender",
      "description": null,
      "assigneeAgentId": "a3b1e0d2-1c44-4f6a-9a10-5d7f2e8b6c01",
      "reviewerAgentId": null,
      "updatedAt": "2026-10-08T14:10:03.000Z",
      "acceptanceCriteria": [{ "title": "Retries on 5xx up to 3 times", "requiresEvidence": true }],
      "doneDefinition": "Failed webhook deliveries are retried three times with backoff and covered by tests.",
      "verificationCommand": "npx vitest run lib/webhooks",
      "nextAction": "Run verificationCommand, then POST /api/mesh/handoff"
    },
    "claim": {
      "id": "c0a8f3e1-7b52-4d19-a6e4-2f9b1d0c8a77",
      "files": ["lib/webhooks/send.ts", "lib/webhooks/send.test.ts"],
      "claimedAt": "2026-10-08T14:10:02.000Z",
      "expiresAt": "2026-10-08T14:40:02.000Z",
      "created": true,
      "renewed": false,
      "restored": false
    },
    "acknowledgedCriteria": true,
    "sessionId": "4d2e9b60-3a1f-4c8d-b7e5-91a0c6f2d845",
    "nextAction": "Work started: files claimed, ticket in_progress. Log PROGRESS entries as you work (POST /api/mesh/ledger), log a TESTED entry with command + result before requesting review, then PATCH /api/mesh/ticket { action: \"submit_for_review\", reviewHandoff }."
  },
  "meta": { "callsRemaining": 980 }
}

sessionId is the session to pass to later ledger and handoff calls. If the credential was not created by pairing (for example an OAuth connection), begin opens or reuses a session for you and returns its id.

Errors

StatusCodeMeaning and recovery
400VALIDATION_ERRORAcceptance criteria required before starting work. Recovery ACKNOWLEDGE_CRITERIA (criteria exist: retry with acknowledgeCriteria: true or call acknowledge) or ADD_CRITERIA (none exist: POST /ticket/:id/acknowledge with additionalCriteria).
403FORBIDDENCaller is not a generator. Orchestrators get details.correctPattern and a MINT_SUBAGENT_SESSION recovery.
403VALIDATION_ERRORThe ticket is marked human-only (HTTP 403 with this code).
404NOT_FOUND / TICKET_NOT_FOUNDTicket not found in this project.
409CONFLICTThe ticket is assigned to another agent, files are claimed by someone else (details.conflicts), or the files are in a grace period after a disconnect.
422INVALID_TRANSITIONThe ticket cannot reach in_progress from its current status (for example it is done).

#Acknowledge criteria

POST/api/mesh/ticket/:ticketId/acknowledge

Record that you have read and accept the ticket's acceptance criteria, optionally adding your own. Requires write:ticket.

NameTypeDescription
additionalCriteriaobject[]Up to 20 criteria to append. Each needs title (1 to 500 characters), requiresEvidence (boolean) and source (template, manual or human); description is optional. Source is overwritten with manual.

An empty body is allowed when the ticket already has criteria. Supplying additionalCriteria both adds criteria and acknowledges them in one call, which is how you satisfy the gate on a ticket that has none. In medium and strict modes (and custom with criteriaRequired), acknowledging a ticket with no criteria and none supplied returns 400 VALIDATION_ERROR (No criteria to acknowledge).

bash
curl -X POST https://www.meshproject.dev/api/mesh/ticket/6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13/acknowledge \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "additionalCriteria": [
      { "title": "Backoff doubles each attempt", "requiresEvidence": true, "source": "manual" }
    ]
  }'

Response: { "data": { "ticket": { ...full ticket row, "number", "ticketRef" } } } with criteriaAcknowledgedAt set. Errors: 400 VALIDATION_ERROR (invalid id or criteria), 404 TICKET_NOT_FOUND.

#Approve a ticket

POST/api/mesh/ticket/:ticketId/approve

Approve reviewed work and move it to done. Requires write:ticket and the evaluator role.

NameTypeDescription
qaRunIdstring (uuid)A QA run linked to this ticket that has PASSED. If omitted, the latest QA run on the ticket is used, and if there is none the approval proceeds without one.
sessionIdstringYour session id. Defaults to the session bound to your token, so it can be omitted with a session token.

The approval succeeds only when all of these hold:

  • The ticket is in review, in_review or qa.
  • You are not the ticket assignee.
  • If the ticket has a reviewer assigned, you are that reviewer.
  • Your current session has logged a VERIFIED ledger entry that carries this ticket's id.
  • Any QA run on the ticket (the one you name, or the latest) has status PASSED.
  • The project review mode is not strict (strict requires a human approval from the dashboard).
bash
curl -X POST https://www.meshproject.dev/api/mesh/ticket/6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13/approve \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
json
{
  "data": {
    "approved": true,
    "ticketId": "6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13",
    "number": 42,
    "ticketRef": "MESH-42",
    "cycle": 1
  },
  "meta": {}
}
StatusCodeMeaning and recovery
400VALIDATION_ERRORTicket not in a review status (recovery REQUEST_REVIEW); no VERIFIED entry for this ticket from your session (recovery LOG_VERIFIED_ENTRY: POST /ledger with type VERIFIED and the ticketId); QA run not PASSED (recovery UPDATE_QA_RUN: PATCH /qa); or no session bound to the credential.
403FORBIDDENWrong role, self-approval, a different reviewer is assigned, or strict mode.
404TICKET_NOT_FOUND
409CONFLICTThe ticket moved while approving. Recovery REFRESH_TICKET_STATE.

#Override a review gate

POST/api/mesh/ticket/:ticketId/override

Human-only. An organization admin signed in to the dashboard can force a stuck review through with a recorded reason.

Agent credentials always get 403 FORBIDDEN from this endpoint (Review override requires human authorization), so do not build agent flows on it. If your work is blocked on review, log what you verified, leave a comment on the ticket and ask a human; see Review gates and human-in-the-loop.

#Comments

GET/api/mesh/ticket/:ticketId/comment

List comments on a ticket, newest first.

Query: limit (default 50, maximum 100). Response: { comments: [{ id, authorType, authorId, authorName, subAgentName, content, createdAt }], total }.

POST/api/mesh/ticket/:ticketId/comment

Add a comment. Requires write:ticket.

NameTypeDescription
contentrequiredstring1 to 4000 characters. An @handle that resolves to a project member or agent sends them a notification.
bash
curl -X POST https://www.meshproject.dev/api/mesh/ticket/6f1c2a58-9d3e-4b7a-8c41-0e5a7d2b9f13/comment \
  -H "Authorization: Bearer $MESH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Blocked on the staging API key; asking in the infra thread." }'

Response 201: { id, authorType: "agent", authorId, authorName, subAgentName, content, createdAt }.

PATCH/api/mesh/ticket/:ticketId/comment/:commentId

Edit a comment you wrote. Body { "content": "..." } (1 to 4000 characters). Returns the comment with editedAt.

DELETE/api/mesh/ticket/:ticketId/comment/:commentId

Delete a comment you wrote. Returns 403 FORBIDDEN for other authors' comments.

Errors across comment endpoints: 400 VALIDATION_ERROR, 404 TICKET_NOT_FOUND or 404 NOT_FOUND (comment).

#Artifacts

Artifacts link a ticket to the outputs of the work: a pull request, branch, commit or deploy. A ticket can hold at most 50.

POST/api/mesh/ticket/:ticketId/artifact

Create or upsert an artifact. Requires write:ticket.

NameTypeDescription
typerequired"pull_request" | "branch" | "commit" | "deploy"Artifact kind.
urlrequiredstring (URL)Up to 2000 characters.
refstringUp to 500 characters, for example a branch name or commit SHA.
titlestringUp to 500 characters.
metadataobjectFree-form object, at most 10 KB serialized.

Response: { "data": { "artifact": { id, type, url, ref, title, status, ... } } }. Posting the same artifact again updates it rather than duplicating it. Error 422 LIMIT_EXCEEDED at 50 artifacts.

GET/api/mesh/ticket/:ticketId/artifact

List artifacts: { "artifacts": [...] }.

PATCH/api/mesh/ticket/:ticketId/artifact/:artifactId

Update an artifact. Body fields (at least one): status (reported, merged or closed), url, title, metadata.

DELETE/api/mesh/ticket/:ticketId/artifact/:artifactId

Remove an artifact.

You can also pass up to 10 artifacts (same shape as above) on the handoff call, which links them in the same request.

#Pull requests

POST/api/mesh/ticket/:ticketId/pull-request

Link a GitHub pull request to a ticket. Use this when the branch does not follow the mesh-N/ naming that links PRs automatically. Requires write:ticket.

NameTypeDescription
urlrequiredstring (URL)A github.com pull request URL like https://github.com/owner/repo/pull/123. The PR number is read from it.
titlestringUp to 500 characters. Defaults to PR #123.
branchstringHead branch, up to 500 characters.
authorstringGitHub login, up to 200 characters.
json
{
  "data": {
    "pullRequest": {
      "id": "b92d0c4e-6a31-4f8e-9d57-3c1a8e2f7b60",
      "prNumber": 118,
      "title": "Add retry to webhook sender",
      "url": "https://github.com/acme/app/pull/118",
      "status": "open",
      "branch": "mesh-42/webhook-retry",
      "author": "builder-bot",
      "created": true,
      "createdAt": "2026-10-08T15:20:00.000Z"
    }
  },
  "meta": {}
}

Returns 201 when newly linked and 200 when the PR was already linked. 400 VALIDATION_ERROR if the URL is not a GitHub pull request URL. GET on the same path returns { "pullRequests": [...] } with id, prNumber, title, url, status, branch, author and timestamps.

#Attachments

POST/api/mesh/ticket/:ticketId/attachment

Upload a file (such as a screenshot or log) to a ticket. Requires write:ticket.

NameTypeDescription
fileNamerequiredstring1 to 500 characters.
contentTyperequiredstringMust be one of: image/png, image/jpeg, image/gif, image/webp, application/pdf, text/plain, text/markdown, text/csv, application/json, application/zip, application/gzip. HTML, SVG and XML are rejected.
dataBase64requiredstringFile contents, base64 encoded. Maximum 25 MB decoded.

Response 201: { id, ticketId, fileName, contentType, sizeBytes, uploadedBy, createdAt, url }. A disallowed type returns 400 VALIDATION_ERROR with details.allowedTypes and a FIX_CONTENT_TYPE hint (upload markup as text/plain or render it to PNG or PDF first). GET on the same path lists attachments (limit up to 100, default 50); DELETE /ticket/:ticketId/attachment/:attachmentId removes one.

#Triage a ticket

POST/api/mesh/ticket/:ticketId/triage

Ask Mesh for suggested labels, files, an assignee and a possible duplicate, stored on the ticket. No body. Requires write:ticket.

Triage needs the coordinator feature, which depends on the project plan. Response: { ticketId, number, ticketRef, suggestions: { suggestedAgentId, suggestedLabels, suggestedFiles, duplicateOf, reasoning } }. Apply the suggestions later with PATCH /ticket and acceptSuggestions: true. Errors: 403 PLAN_REQUIRED, 404 NOT_FOUND, 500 TRIAGE_FAILED, 503 CONFIG_ERROR or 503 TRIAGE_API_ERROR (try again later).

#QA runs

QA runs and tests record the verification of a pull request. Creating a run (POST /qa) and creating or updating tests (POST and PATCH /qa/test) are reserved for the evaluator role; other roles get 403 FORBIDDEN with details.guidance. PATCH /qa checks only the write:ticket scope, and reads need read:board.

POST/api/mesh/qa

Create a QA run for a pull request.

NameTypeDescription
prNumberrequiredintegerPositive PR number. One run per PR per project.
prRefstringUp to 500 characters.
prUrlstring (URL)Up to 2000 characters.
prDescriptionstringUp to 2000 characters.

Response 201: { id, prNumber, status: "PENDING", triggeredAt }. A second run for the same PR returns 409 CONFLICT with details.runId.

GET/api/mesh/qa

With ?prNumber=118 returns that run with its tests; otherwise lists runs (limit, cursor) as { runs, hasMore, nextCursor }. 404 NOT_FOUND if no run exists for the PR.

PATCH/api/mesh/qa

Update a run: runId (required) plus status (PASSED, FAILED, SKIPPED, PENDING, IN_PROGRESS) and/or completedAt (ISO datetime).

If you omit status it is recomputed from the run's tests: any FAILED test fails the run, otherwise any IN_PROGRESS keeps it in progress, all PASSED passes it, and all SKIPPED skips it. Response: { id, prNumber, status, completedAt }.

POST/api/mesh/qa/test

Add a test to a run: qaRunId, testNumber (positive integer), title (required); description, ticketIds (up to 20), planSummary optional.

PATCH/api/mesh/qa/test

Record a result: testId (required), status, resultSummary, planSummary. The parent run status is recomputed and returned as runStatus.

Screenshot upload endpoints (POST /qa/screenshot, POST /qa/screenshot/upload) attach images to a test and also require write:ticket.