Skip to content

API reference

The ContactCTL API

Everything the CLI does goes through one HTTPS API — and that API is yours too. Bearer auth, JSON in and out, typed error codes, and a simple run/poll model for async enrichment. Use it directly for server-side integrations; use the CLI for everything else.

01 — Authentication

One key, one header

Create a key in the dashboard — it looks like ctc_live_… and is shown once. Send it as a Bearer token on every /v1/* request. The base URL is https://contactctl.com/api.

The same key powers the CLI (contactctl auth <key>), so one credential covers both surfaces and one ledger records all spend.

curl
curl -s https://contactctl.com/api/v1/verify \
  -H "Authorization: Bearer ctc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"email": "jane.smith@acme.com"}'

02 — Endpoints

The surface

Ten endpoints cover the whole product. Sync actions return results directly; enrichment returns a run to poll.

EndpointPurpose
GET /v1/meAccount, plan, key prefix, credit balance
GET /v1/usageBalance plus recent ledger entries (?limit=)
POST /v1/verifySynchronous email verification · 0.02 credits
POST /v1/enrichEmail/phone enrichment, ≤100 contacts → run_id
POST /v1/whoisReverse email lookup, ≤100 emails → run_id
GET /v1/runs/{id}Poll an async run for status and results
POST /v1/search/peopleScoped people search · 0.25 credits
POST /v1/search/companiesCompany search · 0.25 credits
POST /v1/lookalike/companiesSimilar companies · 0.35 credits/row
POST /v1/lookalike/peopleSimilar people · 0.35 credits/row

All endpoints require Bearer auth and speak JSON. Full credit costs: docs · pricing

03 — Async model

Submit, then poll the run

Enrichment can take seconds to minutes, so POST /v1/enrich and POST /v1/whois respond immediately with HTTP 202 and a run_id. Poll GET /v1/runs/{id} until status becomes done.

Credits are held at submit time and settled only for contacts where a result was found; holds for misses and failed runs are released. One run carries up to 100 contacts — batch by chunking client-side, or let the CLI do it for you.

run lifecycle
POST /v1/enrich
{ "want": "work_email",
  "contacts": [{ "index": 0, "first_name": "Jane",
    "last_name": "Smith", "domain": "acme.com" }] }

202 → { "run_id": "7c9e…", "status": "pending",
        "contacts_total": 1, "credits_held": 1 }

GET /v1/runs/7c9e…
200 → { "status": "done", "credits_charged": 1,
  "results": [{ "index": 0, "status": "found",
    "contact": { "type": "work_email",
      "value": "jane.smith@acme.com",
      "status": "deliverable", "confidence": 0.88 } }] }

04 — Errors and limits

Typed failures, honest backoff

HTTPCodeMeaning
401invalid_keyKey missing, malformed, or revoked
402insufficient_credits · subscription_requiredBalance too low, or no active subscription
404not_foundUnknown route or run id
422invalid_inputRequest body failed validation
429rate_limitedPer-key limit hit; includes retry_after_seconds
502upstream_errorA data source failed; retry later

Every non-2xx response carries one shape:

error body
{ "error": { "code": "rate_limited",
  "message": "…" }, "retry_after_seconds": 12 }

Rate limit: 120 requests per minute per key, across all endpoints. On 429, wait retry_after_seconds and retry — that is exactly what the CLI does automatically (up to 4 retries, ~90s total), which is one of the reasons we recommend it as the integration surface.

05 — CLI first

When to use which

The CLI is the recommended surface: it adds shape routing, CSV batch handling with atomic writes, automatic 429 backoff, run reattachment, local cost estimates, and deterministic exit codes — behavior you would otherwise rebuild around the raw API. Reach for raw HTTPS when the work happens server-side: webhook handlers, internal services, environments where installing a binary is not an option.