Skip to content

API Reference

All backend routes are mounted under /api.

This page documents the current HTTP surface exposed by server/index.ts and the route handlers in server/routes/*.

Conventions

ConventionMeaning
Ticket identifiersMost ticket endpoints use the external ticket reference, not the local numeric DB id
JSON validationMost write routes validate request bodies with Zod or route-specific parsers
StreamingLive ticket updates use Server-Sent Events from /api/stream
Error shapeError responses usually include error and sometimes details or message

Health, Models, Workflow Meta, And Streaming

MethodRouteNotes
GET/api/healthBasic process health
GET/api/health/opencodeOpenCode reachability and version
GET/api/health/startupStartup recovery and restore status
POST/api/health/startup/restore-notice/dismissDismiss startup restore notice
GET/api/modelsConnected and full model catalog
GET/api/workflow/metaCurrent workflow groups and phases
GET/api/stream?ticketId=<id>Ticket-scoped SSE stream

/api/stream also accepts lastEventId as a query parameter. Browsers normally send Last-Event-ID automatically only for native reconnects; the frontend persists the last event id per ticket and sends the query value after reloads so the backend can replay buffered events when possible.

Example health payload:

json
{
  "status": "ok",
  "timestamp": "2026-04-23T09:00:00.000Z",
  "uptime": 1234.56
}

Example models payload:

json
{
  "models": [],
  "allModels": [],
  "connectedProviders": [],
  "defaultModels": {},
  "message": "OpenCode server is not reachable. Start it with `opencode serve`."
}

Profile Routes

LoopTroop uses a singleton profile, not a collection.

MethodRouteNotes
GET/api/profileReturns the singleton profile or null
POST/api/profileCreates the singleton profile
PATCH/api/profileUpdates the singleton profile

Example profile update payload:

json
{
  "mainImplementer": "openai/gpt-5.4",
  "mainImplementerVariant": "high",
  "councilMembers": "[\"openai/gpt-5.4\",\"anthropic/claude-sonnet-4\"]",
  "minCouncilQuorum": 2,
  "perIterationTimeout": 1800,
  "executionSetupTimeout": 900,
  "councilResponseTimeout": 240,
  "interviewQuestions": 12,
  "coverageFollowUpBudgetPercent": 35,
  "maxCoveragePasses": 3,
  "maxPrdCoveragePasses": 5,
  "maxBeadsCoveragePasses": 5,
  "maxIterations": 5,
  "toolInputMaxChars": 4000,
  "toolOutputMaxChars": 12000,
  "toolErrorMaxChars": 6000
}

Project Routes

MethodRouteNotes
GET/api/projects/check-git?path=...Validates git and GitHub origin status for a folder
GET/api/projects/ls?path=...Directory browser used by the attach-project flow
GET/api/projectsList attached projects
GET/api/projects/:idGet one project
POST/api/projectsAttach a project
PATCH/api/projects/:idUpdate project settings
DELETE/api/projects/:idDelete a project if no active tickets remain

Example project attachment payload:

json
{
  "name": "LoopTroop",
  "shortname": "LOOP",
  "folderPath": "/home/liviu/LoopTroop",
  "icon": "📁",
  "color": "#3b82f6",
  "profileId": 1
}

Ticket Routes

CRUD And UI State

MethodRouteNotes
GET/api/ticketsOptionally filtered with ?projectId=
GET/api/tickets/:idGet one ticket
POST/api/ticketsCreate a ticket
PATCH/api/tickets/:idUpdate title, description, or priority
DELETE/api/tickets/:idOnly allowed for COMPLETED or CANCELED
GET/api/tickets/:id/ui-state?scope=...Read persisted UI state
PUT/api/tickets/:id/ui-stateSave persisted UI state

Example ticket creation payload:

json
{
  "projectId": 1,
  "title": "Implement refresh-token rotation",
  "description": "Rotate refresh tokens and invalidate the family on reuse.",
  "priority": 2
}

Example UI-state payload:

json
{
  "scope": "interview-drafts",
  "data": {
    "draftAnswers": {},
    "skippedQuestions": {},
    "selectedOptions": {}
  }
}

Example UI-state response:

json
{
  "scope": "interview-drafts",
  "exists": true,
  "data": {
    "draftAnswers": {},
    "skippedQuestions": {},
    "selectedOptions": {}
  },
  "updatedAt": "2026-04-23T09:00:00.000Z"
}

Workflow Actions

MethodRouteNotes
POST/api/tickets/:id/startStarts a DRAFT ticket using locked profile and project settings
POST/api/tickets/:id/approveGeneric workflow approval endpoint
POST/api/tickets/:id/cancelCancel active work — accepts an optional JSON body (see below)
POST/api/tickets/:id/approve-interviewApprove interview artifact
POST/api/tickets/:id/approve-prdApprove PRD artifact
POST/api/tickets/:id/approve-beadsApprove bead plan artifact
POST/api/tickets/:id/approve-execution-setup-planApprove execution setup plan
POST/api/tickets/:id/mergeMerge delivered PR
POST/api/tickets/:id/close-unmergedClose without merge
POST/api/tickets/:id/verifyCurrently handled by the merge handler alias
POST/api/tickets/:id/retryRetry a blocked ticket or failed phase
POST/api/tickets/:id/dev-eventDevelopment event injection endpoint

The cancel endpoint accepts an optional JSON request body to trigger cleanup at cancellation time. Both fields default to false; the ticket record itself is never deleted.

json
{
  "deleteContent": false,
  "deleteLog": false
}
FieldTypeDefaultDescription
deleteContentbooleanfalsePermanently removes all AI-generated artifacts (interview Q&A, PRD drafts, beads plan) from the database and deletes the isolated git worktree and its branch
deleteLogbooleanfalsePermanently removes the execution log file (execution-log.jsonl) for this ticket. This is only effective when the worktree still exists; if deleteContent is also true the worktree removal already covers the log

Interview And Planning Editing

MethodRouteNotes
GET/api/tickets/:id/interviewReturns interview payload with winnerId, raw, document, session, and questions
PUT/api/tickets/:id/interviewSave raw interview YAML
PUT/api/tickets/:id/interview-answersSave structured interview answers during approval or planning restart
POST/api/tickets/:id/answerDeprecated, returns 410; use answer-batch
POST/api/tickets/:id/answer-batchSubmit interview answers
POST/api/tickets/:id/skipSkip remaining interview questions
PATCH/api/tickets/:id/edit-answerEdit a previously recorded answer while waiting for interview answers

Current batch-answer payload:

json
{
  "answers": {
    "q-auth-1": "Support both password login and SSO."
  },
  "selectedOptions": {
    "q-auth-2": ["option-password", "option-sso"]
  }
}

Possible answer-batch response shapes:

json
{
  "accepted": true
}
json
{
  "questions": [],
  "progress": {
    "answered": 4,
    "total": 8
  },
  "isComplete": false,
  "isFinalFreeForm": false,
  "aiCommentary": "Need one more clarification about session lifetime.",
  "batchNumber": 2,
  "source": "coverage",
  "roundNumber": 1
}

Structured interview-answer approval payload:

json
{
  "questions": [
    {
      "id": "q-auth-1",
      "answer": {
        "skipped": false,
        "selected_option_ids": [],
        "free_text": "Support password login and SSO."
      }
    }
  ]
}

Execution Setup Plan Routes

MethodRouteNotes
GET/api/tickets/:id/execution-setup-planRead the current setup plan
PUT/api/tickets/:id/execution-setup-planSave setup plan as raw content or structured plan
POST/api/tickets/:id/regenerate-execution-setup-planRegenerate the plan with commentary

Execution setup plan read response:

json
{
  "exists": true,
  "artifactId": 42,
  "updatedAt": "2026-04-23T09:00:00.000Z",
  "raw": "{\n  \"schema_version\": \"1\",\n  \"ticket_id\": \"AUTH-12\"\n}",
  "plan": {
    "schemaVersion": "1",
    "ticketId": "AUTH-12"
  }
}

Regeneration payload:

json
{
  "commentary": "Tighten the temp-root cleanup steps and add the full lint command.",
  "rawContent": "{\n  \"schema_version\": \"1\",\n  \"ticket_id\": \"AUTH-12\"\n}"
}

OpenCode Question Routes

MethodRouteNotes
GET/api/tickets/:id/opencode/questionsList pending OpenCode question requests
POST/api/tickets/:id/opencode/questions/:requestId/replySubmit question answers
POST/api/tickets/:id/opencode/questions/:requestId/rejectReject a question request

Reply payload:

json
{
  "answers": [
    ["yes"],
    ["postgres", "redis"]
  ]
}

Artifact And History Routes

MethodRouteNotes
GET/api/tickets/:id/artifactsList ticket artifacts, optionally filtered
GET/api/tickets/:id/phases/:phase/attemptsList phase attempt history

File Routes

These routes are intentionally narrow.

MethodRouteNotes
GET/api/files/:ticketId/logsRead folded execution logs
GET/api/files/:ticketId/:fileOnly interview or prd
PUT/api/files/:ticketId/:fileOnly interview or prd

There is no generic filesystem browser or arbitrary file read route under /api/files.

Bead Routes

MethodRouteNotes
GET/api/tickets/:id/beadsRead bead plan; accepts optional ?flow=
PUT/api/tickets/:id/beadsReplace bead plan; accepts optional ?flow=
GET/api/tickets/:id/beads/:beadId/diffRead diff artifact for a bead

SSE Events

The stream endpoint emits:

  • connected
  • heartbeat
  • state_change
  • log
  • progress
  • error
  • bead_complete
  • needs_input
  • artifact_change

Current custom event types are defined in server/sse/eventTypes.ts.

SSE replay is an optimization, not the only recovery path. After a reconnect with a remembered event id, the frontend also invalidates the ticket, list, artifacts, interview, setup-plan, bead, and server-log queries so missed events outside the replay buffer are reconciled from durable storage.

Example state_change event payload:

json
{
  "ticketId": "AUTH-12",
  "from": "DRAFTING_PRD",
  "to": "WAITING_PRD_APPROVAL",
  "previousStatus": "VERIFYING_PRD_COVERAGE",
  "timestamp": "2026-04-23T09:00:00.000Z"
}

Example artifact_change event payload:

json
{
  "ticketId": "AUTH-12",
  "phase": "CODING",
  "artifactType": "bead_diff:api-refresh-endpoint",
  "artifact": {
    "id": 84,
    "ticketId": "AUTH-12",
    "phase": "CODING",
    "phaseAttempt": 1,
    "artifactType": "bead_diff:api-refresh-endpoint",
    "filePath": null,
    "content": "diff --git a/server/routes/auth.ts b/server/routes/auth.ts\n...",
    "createdAt": "2026-04-23T09:00:00.000Z",
    "updatedAt": "2026-04-23T09:00:00.000Z"
  },
  "timestamp": "2026-04-23T09:00:00.000Z"
}

LoopTroop documentation for the current runtime.