LeadLoom Tools · API reference

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 URLhttps://leadloom.thewoob.com
AuthenticationAuthorization: Bearer llt_... on every /api/tools/v1 call. The upload, mapping and download endpoints are authorized by the unguessable upload token instead.
FormatJSON 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 file10,000 unique valid phone numbers, 20 MB, 50,000 rows. Larger files by custom request.
Rate limits6 uploads and 30 job starts per 60 seconds per IP. You get a 429 with a message; back off and retry.
RetentionUploaded files and results are deleted 30 days after upload.
Machine-readable specOpenAPI 3.0 at /api/tools/v1/openapi.json, generated from the same values as this page.
Funding and keysTop-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 statusMeaning
queuedHold placed, waiting for a worker.
runningBeing screened.
completeResult ready; final_cents charged; download_url and summary present.
failedCould not finish after retries; hold released; nothing charged.
rejectedNever 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:

ColumnContent
<field> NormalizedTen-digit number, or blank
<field> DNC ResultOne of Registry matchNo registry match foundInvalid numberNot checked
<field> DNC Checked AtISO timestamp of the check
<field> DNC SourceThe 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

POST/api/dnc-upload/analyze

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.

POST/api/dnc-upload/<token>/mapping

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.

GET/api/tools/v1/dnc/quote?upload_token=<token>

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
}
POST/api/tools/v1/dnc/jobs

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.

GET/api/tools/v1/jobs/<id>

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"
}
GET/api/tools/v1/jobs

Bearer key. {"jobs": [...]}, newest first, up to 100.

GET/api/dnc-upload/<token>/download

The result CSV (UTF-8 with a byte-order mark so Excel opens it cleanly). Authorized by the token. Available until retention_until.

GET/api/tools/v1/balance
{
  "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]
}
GET/api/tools/v1/usage

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.

HTTPCodeWhat to do
400upload_token_required, file_required, invalid_phone_mappingFix the request.
401unauthorizedMissing, wrong or revoked key.
403insufficient_scopeThe key cannot do that.
404not_foundUnknown token, or a job that is not yours.
409insufficient_balanceAdd funds in the portal; needed_cents tells you how much. Nothing was held.
409upload_not_ready, nothing_to_bill, needs_customMap the phone columns, or the file is empty of phones, or above the self-service limit.
413file_too_largeSplit the file.
429rate_limitedBack off and retry.
503screening_unavailable, provider_capacity, payment_unavailableTemporary. Retry later; nothing was held.

Your data

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.