> ## Documentation Index
> Fetch the complete documentation index at: https://docs.capy.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from the legacy API

> What changed between the legacy Capy API and the current public API, and how to migrate an integration

The public API moved and its resource shapes changed. This page walks through the migration for a person; the [agent brief](#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

<Steps>
  <Step title="Create a current API key">Legacy personal tokens did not migrate. Create an organization-scoped key in [Settings → API](https://capy.ai/settings/api) — as yourself, or as a service user for least-privilege automation.</Step>
  <Step title="Hand the brief to an agent">Copy the [agent brief](#agent-brief) below and give it to your coding agent together with access to your integration's repository.</Step>
  <Step title="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.</Step>
</Steps>

## 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](https://capy.ai/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:

```bash theme={null}
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

| Flow | Legacy | Current |
| - | - | - |
| Discover projects | `GET /projects` with `limit`/offset paging | Same 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 thread | `POST /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 status | `runState`, `waitingOn`, `blockedOn`, `pendingWakeups` | One `status` field: `active`, `waiting`, `pending_user` (input required), `error`, `ready_for_review`, `idle`, `archived` |
| Send message | `POST /threads/{id}/message` with `{ message, mode }` | Same path with `{ text, delivery }` — `interrupt` (default), `queue`, or `steer` — returning a receipt `{ id, deduped }`. No idempotency key: don't blindly retry a timed-out send |
| Queued messages | — | `POST .../messages/{eventId}/cancel` dequeues, `POST .../messages/{eventId}/send-now` promotes; `eventId` is the receipt's `id` |
| Stop work | `POST /threads/{id}/stop`; archive stopped active work | `POST /threads/{id}/interrupt`; archive no longer stops anything — call both when you want both |
| Read transcript | `content` field, `nextCursor` + `hasMore` | `text` field with `source` of `user`/`assistant`/`tool`; pages are `{ items, cursor }` with `after`, `null` cursor = caught up, cursors are opaque |
| Tasks | Create/message/start/stop/PR routes | Read-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 |
| Usage | Legacy `GET /usage` shape | `GET /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:

```bash theme={null}
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 theme={null}
{ "_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 capability | What to do instead |
| - | - |
| Tags, setup, snapshots, browser snapshots, personal environment variables | Manage in the app; no public replacement |
| `GET /threads/{threadId}/session-token`, `POST /sessions/verify` | No public replacement |
| Task mutation routes (create, message, start, stop, PR) and `GET /tasks/{taskId}/diff` | Drive work through the root thread |
| `GET /models` | Omit `model` to use the configured default; pick models in the app |
| Thread and message `attachmentUrls` | No public upload/download capability |

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

<a id="copy-migration-brief" class="copy-migration-brief" href="#migration-brief-source">
  Copy the migration brief
</a>

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.

<div id="migration-brief-source" class="migration-brief-source">
  ````markdown theme={null}
  # Migrate an integration to the current Capy API

  Use this brief as the task prompt for an agent migrating customer code from the legacy Capy API to the current public API.

  ## Goal

  Update the integration so it uses the current Capy API safely, preserves existing project configuration, handles the new resource shapes, and no longer calls retired endpoints.

  Do not expose API keys, make unapproved production mutations, or claim success without running the integration's relevant checks.

  ## Authoritative API facts

  - Current base URL: `https://api.capy.ai/api/v1`
  - Retired base URL: `https://capy.ai/api/v1`
  - Authentication: `Authorization: Bearer capy_...`
  - Create a current organization-scoped key in **Settings → API**. Legacy personal tokens never migrate — they were user-scoped across organizations, and current keys are organization-scoped. Legacy service-user tokens are planned to carry over unchanged, but do not depend on one until it verifies against the current API.
  - The current OpenAPI document is the one published with this documentation site. Do not generate a client from the legacy OpenAPI document.
  - Public resources currently cover projects (read-only discovery), threads, messages, read-only subagents (formerly tasks), folders (including pinning), automations, reviews, and usage reporting.
  - Project IDs from the legacy system are preserved, and project discovery is public: `GET /projects` lists the projects the key can access and `GET /projects/{projectId}` reads one. A configured project ID keeps working; project configuration itself stays app-only.

  Use environment variables rather than literals:

  ```bash
  CAPY_API_BASE=https://api.capy.ai/api/v1
  CAPY_API_TOKEN=capy_xxxx
  CAPY_PROJECT_ID=your-project-id
  ```

  Never print `CAPY_API_TOKEN` in logs or test output.

  ## Required migration process

  1. Read the repository's agent instructions and identify every Capy API client, endpoint string, request/response type, retry policy, polling loop, fixture, test, and customer-facing configuration reference.
  2. Search for at least: `capy.ai/api`, `/v1/projects`, `/v1/threads`, `/v1/tasks`, `nextCursor`, `hasMore`, `prompt`, `messageId`, `mode`, `runState`, and legacy error codes.
  3. Record which retired capabilities the integration uses. If a required capability has no current public replacement, do not silently remove behavior; report it as a blocker with the exact call site.
  4. Implement the endpoint, payload, response, pagination, status, and retry changes below.
  5. Update relevant fixtures and existing tests. Add tests only when the repository's instructions permit them.
  6. Run the smallest typecheck, lint, formatting, and test commands covering the changed code.
  7. Report changed behavior, unsupported dependencies, checks run, and any production cutover steps.

  ## Base URL and authentication

  Replace:

  ```text
  https://capy.ai/api/v1
  ```

  with:

  ```text
  https://api.capy.ai/api/v1
  ```

  Do not append a second `/api` or `/v1` segment.

  Current API keys do not have per-endpoint scopes. For least-privilege automation, use a service user with the required role and project access, then create the key for that service user.

  A safe read-only smoke request is:

  ```bash
  curl -sS "$CAPY_API_BASE/projects" \\
    -H "Authorization: Bearer $CAPY_API_TOKEN"
  ```

  A `401` response uses this shape:

  ```json
  { "_tag": "capy/Unauthorized" }
  ```

  ## Endpoint crosswalk

  | Legacy API                                                                                                   | Current API or required action                                                                                                                                                                                                                                                                                    |
  | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `GET /projects`                                                                                              | Same path, unpaged: `{ items }` of every project the key can access, with no `limit`/offset parameters. Items carry `id`, `name`, `description`, `code`, `repos` (`repoFullName`, `baseBranch`), `createdAt`, and `updatedAt`.                                                                                     |
  | `GET /projects/{projectId}`                                                                                  | Same path, same item shape. Inaccessible and deleted projects answer `404` like absent ones. Project configuration (setup, snapshots, environment variables) stays app-only.                                                                                                                                       |
  | `GET /threads`                                                                                               | Same path. Supported query fields are `projectId`, `status`, `limit`, and `cursor`.                                                                                                                                                                                                                               |
  | `POST /threads`                                                                                              | Same path with a new request body; see below.                                                                                                                                                                                                                                                                     |
  | `GET /threads/{threadId}`                                                                                    | Same path with the new `Thread` response.                                                                                                                                                                                                                                                                         |
  | `POST /threads/{threadId}/message`                                                                           | Same path with `{ text, model?, delivery? }`.                                                                                                                                                                                                                                                                     |
  | `GET /threads/{threadId}/messages`                                                                           | Same path with `after` pagination and the new `Message` response.                                                                                                                                                                                                                                                 |
  | `POST /threads/{threadId}/stop`                                                                              | Replace with `POST /threads/{threadId}/interrupt`.                                                                                                                                                                                                                                                                |
  | `POST /threads/{threadId}/archive`                                                                           | Same path; now returns the complete `Thread`. It does not stop active work.                                                                                                                                                                                                                                       |
  | `POST /threads/{threadId}/unarchive`                                                                         | Same path; now returns the complete `Thread`.                                                                                                                                                                                                                                                                     |
  | `PUT /threads/{threadId}/tags`                                                                               | No public replacement.                                                                                                                                                                                                                                                                                            |
  | `GET /threads/{threadId}/session-token`                                                                      | No public replacement.                                                                                                                                                                                                                                                                                            |
  | `GET /tasks/{taskId}`                                                                                        | Replace with `GET /subagents/{subagentId}`, using only the opaque subagent ID returned by the current API. `GET /tasks/{taskId}` still answers with the same data.                                                                                                                                                                                                                                     |
  | Legacy task list/create/message/start/stop/PR routes                                                         | No direct replacement. List subagents with `GET /threads/{threadId}/subagents` and drive work through the root thread.                                                                                                                                                                                                                |
  | `GET /tasks/{taskId}/diff`                                                                                   | No public replacement.                                                                                                                                                                                                                                                                                            |
  | `GET /models`                                                                                                | No public replacement. Omit `model` to use the configured default.                                                                                                                                                                                                                                                |
  | `GET /projects/{projectId}/automations`                                                                       | Replace with `GET /automations?projectId={projectId}`. The response is cursor-paginated and includes each automation's standing configuration and enabled state.                                                                                                                                                   |
  | `POST /projects/{projectId}/automations`                                                                      | Replace with `POST /automations`, move `projectId` into the request body, and add a caller-stable `requestId`. The first successful response includes a one-time `webhookUrl` when the trigger set contains `incoming_webhook`; retries converge on the same automation without redisplaying the credential.                                                                    |
  | Legacy automation update or delete routes                                                                    | General updates have no public replacement. Use `POST /automations/{automationId}/disable` and `/enable` to pause or resume runs, and `/delete` and `/restore` for the soft-delete lifecycle.                                                                                                                       |
  | `GET /usage`                                                                                                 | Same path with a new response: `GET /usage` returns a `UsageReport` for the current UTC month through now — pass ISO `from`/`to` timestamps to choose another window — with LLM, image, machine, and total dollars; token totals; and per-member, per-model, and per-image-endpoint splits. Per-thread three-kind credit totals remain on `Thread.usage` and `Task.usage`. |
  | Setup, snapshots, tags, browser snapshots, personal environment variables, session verification              | No current public replacement; manage these in the app.                                                                                                                                                                                                                                                           |
  | Thread or message `attachmentUrls`                                                                           | No current public upload/download capability.                                                                                                                                                                                                                                                                     |

  ## Create a thread

  Replace the legacy `{ projectId, prompt, ... }` request with:

  ```json
  {
    "requestId": "caller-stable-idempotency-key",
    "projectId": "configured-project-id",
    "message": "Investigate the failing integration"
  }
  ```

  Optional fields:

  ```json
  {
    "title": "Custom title",
    "model": {
      "modelId": "model-id",
      "reasoningMode": "high",
      "modes": { "fast": true }
    },
    "machineSize": "medium",
    "authorId": "user-id"
  }
  ```

  Allowed machine sizes are `small`, `medium`, `large`, `ultra`, `hyper`, and `bigguy`.

  `authorId` attributes the thread to an active member of the key's organization: the thread lists under that member's threads, they are seeded as a participant, and the attribution reads back as `Thread.authorId`, while the kickoff message keeps the key's own principal as its author. The key still needs its own access to the project. An id that is not an active member answers `400` with `{ "_tag": "capy/InvalidRequest" }`, and a retried `requestId` returns the original thread with its original attribution even if the `authorId` changed or the member has since left. `GET /users` lists the organization's active members as `{ id, email, firstName, lastName }`, so resolve a person by email or name to the exact set of ids `authorId` accepts.

  `requestId` must be a caller-chosen string of 1–191 characters. Reuse it only when retrying the same logical create request. This makes thread creation retry-safe and prevents duplicate billable runs.

  The response is a `Thread`:

  ```json
  {
    "id": "jam_...",
    "projectId": "...",
    "authorId": null,
    "title": "Investigate the failing integration",
    "titleCustom": false,
    "status": "active",
    "archived": false,
    "lastModelId": null,
    "usage": {
      "llmCredits": 0,
      "imageCredits": 0,
      "vmCredits": 0,
      "totalCredits": 0
    },
    "createdAt": "2026-08-10T18:00:00.000Z",
    "updatedAt": "2026-08-10T18:00:00.000Z",
    "lastActivityAt": "2026-08-10T18:00:00.000Z"
  }
  ```

  One credit equals $0.001 USD. Thread usage includes the complete subagent subtree.

  ## Thread status

  Handle every current thread status:

  - `active`
  - `waiting`: asynchronous work may continue without customer action
  - `pending_user`: customer input is required
  - `error`
  - `ready_for_review`
  - `idle`
  - `archived`

  Do not carry forward logic based on the legacy `runState`, `waitingOn`, `blockedOn`, or `pendingWakeups` fields; they are not present on the current public `Thread`.

  ## Messages, interruption, and retries

  Replace the legacy message body:

  ```json
  { "message": "Focus here", "mode": "interrupt" }
  ```

  with:

  ```json
  { "text": "Focus here", "delivery": "interrupt" }
  ```

  `delivery` is optional and defaults to `interrupt`. Allowed values:

  - `interrupt`
  - `queue`
  - `steer`

  The response is an admission receipt:

  ```json
  { "id": "01K...", "deduped": false }
  ```

  Message sends do not currently accept a caller idempotency key. Do not automatically retry a timed-out message request unless duplicate delivery is acceptable or the integration performs its own reconciliation.

  Queued and steered messages can be changed before delivery:

  - `POST /threads/{threadId}/messages/{eventId}/cancel` returns `{ "outcome": "cancelled" }` or `{ "outcome": "tooLate" }`.
  - `POST /threads/{threadId}/messages/{eventId}/send-now` returns `{ "outcome": "sent", "id": "..." }` or `{ "outcome": "tooLate" }`.

  Use the admission receipt's `id` as `eventId`.

  Replace stop calls with:

  ```text
  POST /threads/{threadId}/interrupt
  ```

  Archiving no longer interrupts active work. When the old behavior expected both actions, call `/interrupt` and then `/archive` explicitly.

  ## Transcript migration

  Read a thread transcript with:

  ```text
  GET /threads/{threadId}/messages?after={cursor}&limit=50
  ```

  Current message fields:

  ```json
  {
    "id": "...",
    "source": "user",
    "text": "Message body",
    "createdAt": "2026-08-10T18:00:00.000Z"
  }
  ```

  Migrate `content` reads to `text`. `source` is now `user`, `assistant`, or `tool`. Tool messages may include `tool`; assistant messages may include `model` and `attachments`; user messages may include `authorName`.

  The public transcript contains rendered tool summaries, not raw tool arguments, results, or thinking. Do not depend on raw event payloads.

  ## Pagination migration

  Remove assumptions about `nextCursor` and `hasMore`.

  - Thread lists accept `cursor` and return `{ items, cursor }`.
  - Thread transcripts, subagent trees, and subagent transcripts accept `after` and return `{ items, cursor }`.
  - A `null` response cursor means the caller is caught up.
  - Pass non-null cursors back unchanged. Do not parse or construct them.

  ## Task migration

  Legacy tasks are now read-only subagents.

  List a thread's subagent tree:

  ```text
  GET /threads/{threadId}/subagents?after={subagentId}&limit=100
  ```

  Read a subagent or its transcript:

  ```text
  GET /subagents/{subagentId}
  GET /subagents/{subagentId}/messages?after={cursor}&limit=50
  ```

  Use the exact opaque subagent `id` returned by the subagent tree. Do not use a legacy task number or project-code identifier.

  Current subagent fields include `id`, `threadId`, `parentId`, `subagentPath`, `projectId`, `title`, `status`, `usage`, and timestamps. Handle status values `working`, `waiting`, `idle`, `done`, and `failed`.

  The same reads are served at `GET /threads/{threadId}/tasks`, `GET /tasks/{taskId}`, and `GET /tasks/{taskId}/messages`, with `taskPath` in place of `subagentPath` and `404 capy/TaskNotFound` in place of `capy/SubagentNotFound`. Code already using those routes keeps working; use the `/subagents` routes for new code.

  To direct or stop subagent work, send a message or interrupt to the subagent's root `threadId`. Do not call task mutation routes.

  ## New current routes

  Thread title routes:

  - `PATCH /threads/{threadId}` with `{ "title": "..." }`
  - Send `{ "title": null }` to return to automatic titles
  - `POST /threads/{threadId}/regenerate-title`

  Usage route:

  - `GET /usage?from={iso}&to={iso}` returns the key-scoped `UsageReport`: billed dollar totals (`llmDollars`, `imageDollars`, `vmDollars`, `totalDollars`), window LLM token totals, per-member billed spend, per-model spend split into billed and unbilled rows, and billed image spend grouped by exact endpoint. `from` defaults to the current UTC month and `to` defaults to now; the response echoes the effective window queried.

  Review routes:

  - `GET` and `PUT /review-settings`
  - `GET /review-billing-transfer`
  - `POST /review-billing-transfer/offer`
  - `POST /review-billing-transfer/accept`
  - `POST /review-billing-transfer/cancel`
  - `POST /review-billing-transfer/decline`
  - `POST /reviews`

  Do not add review integration code unless the customer already uses reviews or explicitly requests it.

  ## Error migration

  Remove dependencies on the legacy wrapper:

  ```json
  {
    "error": {
      "code": "not_found",
      "message": "..."
    }
  }
  ```

  The current API uses HTTP status plus a tagged object:

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

  Branch on the HTTP status first and `_tag` second. Do not expose whether an inaccessible resource exists in another organization; inaccessible and absent resources intentionally share the same `404` behavior.

  ## Acceptance criteria

  - No production code, tests, examples, or configuration defaults call `https://capy.ai/api/v1`.
  - Project IDs come from configuration or `GET /projects`; nothing depends on the legacy `limit`/offset pagination, because the current list is unpaged.
  - A thread create supplies a stable `requestId`, uses `message`, and handles the current `Thread` response.
  - Message sends use `text` and `delivery`; message retries cannot silently duplicate work.
  - Stop behavior uses `/interrupt`; stop-and-archive behavior invokes both operations explicitly.
  - Pagination uses the response `cursor`, `after` where required, and `null` as the completion condition.
  - Transcript handling uses `text` and supports `source: "tool"`.
  - Subagent handling uses `/threads/{threadId}/subagents` (or the equivalent `/tasks` routes), opaque subagent IDs, and root-thread mutations.
  - Error handling uses HTTP status and `_tag`, not legacy `error.code`.
  - Any dependency on an unsupported public capability is reported with file and line references.
  - Relevant formatting, lint, typecheck, and existing tests pass.
  - No secret value appears in source, logs, fixtures, commits, or the final report.

  ## Required final report

  Return:

  1. The files changed and the migration made in each.
  2. Retired endpoints removed or replaced.
  3. Unsupported capabilities that block full parity.
  4. Checks run and their exact results.
  5. Customer cutover steps, including configuration and key rotation.
  ````
</div>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.