close3x


API Reference

Everything you need to integrate Close3x: submit lists, poll results, and react to webhooks. Base URL https://close3x.com/api.

Introduction

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 IntentMarketing Only, bad data filtered out.

All requests are JSON or multipart over HTTPS. The base URL is:

https://close3x.com/api

Authentication

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
A request with a missing, malformed, or revoked key returns 401.

Scoring types & credits

Each upload is charged in credits based on its scoring type and the number of phone numbers. Credits are managed by your admin.

Scoring typeParamTurnaroundCost
Quick Scorequick< 24 hours1 credit / phone
Deep Scoredeep< 5 days3 credits / phone

Cost is reserved up front (rows × rate); invalid rows are refunded when scoring completes. Track everything in Credits.

Create an upload

POST/v1/uploads

Multipart form fields:

FieldRequiredDescription
fileyesCSV with a header row. ≤100 MB, 100–250,000 rows.
phoneColumnyes*Header of the phone column. *Optional when enrichment=true.
scoringTypenoquick (default) or deep.
skipMobileLookupnotrue to skip carrier lookup (faster).
enrichmentnotrue to find phones (+ email) before scoring.
firstNameColumnif enrichingHeader of the first-name column.
lastNameColumnif enrichingHeader of the last-name column.
companyColumn / linkedinColumn / websiteColumnif enrichingProvide at least one to identify the contact.
With 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 }
}

Get status

GET/v1/uploads/{id}

Poll until status is completed (or react to the webhook instead). Lifecycle:

ready_for_processing → processing → completed
                                  ↘ failed | cancelled

GET/v1/uploads lists all your uploads.

Fetch results

GET/v1/uploads/{id}/results
QueryDescription
bucketcallList (High+Low Intent), HIGH_INTENT, LOW_INTENT, MARKETING_ONLY, BAD_DATA, UNVERIFIED, or all. Default callList.
page1-based page number.
pageSizeRows per page (max 200, default 50).
searchFilter 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
}

Priority buckets

Every scored contact lands in one bucket. Suggested playbook:

BucketMeaningPlay
High IntentRight person, very likely to answerCall only, every other day. No voicemails, no emails.
Low IntentRight person, lower connect rateCall → voicemail → email cadence, then call again.
Marketing OnlyPhone won't perform yetEmail / LinkedIn until a signal, then layer on calls.
Bad DataNot dialableEmail / LinkedIn only. Never use the phone.
UnverifiedA number was found during enrichment but not confirmed live (never scored, never charged)Manually verify before dialing, or work it via email / LinkedIn.

Webhooks

Register endpoints in Developers → Webhooks. We POST these events:

EventFires when
list.completedA list finishes scoring (includes counts).
list.failedA 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));
}
Failed deliveries are retried up to 5 times. Respond 2xx quickly to acknowledge.

Errors & rate limits

Errors return a JSON body { "error": "message" } with one of:

CodeMeaning
400Malformed request or CSV.
401Missing / invalid API key.
402Not enough credits, ask your admin to top up.
404Resource not found.
413File too large (>100 MB / 250k rows).
422Fewer than 100 valid phone numbers.
429Rate limited, retry after a short delay.

OpenAPI

Machine-readable spec for codegen and import into Postman/Insomnia:

https://close3x.com/api/openapi.json