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:
curl -sS "https://api.capy.ai/api/v1/threads?projectId=$CAPY_PROJECT_ID&limit=1" \
-H "Authorization: Bearer $CAPY_API_TOKEN"
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) |
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:
{ "_tag": "capy/ThreadNotFound", "threadId": "jam_..." }
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 |
Cutover checklist
- No code, tests, or configuration reference
https://capy.ai/api/v1 - Thread creation sends a stable
requestIdand handles the currentThreadshape - Stop paths call
/interrupt; pagination trustscursorand treatsnullas done - Error handling branches on HTTP status and
_tag - The old key is revoked after the new organization-scoped key is live
Agent brief
Copy the migration md 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.# 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.