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

# Migrating from API v1 to v2

> Endpoint and field changes from the legacy API to the stream-centred v2 surface (audit 086 amendment 12, 2026-06).

## Summary

API v2 is a **stream-centred** redesign. Three primary verbs frame the surface:

* **Create streams** - `POST /api/v2/streams` (one-shot: handles + segments + schedule + delivery)
* **Update streams** - `PATCH /api/v2/streams/{id}` (any setting, including full-replace pairs and inline segment creation)
* **Extract leads** - `GET /api/v2/streams/{id}/leads` (per-stream) or `GET /api/v2/leads` (cross-stream polling)

Everything else (sync runs, re-tests, leads, budgets) is reached via stream sub-resources or the small set of identity / utility endpoints. v1 paths return **410 Gone** with a pointer back here.

## Vocabulary

* **Stream** - a campaign. Owns N `(tracked_account, segment)` pairs and shared config (schedule, enrich, delivery, csv-email). The primary resource.
* **Segment** - a reusable filter set (lead description + applied labels + gender + follower-count tiers). Code-side identifier is still `segments`.
* **Lead** - a discovered profile that survived bot-filtering and (optionally) matched one or more segments. Renamed from "people of interest".
* **Lookup** - a one-shot enrichment or NL-to-segment operation for the Chrome extension. Distinct surface at `/api/v2/lookups/*`.

## Endpoint mapping

### Identity

| v1 path          | v2 path                                                                                      |
| ---------------- | -------------------------------------------------------------------------------------------- |
| `GET /api/v1/me` | `GET /api/v2/me` (now includes `accounts`, `credits`, `subscriptionStatus`, `totals` rollup) |

### Streams (new primary resource)

| Old path or behaviour                                                                               | New path                                                                                        |
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `POST /api/v1/tracked-accounts` + `POST /api/v1/saved-searches` + assigning + setting sync schedule | `POST /api/v2/streams` (one-shot: handles + inline segments + everything)                       |
| n/a                                                                                                 | `GET /api/v2/streams` (list campaigns)                                                          |
| n/a                                                                                                 | `GET /api/v2/streams/{id}` (single campaign with pairs + full config)                           |
| `PUT /api/v1/saved-searches/{id}` (filter edit)                                                     | `PATCH /api/v2/streams/{id}` (settings + full-replace `pairs` array with inline-or-id segments) |
| `DELETE /api/v1/saved-searches/{id}`                                                                | `DELETE /api/v2/streams/{id}` (archives the campaign)                                           |
| `POST /api/v1/sync`                                                                                 | `POST /api/v2/streams/{id}/run` (sync every pair in the campaign)                               |
| `POST /api/v1/explore`                                                                              | `POST /api/v2/streams/{id}/re-test` (optional `{accountId?, segmentId?}` body to scope)         |
| `GET /api/v1/sync-status`                                                                           | `GET /api/v2/streams/{id}/runs/{runId}`                                                         |
| `GET /api/v1/sync-history`                                                                          | `GET /api/v2/streams/{id}/runs` (per-stream) OR `GET /api/v2/runs` (flat across all streams)    |

### Leads

