Everything you need to integrate Close3x: submit lists, poll results, and react to webhooks. Base URL https://close3x.com/api.
The Close3x API lets you submit contact lists for phone-intent scoring and retrieve prioritized results programmatically. It mirrors the in-app flow: upload a CSV, we score every number, and you get back contacts ranked High Intent → Marketing Only, bad data filtered out.
All requests are JSON or multipart over HTTPS. The base URL is:
https://close3x.com/api
Authenticate every request with a Bearer API key. Create and manage keys in Developers → API Keys. Keys are shown once on creation, so store them securely.
Authorization: Bearer ta_live_xxxxxxxxxxxxxxxxxxxxxxxx
401.Each upload is charged in credits based on its scoring type and the number of phone numbers. Credits are managed by your admin.
| Scoring type | Param | Turnaround | Cost |
|---|---|---|---|
| Quick Score | quick | < 24 hours | 1 credit / phone |
| Deep Score | deep | < 5 days | 3 credits / phone |
Cost is reserved up front (rows × rate); invalid rows are refunded when scoring completes. Track everything in Credits.
/v1/uploadsMultipart form fields:
| Field | Required | Description |
|---|---|---|
| file | yes | CSV with a header row. ≤100 MB, 100–250,000 rows. |
| phoneColumn | yes* | Header of the phone column. *Optional when enrichment=true. |
| scoringType | no | quick (default) or deep. |
| skipMobileLookup | no | true to skip carrier lookup (faster). |
| enrichment | no | true to find phones (+ email) before scoring. |
| firstNameColumn | if enriching | Header of the first-name column. |
| lastNameColumn | if enriching | Header of the last-name column. |
| companyColumn / linkedinColumn / websiteColumn | if enriching | Provide at least one to identify the contact. |
enrichment=true we find the best phone (and email) for each contact, then score it. You're billed per contact we find: rows that already have a phone cost the normal scoring rate, and contacts we can't find are free. Status passes through enriching before processing.curl -X POST https://close3x.com/api/v1/uploads \ -H "Authorization: Bearer ta_live_..." \ -F "file=@contacts.csv" \ -F "phoneColumn=Phone" \ -F "scoringType=quick"
Response 201:
{
"id": "cmq...",
"status": "processing",
"scoringType": "quick",
"creditsReserved": 1500,
"validRowCount": 1460,
"counts": { "HIGH_INTENT": 0, "LOW_INTENT": 0, "MARKETING_ONLY": 0, "BAD_DATA": 0, "UNVERIFIED": 0 }
}/v1/uploads/{id}Poll until status is completed (or react to the webhook instead). Lifecycle:
ready_for_processing → processing → completed
↘ failed | cancelledGET/v1/uploads lists all your uploads.
/v1/uploads/{id}/results| Query | Description |
|---|---|
| bucket | callList (High+Low Intent), HIGH_INTENT, LOW_INTENT, MARKETING_ONLY, BAD_DATA, UNVERIFIED, or all. Default callList. |
| page | 1-based page number. |
| pageSize | Rows per page (max 200, default 50). |
| search | Filter by phone substring. |
GET https://close3x.com/api/v1/uploads/{id}/results?bucket=callList&page=1
{
"items": [
{ "id": "...", "phone": "(415) 555-0142", "priority": "HIGH_INTENT", "odStatus": "Likely Answer", "data": { ... } }
],
"total": 66, "page": 1, "pageSize": 50
}Every scored contact lands in one bucket. Suggested playbook:
| Bucket | Meaning | Play |
|---|---|---|
| High Intent | Right person, very likely to answer | Call only, every other day. No voicemails, no emails. |
| Low Intent | Right person, lower connect rate | Call → voicemail → email cadence, then call again. |
| Marketing Only | Phone won't perform yet | Email / LinkedIn until a signal, then layer on calls. |
| Bad Data | Not dialable | Email / LinkedIn only. Never use the phone. |
| Unverified | A number was found during enrichment but not confirmed live (never scored, never charged) | Manually verify before dialing, or work it via email / LinkedIn. |
Register endpoints in Developers → Webhooks. We POST these events:
| Event | Fires when |
|---|---|
| list.completed | A list finishes scoring (includes counts). |
| list.failed | A list fails to process. |
Payload:
{
"id": "evt_...",
"type": "list.completed",
"created_at": "2026-06-08T10:28:00Z",
"data": {
"upload": {
"id": "cmq...",
"filename": "contacts.csv",
"status": "completed",
"scoring_type": "quick",
"valid_row_count": 1460,
"counts": { "HIGH_INTENT": 300, "LOW_INTENT": 360, "MARKETING_ONLY": 610, "BAD_DATA": 230, "UNVERIFIED": 15 }
}
}
}Each request carries X-Close3x-Signature: t=<unix>,v1=<hex>. Verify it (reject if the timestamp is >5 min old):
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(parts.t + "." + rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}2xx quickly to acknowledge.Errors return a JSON body { "error": "message" } with one of:
| Code | Meaning |
|---|---|
| 400 | Malformed request or CSV. |
| 401 | Missing / invalid API key. |
| 402 | Not enough credits, ask your admin to top up. |
| 404 | Resource not found. |
| 413 | File too large (>100 MB / 250k rows). |
| 422 | Fewer than 100 valid phone numbers. |
| 429 | Rate limited, retry after a short delay. |
Machine-readable spec for codegen and import into Postman/Insomnia:
https://close3x.com/api/openapi.json