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 Intent → Marketing 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": { "P1": 0, "P2": 0, "P3": 0, "UNKNOWN": 0, "INVALID": 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 (P1+P2), P1, P2, P3, UNKNOWN, INVALID, 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": "P1", "p1Grade": "PERFECT", "identity": "VERY_HIGH", "rightParty": true, "odStatus": "1 - Likely Right-Party Answer", "data": { ... } }
  ],
  "total": 66, "page": 1, "pageSize": 50
}

Priority buckets

Every scored contact lands in one bucket. A bucket describes answer behaviour only — whether the number reaches the right person is a separate signal, returned as

identity
.

BucketMeaningPlay
P1Picks up the phoneCall only, every other day. No voicemails, no emails.
P2Screens calls before answeringCall → voicemail → email cadence, then call again.
P3Most likely goes to voicemailEmail / LinkedIn until a signal, then layer on calls.
UNKNOWNNot classifiable, or found by enrichment and never confirmed live (never scored, never charged)Work last. Unproven rather than disproven.
INVALIDWrong number, or not a mobileEmail / LinkedIn only. Never use the phone.

Right-person confidence. When you map a name column on upload, each contact also returns an

identity
band —
VERY_HIGH
,
HIGH
,
MEDIUM
,
LOW
or
UNKNOWN
— plus
rightParty
. This is independent of the bucket: a P3 can be a confirmed right party who simply doesn't pick up. P1 contacts also carry a
p1Grade
of
PERFECT
(right party confirmed) or
LIKELY_RIGHT
.

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": { "P1": 300, "P2": 360, "P3": 610, "UNKNOWN": 245, "INVALID": 230 }
    }
  }
}

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