> ## 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/threads?projectId=$CAPY_PROJECT_ID&limit=1" \
  -H "Authorization: Bearer $CAPY_API_TOKEN"
```

Project IDs carried over from the legacy system, but project discovery is no longer public — keep your existing project ID in configuration, or read it from the project page in the app.

## What changed

| Flow            | Legacy                                                 | Current                                                                                                                                                                                                                                          |
| --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Create thread   | `POST /threads` with `{ projectId, prompt }`           | `POST /threads` with `{ requestId, projectId, message }` plus optional `title`, `model`, `machineSize`. `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 child threads: `GET /threads/{id}/tasks`, `GET /tasks/{taskId}` (statuses `working`, `waiting`, `idle`, `done`, `failed`); 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 dollar totals, token totals, per-member and per-model splits; per-thread 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                                                 |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `GET /projects`, `GET /projects/{projectId}`                                           | Keep the project ID in configuration; manage projects in the app   |
| Tags, setup, snapshots, browser snapshots, automations, 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="/capy-api-migration.txt" download="capy-api-migration.md">
  Copy the migration md
</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 threads, messages, read-only tasks, reviews, and usage reporting.
  - Project IDs from the legacy system are preserved, but project discovery is not public. Keep the existing project ID in configuration or recover it from the selected project page in the Capy app.

  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/threads?projectId=$CAPY_PROJECT_ID&limit=1" \\
    -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`                                                                                              | No public replacement. Read the project ID from configuration or the app.                                                                                                                                                                                                                                         |
  | `GET /projects/{projectId}`                                                                                  | No public replacement. Project configuration is 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}`                                                                                        | Same path, but use only the opaque child-task ID returned by the current API.                                                                                                                                                                                                                                     |
  | Legacy task list/create/message/start/stop/PR routes                                                         | No direct replacement. Read tasks through the root thread and drive work through that root thread.                                                                                                                                                                                                                |
  | `GET /tasks/{taskId}/diff`                                                                                   | No public replacement.                                                                                                                                                                                                                                                                                            |
  | `GET /models`                                                                                                | No public replacement. Omit `model` to use the configured default.                                                                                                                                                                                                                                                |
  | `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 dollar totals, token totals, and per-member and per-model splits. Per-thread credit totals remain on `Thread.usage` and `Task.usage`. |
  | Setup, snapshots, tags, automations, 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"
  }
  ```

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

  `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": "...",
    "title": "Investigate the failing integration",
    "titleCustom": false,
    "status": "active",
    "archived": false,
    "lastModelId": null,
    "usage": {
      "llmCredits": 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 task 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, task trees, and task 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

  Tasks are read-only child threads.

  List a thread's task tree:

  ```text
  GET /threads/{threadId}/tasks?after={taskId}&limit=100
  ```

  Read a task or its transcript:

  ```text
  GET /tasks/{taskId}
  GET /tasks/{taskId}/messages?after={cursor}&limit=50
  ```

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

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

  To direct or stop task work, send a message or interrupt to the task'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`, `vmDollars`, `totalDollars`), window token totals, per-member billed spend, and per-model spend split into billed and unbilled rows. `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`.
  - The integration does not call `GET /projects`; it reads a configured project ID.
  - 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"`.
  - Task handling uses `/threads/{threadId}/tasks`, opaque task 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>
