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 -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.
| Endpoint | Purpose |
|---|---|
| GET /v1/me | Account, plan, key prefix, credit balance |
| GET /v1/usage | Balance plus recent ledger entries (?limit=) |
| POST /v1/verify | Synchronous email verification · 0.02 credits |
| POST /v1/enrich | Email/phone enrichment, ≤100 contacts → run_id |
| POST /v1/whois | Reverse email lookup, ≤100 emails → run_id |
| GET /v1/runs/{id} | Poll an async run for status and results |
| POST /v1/search/people | Scoped people search · 0.25 credits |
| POST /v1/search/companies | Company search · 0.25 credits |
| POST /v1/lookalike/companies | Similar companies · 0.35 credits/row |
| POST /v1/lookalike/people | Similar 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.
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
| HTTP | Code | Meaning |
|---|---|---|
| 401 | invalid_key | Key missing, malformed, or revoked |
| 402 | insufficient_credits · subscription_required | Balance too low, or no active subscription |
| 404 | not_found | Unknown route or run id |
| 422 | invalid_input | Request body failed validation |
| 429 | rate_limited | Per-key limit hit; includes retry_after_seconds |
| 502 | upstream_error | A data source failed; retry later |
Every non-2xx response carries one shape:
{ "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.