Migrate from the legacy API

The public API moved and its resource shapes changed. This page walks through the migration for a person; the agent brief at the bottom is the same content as a ready-made task prompt you can paste into any coding agent.

The fastest path⁠

  1. Create a current API key

    Legacy personal tokens did not migrate. Create an organization-scoped key in Settings → API — as yourself, or as a service user for least-privilege automation.
  2. Hand the brief to an agent

    Copy the agent brief below and give it to your coding agent together with access to your integration's repository.
  3. Review the report

    The brief requires the agent to report changed files, removed endpoints, unsupported capabilities, and the checks it ran. Review that report before cutting over.

Base URL and authentication⁠

https://capy.ai/api/v1 is retired; the current base is https://api.capy.ai/api/v1 (don't append another /api or /v1). Authentication is unchanged in shape — Authorization: Bearer capy_... — but the key itself is new: legacy personal tokens were user-scoped across organizations and never migrated, so create an organization-scoped key in Settings → API. Keys have no per-endpoint scopes; for least-privilege automation, create a service user with the role and project access you want and mint the key for it.

A safe smoke test for a new key:

Terminal
curl -sS "https://api.capy.ai/api/v1/projects" \
  -H "Authorization: Bearer $CAPY_API_TOKEN"

Project IDs carried over from the legacy system, and project discovery is public: GET /projects lists the projects your key can access and GET /projects/{projectId} reads one, so a configured project ID keeps working and new ones resolve through the API instead of the app.

What changed⁠

FlowLegacyCurrent
Discover projectsGET /projects with limit/offset pagingSame paths, unpaged: GET /projects returns every project your key can access as { items }, GET /projects/{projectId} reads one. Items carry id, name, description, code, repos (repoFullName, baseBranch), and timestamps
Create threadPOST /threads with { projectId, prompt }POST /threads with { requestId, projectId, message } plus optional title, model, machineSize, authorId (attribute the thread to an organization member). requestId (1–191 chars) is your idempotency key: reusing it on a retry returns the original thread instead of a duplicate billable run
Poll statusrunState, waitingOn, blockedOn, pendingWakeupsOne status field: working, waiting (resumes by itself), idle (stopped, waits for your message), failed, archived. Poll while it reads working
Send messagePOST /threads/{id}/message with { message, mode }Same path with { text, delivery } — interrupt (default), queue, or steer — returning a receipt { id, deduped }. Pass clientKey as the idempotency key: a retry with the same key returns the first receipt with deduped: true instead of posting twice
Queued messages—POST .../messages/{eventId}/cancel dequeues, POST .../messages/{eventId}/send-now promotes; eventId is the receipt's id
Stop workPOST /threads/{id}/stop; archive stopped active workPOST /threads/{id}/interrupt; archive no longer stops anything — call both when you want both
Read transcriptcontent field, nextCursor + hasMoretext field with source of user/assistant/tool; pages are { items, cursor } with after, null cursor = caught up, cursors are opaque
TasksCreate/message/start/stop/PR routesRead-only subagents: GET /threads/{id}/subagents, GET /subagents/{subagentId} (statuses working, waiting, idle, done, failed), also served at GET /threads/{id}/tasks and GET /tasks/{taskId}; drive work by messaging the root thread
UsageLegacy GET /usage shapeGET /usage returns the current UTC month through now; pass ISO from/to timestamps for another window. Billed LLM, image, machine, and total dollars; token totals; per-member, per-model, and per-image-endpoint splits. Per-thread three-kind totals stay on Thread.usage (one credit = $0.001)

Creating a thread end to end:

Terminal
curl -sS -X POST "https://api.capy.ai/api/v1/threads" \
  -H "Authorization: Bearer $CAPY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "requestId": "deploy-fix-1", "projectId": "'$CAPY_PROJECT_ID'", "message": "Investigate the failing integration" }'

Errors⁠

The legacy { "error": { "code", "message" } } wrapper is gone. Errors are an HTTP status plus a tagged object — branch on the status first, then _tag:

JSON
{ "_tag": "capy/ThreadNotFound", "threadId": "jam_..." }

Inaccessible and absent resources intentionally share the same 404, so a thread in another organization is indistinguishable from one that doesn't exist.

Retired capabilities⁠

Legacy capabilityWhat to do instead
Tags, setup, snapshots, browser snapshots, personal environment variablesManage in the app; no public replacement
GET /threads/{threadId}/session-token, POST /sessions/verifyNo public replacement
Task mutation routes (create, message, start, stop, PR) and GET /tasks/{taskId}/diffDrive work through the root thread
GET /modelsOmit model to use the configured default; pick models in the app
Thread and message attachmentUrlsUpload with POST /files and attach by fileId on a message send; list and download with GET /threads/{threadId}/attachments

If your integration depends on one of these, that dependency is a migration blocker to resolve product-side, not something to paper over in code.

Cutover checklist⁠

  • No code, tests, or configuration reference https://capy.ai/api/v1
  • Thread creation sends a stable requestId and handles the current Thread shape
  • Stop paths call /interrupt; pagination trusts cursor and treats null as done
  • Error handling branches on HTTP status and _tag
  • The old key is revoked after the new organization-scoped key is live

Agent brief⁠

The button copies the full agent brief to your clipboard (or downloads it where clipboard access is unavailable). Paste it into your coding agent as the task prompt.

Copy the migration brief
Open in Capy