Getting started
Three calls do everything: send a file, ask whether it is finished, download the result. Everything is ordinary HTTPS and multipart form uploads — there is no SDK to install and nothing unusual to support.
You need two things:
- An account at cleanmydatabase.com with credits on it. Create one free.
- An API key, made in Your account → API keys.
Base URL for every endpoint:
https://cleanmydatabase.com/api/v1/
Authentication
Send your key as a bearer token on every request.
Authorization: Bearer cmd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
If your HTTP client or hosting strips the Authorization header, send the key as X-API-Key instead — we accept either.
Checks available
Pass these as a comma-separated services value. Combine as many as you like in one file.
| Value | Check | Needs | Columns added to your file |
|---|---|---|---|
| tps | TPS / CTPS screening | A phone column | On TPS, On CTPS, On DNC |
| hlr | HLR mobile validation | A phone column | Mobile Live/Dead, Network, Switched On |
| Email validation | An email column | Email Quality, Email Reason, Free Email Service, Disposable Email, Role Address, Catch-all Domain |
Other checks exist but are switched on per account. Ask us if you need one that isn’t listed.
Send a file
A multipart/form-data upload. CSV or XLSX, up to 45MB. Every column in your file is kept and returned; we add the result columns alongside.
Fields
| Field | Required | What it does |
|---|---|---|
| file | Yes | The list itself, as a file upload. |
| services | Yes | Which checks to run, e.g. tps,hlr. |
| phone_column | No | The column holding phone numbers — a header name (Mobile) or a number starting at 0. Left out, we work it out from the file. |
| email_column | No | Same, for email addresses. |
| has_header | No | 1, 0, or auto (the default — we detect it). |
| country | No | Where the numbers are from, e.g. GB, US. Only used for numbers that don’t already carry a country code. Defaults to GB. |
| reference | No | Anything you want handed back on every status response — your own batch id, for example. |
| notify_email | No | Email a download link to a person as well. Left out, nobody is emailed and your software just polls. |
Example
curl -X POST https://cleanmydatabase.com/api/v1/jobs.php \ -H "Authorization: Bearer cmd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -F "file=@leads.csv" \ -F "services=tps,hlr" \ -F "phone_column=Mobile" \ -F "reference=batch-4471"
Response 201 Created
{
"job": {
"id": "k3Rd8Qx2mVaP7yLsT1wNbZfH",
"status": "running",
"reference": "batch-4471",
"filename": "leads.csv",
"services": ["tps", "hlr"],
"rows": 4820,
"processed": 0,
"percent": 0,
"credits_used": { "tps": 4820, "hlr": 4796 },
"created_at": "2026-09-21T14:02:55+01:00",
"status_url": "https://cleanmydatabase.com/api/v1/job.php?id=k3Rd8Qx2mVaP7yLsT1wNbZfH",
"result_url": null
}
}
credits_used is what was taken from your balance. Note that the two numbers can differ: we only charge for rows that actually have a value in the column being checked, so blank cells cost nothing.
Check progress
Poll this until status is complete. Every 10 seconds is plenty; each call also pushes the clean along, so small files often finish on the second poll. Large files run at roughly 20,000–40,000 lookups an hour.
{
"job": {
"id": "k3Rd8Qx2mVaP7yLsT1wNbZfH",
"status": "complete",
"reference": "batch-4471",
"rows": 4820,
"processed": 4820,
"percent": 100,
"summary": {
"records": 4820,
"ok_to_contact": 3611,
"with_errors": 0,
"tps_or_ctps_registered": 702,
"dead_mobiles": 486,
"invalid_mobiles": 21,
"not_mobiles": 0
},
"completed_at": "2026-09-21T14:19:02+01:00",
"result_url": "https://cleanmydatabase.com/api/v1/result.php?id=k3Rd8Qx2mVaP7yLsT1wNbZfH"
}
}
To list your recent jobs instead, GET /api/v1/jobs.php (optionally ?limit=50).
Download results
Returns the cleaned file itself as text/csv, not JSON. Your original columns come back untouched and in the same order, followed by a column per check, then Check Errors and Recommendation.
curl -L "https://cleanmydatabase.com/api/v1/result.php?id=JOB_ID" \ -H "Authorization: Bearer cmd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -o leads_cleaned.csv
Recommendation is the column to key your automation off. It reads in plain English — OK to contact, Do not call – TPS, Dead mobile, Do not email — so you can map it straight to a disposition without parsing anything.
Results are kept for 30 days and then deleted automatically. Download them within that window.
Balance & prices
Worth calling before you send a large file, so you can top up rather than have the job rejected.
{
"account": { "company": "Acme Contact Centre", "email": "ops@acme.co.uk" },
"checks": {
"tps": { "name": "TPS / CTPS screening", "needs": "phone", "credits": 128400, "price_per_record": 0.005, "currency": "GBP" },
"hlr": { "name": "HLR mobile validation", "needs": "phone", "credits": 96120, "price_per_record": 0.008, "currency": "GBP" }
},
"limits": { "max_file_bytes": 47185920, "max_running_jobs": 5, "results_kept_days": 30 }
}
Errors
Anything that isn’t a success returns the same shape, with a machine-readable code and a message you can safely show your own users.
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits for this file. Nothing has been charged and no lookups were made.",
"details": {
"required": { "tps": 4820, "hlr": 4796 },
"short": { "hlr": { "needed": 4796, "balance": 1200, "shortfall": 3596 } }
}
}
}
| Status | Code | What to do |
|---|---|---|
| 401 | missing_key / invalid_key | Check the Authorization header, and that the key hasn’t been revoked. |
| 400 | missing_column | We couldn’t find the column to check. Name it with phone_column or email_column — the response lists your file’s headers. |
| 400 | bad_file | Not a readable CSV/XLSX, or empty. |
| 402 | insufficient_credits | Top up and send the file again. Nothing was charged and no lookups ran. |
| 409 | job_not_finished | Keep polling the status URL. |
| 410 | result_expired | Past the 30-day retention window. The file would need cleaning again. |
| 413 | file_too_large | Split the file and send the parts. |
| 429 | too_many_running_jobs | Wait for one of your cleans to finish. Usually means a loop at your end sent the same file repeatedly. |
A complete example
Send a file, wait for it, save the result. PHP, no libraries:
<?php
$key = 'cmd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
$base = 'https://cleanmydatabase.com/api/v1';
$auth = array('Authorization: Bearer ' . $key);
// 1. Send the file
$ch = curl_init($base . '/jobs.php');
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $auth,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => array(
'file' => new CURLFile('leads.csv'),
'services' => 'tps,hlr',
'phone_column' => 'Mobile',
),
));
$job = json_decode(curl_exec($ch), true);
curl_close($ch);
if (isset($job['error'])) {
exit('Could not start: ' . $job['error']['message'] . "\n");
}
$id = $job['job']['id'];
// 2. Wait for it
do {
sleep(10);
$ch = curl_init($base . '/job.php?id=' . urlencode($id));
curl_setopt_array($ch, array(CURLOPT_HTTPHEADER => $auth, CURLOPT_RETURNTRANSFER => true));
$status = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $status['job']['percent'] . "%\n";
} while ($status['job']['status'] !== 'complete');
// 3. Save the cleaned file
$ch = curl_init($base . '/result.php?id=' . urlencode($id));
curl_setopt_array($ch, array(CURLOPT_HTTPHEADER => $auth, CURLOPT_RETURNTRANSFER => true));
file_put_contents('leads_cleaned.csv', curl_exec($ch));
curl_close($ch);
The same in Python:
import time, requests
key = 'cmd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
base = 'https://cleanmydatabase.com/api/v1'
head = {'Authorization': 'Bearer ' + key}
job = requests.post(base + '/jobs.php', headers=head,
files={'file': open('leads.csv', 'rb')},
data={'services': 'tps,hlr', 'phone_column': 'Mobile'}).json()
if 'error' in job:
raise SystemExit(job['error']['message'])
job_id = job['job']['id']
while True:
time.sleep(10)
status = requests.get(base + '/job.php', headers=head, params={'id': job_id}).json()
print(status['job']['percent'], '%')
if status['job']['status'] == 'complete':
break
csv = requests.get(base + '/result.php', headers=head, params={'id': job_id})
open('leads_cleaned.csv', 'wb').write(csv.content)
Plug it in and stop thinking about it.
Create an account, make a key and send your first file today. There is no set-up fee and no contract.