| v1 path                          | v2 path                                                                                                                                                                                                          |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /api/v1/people-of-interest` | `GET /api/v2/leads` (with `?streamId`, `?lifecycle=new\|exported`, `?matchConfidence=high\|medium`, `?since=ISO`, `?groupBy=stream`). `matchConfidence` enum changed from `'full'/'maybe'` to `'high'/'medium'`. |
| n/a                              | `GET /api/v2/streams/{id}/leads` (scoped to a stream, with `?summary=true` for counts only)                                                                                                                      |
| `POST /api/v1/feedback`          | `POST /api/v2/leads/{id}/feedback`                                                                                                                                                                               |
| n/a                              | `POST /api/v2/leads/{id}/enrich` (per-lead on-demand enrichment)                                                                                                                                                 |
| n/a                              | `POST /api/v2/leads/{id}/export` (mark exported; sets `x_leads.exported_at`)                                                                                                                                     |

### Account management

Tracked accounts no longer have a public "add" verb - adding happens via stream creation. Management verbs survive:

| v1 path                                                      | v2 path                                                                            |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `GET /api/v1/tracked-accounts`                               | `GET /api/v2/accounts` (active + sleeping lists; `assignedSegmentIds` per account) |
| `POST /api/v1/tracked-accounts`                              | **No public replacement.** Use `POST /api/v2/streams` with the handle in a pair.   |
| `DELETE /api/v1/tracked-accounts/{id}` (sleep)               | `POST /api/v2/accounts/{id}/sleep`                                                 |
| `PUT /api/v1/tracked-accounts/{id}` (wake)                   | `POST /api/v2/accounts/{id}/wake`                                                  |
| `POST /api/v1/tracked-accounts/{id}/change-tier`             | `POST /api/v2/accounts/{id}/change-tier`                                           |
| `PATCH /api/v1/tracked-accounts/{id}` (assign single search) | `PATCH /api/v2/streams/{id}` with the new pair in the array.                       |

### Segments

Editing a segment's filters in place is intentionally NOT exposed (footgun risk for external integrators - mutating a segment affects classification across every stream that references it). Workflow: create a new segment via `POST`, then PATCH the affected streams to swap.

| v1 path                                        | v2 path                                                                  |
| ---------------------------------------------- | ------------------------------------------------------------------------ |
| `GET /api/v1/saved-searches`                   | `GET /api/v2/segments` (with `linkedStreamCount` per row)                |
| `POST /api/v1/saved-searches`                  | `POST /api/v2/segments`                                                  |
| `GET /api/v1/saved-searches/{id}`              | Visible via `GET /api/v2/streams/{id}` (pairs include their segment)     |
| `PUT /api/v1/saved-searches/{id}`              | **Not exposed.** Create a new segment + PATCH affected streams.          |
| `DELETE /api/v1/saved-searches/{id}`           | **Not exposed.** Archive the streams that reference the segment instead. |
| `POST /api/v1/saved-searches/{id}/assignments` | `PATCH /api/v2/streams/{id}` with the new pair in the array.             |

### Sync settings

| v1 path                      | v2 path                                                                                  |
| ---------------------------- | ---------------------------------------------------------------------------------------- |
| `GET /api/v1/sync-settings`  | **Removed.** Per-tier schedules are gone; schedule is per-stream on `streams.syncTimes`. |
| `PUT /api/v1/sync-settings`  | **Removed.** `PATCH /api/v2/streams/{id}` with `syncTimes` + `timezone`.                 |
| `GET /api/v1/explore/budget` | Folded into `GET /api/v2/me.accounts[].monthlyRetest`.                                   |

### Lookups (Chrome extension surface)

| v1 path                                        | v2 path                                          |
| ---------------------------------------------- | ------------------------------------------------ |
| `POST /api/v1/enrich`                          | `POST /api/v2/lookups/enrich`                    |
| `GET /api/v1/enrich/history`                   | `GET /api/v2/lookups/enrich/history`             |
| `POST /api/v1/enrich-email`                    | `POST /api/v2/lookups/enrich-email`              |
| `GET /api/v1/enrich-email/history`             | `GET /api/v2/lookups/enrich-email/history`       |
| `POST /api/v1/saved-searches/from-description` | `POST /api/v2/lookups/segments/from-description` |
| `DELETE /api/v1/api-keys/{id}`                 | `DELETE /api/v2/lookups/api-keys/{id}`           |

### Utility (unchanged path, sometimes shape)

| Path                                         | Notes                                                                                                                                                 |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET, POST /api/v2/dm-templates`             | No change.                                                                                                                                            |
| `GET, PUT, DELETE /api/v2/dm-templates/{id}` | No change.                                                                                                                                            |
| `GET, PUT /api/v2/notification-settings`     | No change.                                                                                                                                            |
| `POST, DELETE /api/v2/webhooks/subscribe`    | Filter shape updated: `segmentId` dropped, `streamId` added, `trackedAccountId` renamed to `accountId`. matchConfidence enum is `'high' \| 'medium'`. |

## Conceptual changes

### Stream is a campaign

