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) orGET /api/v2/leads(cross-stream polling)
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 viaPOST, 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. Onestream 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-derivedexported_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 viaPOST /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 viaAuthorization: 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).