Verify a list
Upload a CSV and get it back with a verdict on every row. In the dashboard that is the Bulk verify screen; from code it is four calls. If you would rather the list never leave your systems, the loop at the end of this page does the same job one address at a time.
In the dashboard
Open Verify → Bulk verify, drop a CSV (or paste up to 1,000 addresses), and check what Sortance read: the row count, unique addresses, rows that are not an address at all, and which column holds the addresses (change it if the guess is wrong). The estimate shows how many verifications the list will use, how many are already answered from cache, your remaining quota and the most it can cost. Start it, watch it run, and download the results: every row, or one file per segment.
- Files up to 50 MB and 100,000 rows. Split anything larger.
- Each unique address counts as one verification against your monthly quota, cache hits included. Duplicate rows share one answer. A list that does not fit what remains this month is not started unless you choose to verify the part that fits; a job that runs out pauses and resumes on the 1st, or by itself once the quota is raised (Resume starts it at once). Files are still deleted 7 days after upload, so a job still paused then ends unfinished. Tick “Email me when it’s done” on Start to get one “Your list is ready” email with the counts and a link; it never contains a row of your file.
- Two jobs run at a time per organisation; a third waits its turn.
- The file and its results are kept for 7 days, then deleted. They are the only place Sortance stores raw addresses (see trust). Every verified address also gets an ordinary decision trace, which stays: “Open in Traces” shows the job’s rows there.
- “Block undeliverable addresses” adds that segment to your address blocklist, so those addresses fail every future check for free. An account holds 10,000 address rules, blocklist and allowlist together; the result says how many were added and how many did not fit.
With the API
The same four steps. Full reference: /v1/bulk.
# 1. create a job
curl -X POST https://api.sortance.com/v1/bulk/jobs \
-H "Authorization: Bearer $SORTANCE_KEY" -H "Content-Type: application/json" \
-d '{ "name": "leads.csv" }'
# → 201 { "job": { "id": "bj_…", "state": "uploading", … } }
# 2. upload the file (streamed; the response says what was read)
curl -X PUT https://api.sortance.com/v1/bulk/jobs/bj_…/upload \
-H "Authorization: Bearer $SORTANCE_KEY" -H "Content-Type: text/csv" \
--data-binary @leads.csv
# → { "job": { "state": "ready", "rows_total": 12400, "rows_unique": 11980, "email_column": "email", … } }
# 3. start it (GET …/estimate first if you want the numbers)
curl -X POST https://api.sortance.com/v1/bulk/jobs/bj_…/start -H "Authorization: Bearer $SORTANCE_KEY"
# → 202, then poll GET /v1/bulk/jobs/bj_… until "state": "done"
# 4. download: all rows, or ?segment=deliverable | undeliverable | review | business
curl -o leads.verified.csv \
"https://api.sortance.com/v1/bulk/jobs/bj_…/results.csv?segment=all" \
-H "Authorization: Bearer $SORTANCE_KEY"Or with the JavaScript SDK: verifyList(csv), waitForList(jobId) and downloadList(jobId, segment).
Your columns come back first, unchanged, followed by sortance_status, sortance_verdict, sortance_bounce_probability, sortance_recommendation, sortance_reason, sortance_email_type, the signals and sortance_trace_id. Rows that are not an address are echoed with sortance_status: skipped and cost nothing. Pass a policy_slug when you create the job and each row also gets your policy’s decision, and the segments become allow, deny and review. To keep only business addresses, download business.
To clean a list against your own rules instead, for example “no free or role addresses”, set those on the Email types card of the Policy screen (or describe the rule there), choose the default policy when you start the job, and download deny to see what your rules turn away, or allow to keep the rest. See email types.
The API way: a loop against /v1/verify
A bulk job means the list sits in Sortance’s storage for up to 7 days. If that is not acceptable, keep the list in your database and send it one address at a time from your own system. Nothing about the addresses is stored except a peppered hash. It is about thirty lines:
const KEY = process.env.SORTANCE_KEY
const CONCURRENCY = 8 // well under the 600/min ceiling
async function verify(email, attempt = 0) {
const res = await fetch("https://api.sortance.com/v1/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, context: { purpose: "list_hygiene" } }),
})
if (res.status === 429) {
if (attempt >= 5) throw new Error("rate limited, gave up")
// Exponential with jitter. Without the jitter a whole batch retries
// in lockstep and hits the same ceiling again.
const wait = 2 ** attempt * 250 + Math.random() * 250
await new Promise((r) => setTimeout(r, wait))
return verify(email, attempt + 1)
}
if (res.status === 402) {
// Quota, not rate. Retrying will not help — stop the run.
throw new Error("quota_exceeded")
}
return res.json()
}
async function run(emails) {
const out = []
const queue = [...emails]
await Promise.all(
Array.from({ length: CONCURRENCY }, async () => {
while (queue.length) {
const email = queue.shift()
out.push(await verify(email))
}
}),
)
return out
}What to do with the results
Do not sort into two piles. The whole point of a probability is that the middle exists:
| Condition | Do |
|---|---|
bounce_probability low, interval tight | Send. This is the bulk of a healthy list. |
| Interval wide, probability middling | Usually a catch-all domain. Suppress from cold outreach, keep for transactional mail where you have a prior relationship. |
actor_class: role_account | Deliverable and a bad idea to cold-mail. Two different questions, answered separately. |
degraded: true | Nobody answered. Do not treat this as evidence about the address — re-run those later. |
What it costs to re-run
Re-running the same list inside the cache window is close to free in provider spend: repeated addresses resolve from cache with cost_usd: 0, and addresses the local gate settles never reach a paid provider at all. Each one still counts as a verification against your quota, in a bulk job and in the loop alike. Sortance is priced per thousand sends protected rather than per check precisely so that this is not a reason to check less.
Then report the bounces
A list run is the best calibration data you will ever have. Keep the trace_id against each address, and when your ESP reports a bounce, post it to /v1/outcome. The next run on that list will be fitted to your traffic rather than to everyone else’s.