The keystone of the v2 redesign. One `stream` row owns N `(tracked_account, segment)` pairs and a shared config (schedule, enrich, delivery, csv-email). A segment can appear in multiple streams. A stream can contain multiple segments AND/OR multiple accounts in any combination.

### One-shot stream creation

`POST /api/v2/streams` accepts handles (resolves via ScrapeBadger -> kaitoeasyapi, upserts tracked accounts, increments Stripe quantity) AND inline segment objects (creates new `segments` rows) in one call. Existing accounts/segments can be referenced by ID instead.

### Full-replace pair updates

`PATCH /api/v2/streams/{id}` accepts a `pairs` array as full-replace semantics: server diffs against existing pairs, adds new (upserting accounts + creating inline segments), removes ones not in the new array.

### Lead lifecycle

Leads carry a server-derived `exported_at` field. `?lifecycle=new` filters for `IS NULL` (default). `?lifecycle=exported` filters for `IS NOT NULL`. Exporting is done via `POST /api/v2/leads/{id}/export`.

### matchConfidence enum

`'full' / 'maybe'` -> `'high' / 'medium'` (column rewrite in migration 0113). Reflected in `/api/v2/leads`, `/api/v2/streams/{id}/leads`, webhook filter config.

### Per-account billing visibility

`GET /api/v2/me` exposes per-account daily-sync slots + monthly re-test budgets, plus credit balance + low-balance flag, plus a `totals` rollup ("how much headroom do I have anywhere right now"). No standalone `/api/v2/budget` - it's folded into `/me`.

### Tracked accounts are second-class in the API

Adding accounts happens via stream creation. Removing pairs leaves orphan accounts (tracked but referenced by no active stream); orphans stay tracked + billed until explicitly slept via `POST /api/v2/accounts/{id}/sleep`. Account-level operations (sleep / wake / change-tier) survive as `/api/v2/accounts/{id}/<verb>`.

### Webhook filter

Filter config supports `{streamId?, accountId?, sourceType?, feedbackStatus?, matchConfidence?}`. `segmentId` is removed. Existing subscriptions stored with `segmentId` filter silently match nothing post-deploy (the filter was a TODO that never actually applied anyway). Re-subscribe with `streamId` if you want filtering.

## Paywall error codes

Every server gate that surfaces a paywall returns `{ error: '<code>', message: '<copy>' }` with HTTP 403 (or 400 / 429 where appropriate). Branch on the code, not the message text.

| Code                        | When                                                     | HTTP |
| --------------------------- | -------------------------------------------------------- | ---- |
| `free_retest_limit`         | Free user tries to re-test                               | 403  |
| `retest_limit`              | Starter/Growth re-test budget exhausted                  | 403  |
| `retest_limit_pro`          | Pro re-test budget exhausted                             | 403  |
| `daily_sync_limit`          | Account hit its daily sync cap                           | 403  |
| `free_account_limit`        | Free user trying to add 2nd account                      | 403  |
| `free_account_ip_duplicate` | Free user shares signup IP                               | 409  |
| `subscription_required`     | Paid user with lapsed sub                                | 403  |
| `stream_cap_exceeded`       | Tier cap of automated streams per account exceeded       | 400  |
| `stream_multi_segment`      | A stream targets exactly one segment; pairs spanned more | 400  |
| `credits_exhausted`         | Enrichment endpoints out of credits                      | 429  |
| `handle_not_found`          | X handle didn't resolve                                  | 404  |
| `handle_protected`          | X handle is a private account                            | 422  |
| `handle_lookup_failed`      | Upstream lookup service down                             | 503  |
| `orphan_label`              | Segment write contained labels with no canonical match   | 400  |

## Auth (unchanged)

API key auth via `Authorization: Bearer <ctgo_…>` header. Existing PATs continue working post-deploy.

In-app UI calls (cookie session) work transparently on the hybrid-auth routes (`/api/v2/streams*`, `/api/v2/leads*`, `/api/v2/segments`, `/api/v2/accounts/{id}/*`, `/api/v2/me`, `/api/v2/runs`).

## Rate limits (unchanged)

60 requests per minute per Bearer key. Cookie-session in-app calls are not rate-limited at this layer (session middleware gates them).
