Skip to main content

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 pathv2 path
GET /api/v1/meGET /api/v2/me (now includes accounts, credits, subscriptionStatus, totals rollup)

Streams (new primary resource)

Old path or behaviourNew path
POST /api/v1/tracked-accounts + POST /api/v1/saved-searches + assigning + setting sync schedulePOST /api/v2/streams (one-shot: handles + inline segments + everything)
n/aGET /api/v2/streams (list campaigns)
n/aGET /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/syncPOST /api/v2/streams/{id}/run (sync every pair in the campaign)
POST /api/v1/explorePOST /api/v2/streams/{id}/re-test (optional {accountId?, segmentId?} body to scope)
GET /api/v1/sync-statusGET /api/v2/streams/{id}/runs/{runId}
GET /api/v1/sync-historyGET /api/v2/streams/{id}/runs (per-stream) OR GET /api/v2/runs (flat across all streams)

Leads

v1 pathv2 path
GET /api/v1/people-of-interestGET /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/aGET /api/v2/streams/{id}/leads (scoped to a stream, with ?summary=true for counts only)
POST /api/v1/feedbackPOST /api/v2/leads/{id}/feedback
n/aPOST /api/v2/leads/{id}/enrich (per-lead on-demand enrichment)
n/aPOST /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 pathv2 path
GET /api/v1/tracked-accountsGET /api/v2/accounts (active + sleeping lists; assignedSegmentIds per account)
POST /api/v1/tracked-accountsNo 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-tierPOST /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 pathv2 path
GET /api/v1/saved-searchesGET /api/v2/segments (with linkedStreamCount per row)
POST /api/v1/saved-searchesPOST /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}/assignmentsPATCH /api/v2/streams/{id} with the new pair in the array.

Sync settings

v1 pathv2 path
GET /api/v1/sync-settingsRemoved. Per-tier schedules are gone; schedule is per-stream on streams.syncTimes.
PUT /api/v1/sync-settingsRemoved. PATCH /api/v2/streams/{id} with syncTimes + timezone.
GET /api/v1/explore/budgetFolded into GET /api/v2/me.accounts[].monthlyRetest.

Lookups (Chrome extension surface)

v1 pathv2 path
POST /api/v1/enrichPOST /api/v2/lookups/enrich
GET /api/v1/enrich/historyGET /api/v2/lookups/enrich/history
POST /api/v1/enrich-emailPOST /api/v2/lookups/enrich-email
GET /api/v1/enrich-email/historyGET /api/v2/lookups/enrich-email/history
POST /api/v1/saved-searches/from-descriptionPOST /api/v2/lookups/segments/from-description
DELETE /api/v1/api-keys/{id}DELETE /api/v2/lookups/api-keys/{id}

Utility (unchanged path, sometimes shape)

PathNotes
GET, POST /api/v2/dm-templatesNo change.
GET, PUT, DELETE /api/v2/dm-templates/{id}No change.
GET, PUT /api/v2/notification-settingsNo change.
POST, DELETE /api/v2/webhooks/subscribeFilter 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.
CodeWhenHTTP
free_retest_limitFree user tries to re-test403
retest_limitStarter/Growth re-test budget exhausted403
retest_limit_proPro re-test budget exhausted403
daily_sync_limitAccount hit its daily sync cap403
free_account_limitFree user trying to add 2nd account403
free_account_ip_duplicateFree user shares signup IP409
subscription_requiredPaid user with lapsed sub403
stream_cap_exceededTier cap of automated streams per account exceeded400
stream_multi_segmentA stream targets exactly one segment; pairs spanned more400
credits_exhaustedEnrichment endpoints out of credits429
handle_not_foundX handle didn’t resolve404
handle_protectedX handle is a private account422
handle_lookup_failedUpstream lookup service down503
orphan_labelSegment write contained labels with no canonical match400

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).