Plug your system into registry screening.
Point your CRM, dialer, or list workflow at LeadLoom and get National Do Not Call Registry results back on your own files. Fund a balance once, then upload, quote, run and download over a small REST API. Billed per unique valid phone number, held when a job starts and charged when it completes.
At a glance
| Base URL | https://leadloom.thewoob.com |
|---|---|
| Authentication | Authorization: Bearer llt_... on every /api/tools/v1 call. The upload, mapping and download endpoints are authorized by the unguessable upload token instead. |
| Format | JSON in and out; the result file is CSV. Money is always integer cents. |
| Price | $0.10 per unique valid phone number per job. No per-file minimum. The only minimum is the $25.00 top-up. |
| Limits per file | 10,000 unique valid phone numbers, 20 MB, 50,000 rows. Larger files by custom request. |
| Rate limits | 6 uploads and 30 job starts per 60 seconds per IP. You get a 429 with a message; back off and retry. |
| Retention | Uploaded files and results are deleted 30 days after upload. |
| Machine-readable spec | OpenAPI 3.0 at /api/tools/v1/openapi.json, generated from the same values as this page. |
| Funding and keys | Top-ups and key rotation happen in the portal. The API reads your balance and runs jobs; it never takes a card. |
Quick start
Five calls from an empty account to an annotated file.
1. Fund a balance and get your key
Add funds at https://leadloom.thewoob.com/tools. Your first top-up creates the account and emails your access key once. Store it like a password.
2. Upload the file
curl -s -F "[email protected]" {{BASE}}/api/dnc-upload/analyze
Returns 201 with a token, the detected phone columns and quote.unique_valid_phones. If status is needs_mapping, tell us which columns hold phone numbers:
curl -s -X POST {{BASE}}/api/dnc-upload/{{TOKEN}}/mapping \
-H "Content-Type: application/json" \
-d '{"phone_fields": ["Phone", "Mobile"]}'
3. Quote it against your balance
curl -s "{{BASE}}/api/tools/v1/dnc/quote?upload_token={{TOKEN}}" \
-H "Authorization: Bearer $LEADLOOM_KEY"
4. Start the job
curl -s -X POST {{BASE}}/api/tools/v1/dnc/jobs \
-H "Authorization: Bearer $LEADLOOM_KEY" \
-H "Content-Type: application/json" \
-d '{"upload_token": "{{TOKEN}}", "idempotency_key": "batch-2026-09-12-001"}'
201 means the hold is placed and the job is queued. Retrying the same idempotency_key returns the same job with 200 and never holds money twice.
5. Poll, then download
curl -s {{BASE}}/api/tools/v1/jobs/{{JOB_ID}} -H "Authorization: Bearer $LEADLOOM_KEY"
# when "status" is "complete", "download_url" is present:
curl -s -o results.csv {{BASE}}{{DOWNLOAD_URL}}
Small files finish in seconds; a file at the limit takes a few minutes. Poll every few seconds. The job also has a private status_url page you can hand to a teammate.
Concepts
Billable unit
One unique valid phone number in the file. The same number in ten rows is checked and billed once. Blank cells and values that do not normalize to ten digits are not billed. quote.unique_valid_phones on the upload and billable_units on the job are the same number.
Holds, not charges
Starting a job holds billable_units x unit_price_cents. Completion turns the hold into the charge (final_cents). Failure releases it. Your available_cents already excludes open holds; held_cents shows them.
Idempotency
Generate one idempotency_key per intended job and reuse it on retries. Same key, same job. A refused job (insufficient balance) replays as the same refusal. An upload token can be bound to exactly one job, so a second request for the same file returns the first job even without a key.
Two ways to pay
The upload endpoint is shared with the pay-per-file product, so its quote.amount includes that product's per-file minimum. Balance jobs have no per-file minimum: price them with /api/tools/v1/dnc/quote.
Statuses
| Job status | Meaning |
|---|---|
queued | Hold placed, waiting for a worker. |
running | Being screened. |
complete | Result ready; final_cents charged; download_url and summary present. |
failed | Could not finish after retries; hold released; nothing charged. |
rejected | Never started, usually insufficient balance; nothing held. |
What comes back
The result CSV keeps every original row and column, in order. For each phone column you selected, four columns are appended:
| Column | Content |
|---|---|
<field> Normalized | Ten-digit number, or blank |
<field> DNC Result | One of Registry matchNo registry match foundInvalid numberNot checked |
<field> DNC Checked At | ISO timestamp of the check |
<field> DNC Source | The registry the result came from |
The job summary carries the counts: rows, unique_valid_phones, registry_match_phones, no_match_phones, invalid_phone_cells, blank_phone_cells.
Endpoints
Multipart body with one file (CSV or XLSX). No key required; the response token is the credential for this upload.
{
"token": "0TFfu7rlRkgZrNOfFlyld1KjKQn0Q16Q",
"status": "analyzed",
"row_count": 12,
"headers": ["First Name", "Last Name", "Phone", "City"],
"suggested_fields": ["Phone"],
"phone_fields": ["Phone"],
"quote": {"unique_valid_phones": 9, "price_per_phone": 0.10, "minimum": 10.00, "amount": 10.00},
"expires_at": "2026-10-12T18:41:14+00:00"
}
Errors: 400 file_required, 413 file_too_large, 429 rate_limited, 400 with a validation message for unreadable files.
Body {"phone_fields": [...]}. Re-analyzes with those columns and returns the same shape as analyze. Use when status is needs_mapping, or to check more than one phone column per row.
Bearer key required. The server-side price for a balance job, so nothing is ever multiplied on your side.
{
"upload_token": "0TFfu7rlRkgZrNOfFlyld1KjKQn0Q16Q",
"upload_status": "analyzed",
"input_rows": 12,
"billable_units": 9,
"unit_price_cents": 10,
"total_cents": 90,
"available_cents": 2500,
"max_unique": 10000
}Bearer key with the dnc:run scope. Body {"upload_token": "...", "idempotency_key": "..."}. Returns 201 on creation, 200 on an idempotent replay.
{
"id": 1,
"service": "dnc",
"status": "queued",
"upload_status": "queued",
"billable_units": 9,
"unit_price_cents": 10,
"reserved_cents": 90,
"final_cents": null,
"price_version": "dnc:10c",
"input_rows": 12,
"idempotency_key": "batch-2026-09-12-001",
"original_filename": "leads.csv",
"summary": null,
"status_url": "/dnc-list-scrubbing/order/0TFfu7rlRkgZrNOfFlyld1KjKQn0Q16Q",
"error": null,
"created_at": "2026-09-12T14:03:22+00:00",
"retention_until": "2026-10-12T18:41:14+00:00"
}
Errors: 401 unauthorized, 403 insufficient_scope, 400 upload_token_required, 404 not_found, 409 upload_not_ready (with upload_status), 409 nothing_to_bill, 409 needs_custom, 409 insufficient_balance (with available_cents and needed_cents), 429 rate_limited, 503 screening_unavailable, 503 provider_capacity.
Bearer key. Same shape as above; when complete, final_cents, summary and download_url are filled. Jobs belong to the key's account only; another account's job id is a 404.
{
"id": 1,
"status": "complete",
"upload_status": "complete",
"final_cents": 90,
"summary": {"rows": 12, "unique_valid_phones": 9, "registry_match_phones": 3,
"no_match_phones": 6, "invalid_phone_cells": 1, "blank_phone_cells": 1},
"download_url": "/api/dnc-upload/0TFfu7rlRkgZrNOfFlyld1KjKQn0Q16Q/download",
"completed_at": "2026-09-12T14:03:31+00:00"
}Bearer key. {"jobs": [...]}, newest first, up to 100.
The result CSV (UTF-8 with a byte-order mark so Excel opens it cleanly). Authorized by the token. Available until retention_until.
{
"account_email": "[email protected]",
"available_cents": 2410,
"held_cents": 0,
"spent_cents": 90,
"funded_cents": 2500,
"unit_price_cents": 10,
"topup_min_cents": 2500,
"topup_presets_cents": [2500, 5000, 10000, 25000]
}Bearer key. {"entries": [...]}, newest first. Kinds: topup, reserve (negative, the hold), finalize (usage_cents is what the job cost; amount_cents is any part of the hold returned), release (a failed job's hold coming back), adjust and promo (operator entries, always with a reason).
Errors
Every error is {"error": "customer-safe message", "code": "stable_code"}, sometimes with extra fields named above. Codes are stable; messages may change.
| HTTP | Code | What to do |
|---|---|---|
| 400 | upload_token_required, file_required, invalid_phone_mapping | Fix the request. |
| 401 | unauthorized | Missing, wrong or revoked key. |
| 403 | insufficient_scope | The key cannot do that. |
| 404 | not_found | Unknown token, or a job that is not yours. |
| 409 | insufficient_balance | Add funds in the portal; needed_cents tells you how much. Nothing was held. |
| 409 | upload_not_ready, nothing_to_bill, needs_custom | Map the phone columns, or the file is empty of phones, or above the self-service limit. |
| 413 | file_too_large | Split the file. |
| 429 | rate_limited | Back off and retry. |
| 503 | screening_unavailable, provider_capacity, payment_unavailable | Temporary. Retry later; nothing was held. |
Your data
- Your file is processed for one purpose: producing your result. The records in it are never added to any LeadLoom database, sold, shared or reused.
- Only the phone numbers are sent to the registry source. Names, addresses and every other column stay with us and come back to you unchanged.
- Uploaded files and results live in private storage behind unguessable links and are deleted 30 days after upload. Download your results before then.
- Each checked phone number is kept with its registry result in an internal lookup cache and reused for at most 30 days, without any other column from your file.
- Access keys are stored only as a hash. The plaintext exists once, in the email that delivers it. Rotate in the portal; the old key stops immediately. Never put a key in a URL.
- Keys grant access to your own account only. They cannot read LeadLoom data products or anyone else's files.
What the results mean
LeadLoom reports what the National Do Not Call Registry source returned for each number at the time of the check, labeled with one of the four values above. That is registry information, delivered accurately and quickly, with your file intact. It is not legal advice, and it does not by itself establish that any call or text is permitted: consent, established business relationships, state lists and your own internal do-not-call list are yours to manage. Read the Terms for the full statement.
Help
Stuck, need a larger limit, or lost a key? Contact LeadLoom from the email on the account. Balances never expire, and unused balance is refunded to the original payment method on request.