API reference
One call to assess the fraud risk of a phone number, email address and IP address.
Draft contract.
This reference is under review and will change before v1. Field names may still move.
- Base URL
https://api.prooftell.com- Format
- JSON in, JSON out. Money is a decimal string in USD, never a float: four places for the cost of a call, two for balances.
- Keys
- Created in the dashboard:
pt_live_for real calls,pt_test_for the sandbox.
Authentication
Authorization: Bearer pt_live_…, or pt_test_… for sandbox calls (simulated, free).
Keys are created in the dashboard. A missing, unknown or revoked key gets 401 invalid_api_key.
curl https://api.prooftell.com/v1/balance \
-H "Authorization: Bearer pt_live_…"Errors
Every error is a JSON body with a stable code and a readable message; some carry more, such asretry_after or the amounts to top up. Codes are stable, messages are not.
Stable: invalid_api_key, bad_request, invalid_phone, invalid_country, invalid_email, invalid_timeout, invalid_ip, daily_quota_reached, insufficient_balance, rate_limited, signals_unavailable, sandbox_not_available, not_implemented.
{
"error": {
"code": "insufficient_balance",
"message": "Top up to run this signal.",
"cost_required": "0.0010",
"cost_available": "0.0000",
"cost_missing": "0.0010",
"topup_url": "https://app.prooftell.com/billing"
}
}Sandbox
A pt_test_ key turns every signal endpoint into a sandbox: results are simulated and deterministic, no signal source is called, nothing is billed, and an assessment answers with mode: test. Any input works; each endpoint documents the inputs that produce specific results, so you can build and test your integration, an unavailable signal included, before a real key is involved. Files need a live key.
Assess
/v1/assessAssess the risk of an identity
Send any mix of phone, email and IP. Each signal runs in parallel; a signal that cannot run (not supplied, or its source is unavailable) is reported, never guessed.
When a supplied signal is unavailable, the score comes from the signals that ran, the
missing one adds a 0-point reason signal_unavailable_<signal>, and the verdict is raised
to at least review (an organization setting can turn the raise off). When none of the
supplied signals ran, the call fails with 503 signals_unavailable.
The call costs the signals that returned a result, each at the rate card's list price
(cost, currency, and a line per signal); unavailable signals cost nothing. Static
signals draw on the daily quota when the balance cannot cover them.
With a pt_test_ key the call is a sandbox: results are simulated and deterministic, no
signal is called, nothing is billed, and mode is test. Any input works; these produce
specific results: phones +15005550001 (valid mobile), +15005550002 (VoIP),
+15005550003 (invalid), +15005550004 (signal unavailable); emails
<name>@sandbox.prooftell.com with <name> one of allow, undeliverable, disposable,
catchall, greylisted, risky, unavailable; IPs 203.0.113.1 (clean), 203.0.113.66
(Tor), 203.0.113.77 (VPN), 203.0.113.99 (datacenter), 203.0.113.4 (unavailable).
Rate limit: 20 requests a second per key, bursts of 50 (429 rate_limited).
Request body
| Field | Type | Description |
|---|---|---|
| phone | string | E.164 preferred; national format needs country. Example +447700900123. |
| string (email) | Example jane@example.com. | |
| ip | string | IPv4 or IPv6 of the end user. Example 203.0.113.7. |
| country | string | ISO 3166-1 alpha-2 hint for national-format phones. Example GB. |
| reference | string | Your own id for this check, echoed back. Up to 128 characters. |
Responses
curl -X POST https://api.prooftell.com/v1/assess \ -H "Authorization: Bearer pt_live_…" \ -H "Content-Type: application/json" \ -d '{ "phone": "+447700900123", "email": "jane@example.com", "ip": "203.0.113.7", "country": "GB" }'
{
"id": "asm_8f3k2m9q",
"mode": "live",
"reference": "string",
"score": 0,
"verdict": "allow",
"reasons": [
{
"code": "email_disposable",
"points": 40,
"message": "The email address uses a disposable provider."
}
],
"signals": {
"phone": {
"status": "ok",
"cost": "0.0010",
"result": {}
},
"email": {
"status": "ok",
"cost": "0.0010",
"result": {}
},
"ip": {
"status": "ok",
"cost": "0.0010",
"result": {}
}
},
"ruleset": "2026-10-01",
"cost": "0.0045",
"currency": "USD",
"createdAt": "2026-10-01T09:00:00Z"
}Signals
/v1/phoneVerify a phone number
Veriphone's static verification, sold by ProofTell: the response is Veriphone's
/v3/verify JSON in static mode — status, phone_valid, e164, country_code, carrier,
phone_type and the rest, unchanged — plus ProofTell's cost, currency and signals.
A run is charged exactly when Veriphone would have charged it (status is success); a
number that could not be parsed is answered and free.
Charged at the rate card's list price from your balance (cost says what this call cost).
Static runs draw on your organization's daily quota when the balance cannot cover them,
at no charge; when it is used up the call fails with 429 daily_quota_reached and
retry_after seconds. A provider-backed signal the balance cannot cover fails with
402 insufficient_balance and the amounts to top up.
With a pt_test_ key the call is a sandbox: the result is simulated and deterministic, no
signal is called, nothing is billed. Any number works; +15005550001 is a valid mobile,
+15005550002 VoIP, +15005550003 invalid and +15005550004 answers 503 as an
unavailable signal would.
Request body
| Field | Type | Description |
|---|---|---|
| phonerequired | string | E.164 preferred; national format needs country. Up to 32 characters. Example +447700900123. |
| country | string | ISO 3166-1 alpha-2 hint for national-format phones. Example GB. |
Responses
curl -X POST https://api.prooftell.com/v1/phone \ -H "Authorization: Bearer pt_live_…" \ -H "Content-Type: application/json" \ -d '{ "phone": "+447700900123", "country": "GB" }'
{
"status": "success",
"phone_valid": true,
"e164": "+447700900123",
"country_code": "GB",
"carrier": "string",
"phone_type": "string",
"cost": "0.0000",
"currency": "USD",
"signals": {
"static": {
"status": "ok",
"cost": "0.0000"
}
}
}/v1/emailVerify an email address
Verimail's verification, sold by ProofTell: the response is Verimail's /v3/verify JSON —
status, result, verdict, reason, billed, suggested_action, flags and the rest,
unchanged — plus ProofTell's cost, currency and signals. A run is charged exactly when
Verimail would have charged it (its billed is true): syntax errors, unknowns and
softbounces are free.
Same quota rules as /v1/phone. timeout (milliseconds) bounds Verimail's own SMTP
budget; past it the address comes back unknown, free.
With a pt_test_ key the call is a sandbox, simulated and free: any address is
deliverable, and <name>@sandbox.prooftell.com with <name> one of undeliverable,
disposable, catchall, greylisted, risky gives that result; unavailable answers 503.
Request body
| Field | Type | Description |
|---|---|---|
| emailrequired | string | Up to 254 characters. Example jane@example.com. |
| timeout | integer | Verimail's budget for the SMTP dialogue, milliseconds. 1000 to 60000. |
Responses
curl -X POST https://api.prooftell.com/v1/email \ -H "Authorization: Bearer pt_live_…" \ -H "Content-Type: application/json" \ -d '{ "email": "jane@example.com" }'
{
"status": "string",
"result": "string",
"verdict": "string",
"reason": "string",
"billed": true,
"cost": "0.0000",
"currency": "USD",
"signals": {
"email": {
"status": "ok",
"cost": "0.0000"
}
}
}/v1/ipLook up an IP address
ProofTell's own IP intelligence: geography and network from GeoLite2, plus open Tor,
VPN and datacenter lists — all in ProofTell's infrastructure, the address never leaves.
Charged when the address parsed and was looked up; a private or reserved address is
answered (routable: false) and free. Same quota rules as /v1/phone.
With a pt_test_ key the call is a sandbox, simulated and free: 203.0.113.1 is clean,
203.0.113.66 Tor, 203.0.113.77 VPN, 203.0.113.99 datacenter and 203.0.113.4
answers 503.
Request body
| Field | Type | Description |
|---|---|---|
| iprequired | string | IPv4 or IPv6. Example 203.0.113.7. |
Responses
curl -X POST https://api.prooftell.com/v1/ip \ -H "Authorization: Bearer pt_live_…" \ -H "Content-Type: application/json" \ -d '{ "ip": "203.0.113.7" }'
{
"status": "success",
"ip": "string",
"version": 4,
"routable": true,
"country_code": "GB",
"country": "string",
"region": "string",
"city": "string",
"latitude": 0,
"longitude": 0,
"accuracy_km": 0,
"timezone": "string",
"asn": 15169,
"as_org": "GOOGLE",
"is_tor": true,
"is_vpn": true,
"is_datacenter": true,
"is_anonymous": true,
"anycast": true,
"data_version": "string",
"cost": "string",
"currency": "USD",
"signals": {
"ip": {
"status": "ok",
"cost": "0.0000"
}
}
}Files
/v1/filesYour organization's files
curl https://api.prooftell.com/v1/files \
-H "Authorization: Bearer pt_live_…"[
{
"id": "string",
"name": "string",
"status": "uploading",
"stage": "string",
"format": {},
"columns": {
"phone": 0,
"email": 0,
"ip": 0,
"header_row": true
},
"header": [
"string"
],
"samples": [
[
"string"
]
],
"rows": 0,
"unique_rows": 0,
"signals": [
"string"
],
"hold": "string",
"charged": "string",
"counters": {},
"error": "string",
"created_at": "2026-10-01T09:00:00Z",
"expires_at": "2026-10-01T09:00:00Z"
}
]/v1/filesStart a file job
Two ways in. Signed upload (any size): send JSON {name, content_type}; the answer carries
upload_url and headers — PUT the bytes there with exactly those headers (the URL accepts one
upload, for 12 hours), then call finish. Direct upload (up to 32 MB): send the bytes as
multipart/form-data field file; analysis runs at once and the answer is the analysed job.
Upload and analysis are free. CSV, TSV, TXT (one value per line) and XLSX. Files need a
live key: with a pt_test_ key every /v1/files call answers 501 sandbox_not_available.
Request bodyapplication/json · multipart/form-data
| Field | Type | Description |
|---|---|---|
| namerequired | string | Up to 200 characters. Example leads.csv. |
| content_type | string | Example text/csv. |
Responses
201The job — withupload_urlandheadersfor the signed flow. Body: FileCreated.400An Error body.401An Error body.413An Error body.501An Error body.
curl -X POST https://api.prooftell.com/v1/files \ -H "Authorization: Bearer pt_live_…" \ -H "Content-Type: application/json" \ -d '{ "name": "leads.csv", "content_type": "text/csv" }'
{
"file": {
"id": "string",
"name": "string",
"status": "uploading",
"stage": "string",
"format": {},
"columns": {
"phone": 0,
"email": 0,
"ip": 0,
"header_row": true
},
"header": [
"string"
],
"samples": [
[
"string"
]
],
"rows": 0,
"unique_rows": 0,
"signals": [
"string"
],
"hold": "string",
"charged": "string",
"counters": {},
"error": "string",
"created_at": "2026-10-01T09:00:00Z",
"expires_at": "2026-10-01T09:00:00Z"
},
"upload_url": "https://…",
"headers": {}
}/v1/files/{fileId}A file job and its progress
Parameters
| Field | Type | Description |
|---|---|---|
| fileIdrequired | string · path | Example fil_k2m9q7x3a8b4c5d6. |
Responses
curl https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6 \
-H "Authorization: Bearer pt_live_…"{
"id": "string",
"name": "string",
"status": "uploading",
"stage": "string",
"format": {},
"columns": {
"phone": 0,
"email": 0,
"ip": 0,
"header_row": true
},
"header": [
"string"
],
"samples": [
[
"string"
]
],
"rows": 0,
"unique_rows": 0,
"signals": [
"string"
],
"hold": "string",
"charged": "string",
"counters": {},
"error": "string",
"created_at": "2026-10-01T09:00:00Z",
"expires_at": "2026-10-01T09:00:00Z"
}/v1/files/{fileId}Delete a file job and its objects
/v1/files/{fileId}/finishThe signed upload landed — analyse it
Parameters
| Field | Type | Description |
|---|---|---|
| fileIdrequired | string · path | Example fil_k2m9q7x3a8b4c5d6. |
Responses
curl -X POST https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6/finish \
-H "Authorization: Bearer pt_live_…"{
"id": "string",
"name": "string",
"status": "uploading",
"stage": "string",
"format": {},
"columns": {
"phone": 0,
"email": 0,
"ip": 0,
"header_row": true
},
"header": [
"string"
],
"samples": [
[
"string"
]
],
"rows": 0,
"unique_rows": 0,
"signals": [
"string"
],
"hold": "string",
"charged": "string",
"counters": {},
"error": "string",
"created_at": "2026-10-01T09:00:00Z",
"expires_at": "2026-10-01T09:00:00Z"
}/v1/files/{fileId}/verifyRun the file
Holds unique rows × rate per selected signal on your balance (402 insufficient_balance
with the amounts when it cannot cover it), then runs. Rows are charged as they complete,
against the hold; duplicates run once and are flagged in pt_duplicate_of; rows a source
could not judge are free. columns overrides the detected mapping (0-based; -1 = none).
Parameters
| Field | Type | Description |
|---|---|---|
| fileIdrequired | string · path | Example fil_k2m9q7x3a8b4c5d6. |
Request body
| Field | Type | Description |
|---|---|---|
| signalsrequired | array of string | |
| columns | FileColumns |
Responses
curl -X POST https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6/verify \ -H "Authorization: Bearer pt_live_…" \ -H "Content-Type: application/json" \ -d '{ "signals": [ "phone_static" ] }'
{
"id": "string",
"name": "string",
"status": "uploading",
"stage": "string",
"format": {},
"columns": {
"phone": 0,
"email": 0,
"ip": 0,
"header_row": true
},
"header": [
"string"
],
"samples": [
[
"string"
]
],
"rows": 0,
"unique_rows": 0,
"signals": [
"string"
],
"hold": "string",
"charged": "string",
"counters": {},
"error": "string",
"created_at": "2026-10-01T09:00:00Z",
"expires_at": "2026-10-01T09:00:00Z"
}/v1/files/{fileId}/stopStop a running file
/v1/files/{fileId}/downloadA 15-minute link to the result
Parameters
| Field | Type | Description |
|---|---|---|
| fileIdrequired | string · path | Example fil_k2m9q7x3a8b4c5d6. |
| which | string · query | One of result, xlsx, source. Default result. |
Responses
200The link (resultis a CSV; while running, a partial result with unreached rowspending)401An Error body.404An Error body.409An Error body.501An Error body.
| Field | Type | Description |
|---|---|---|
| urlrequired | string (uri) |
curl https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6/download \
-H "Authorization: Bearer pt_live_…"{
"url": "https://…"
}Account
/v1/pricingPublicThe rate card
Public. One price list for everyone: the rate of every signal per run and per 1,000, country bands for banded signals, the top-up bonus ladder, the minimum top-up and the subscription amounts. Cached for five minutes. The website, the dashboard and the docs all read prices from here.
Responses
200The rate card. Body: Pricing.
curl https://api.prooftell.com/v1/pricing
{
"version": "string",
"currency": "USD",
"services": [
{
"service": "phone_static",
"label": "Phone verification",
"per_run": "0.0010",
"per_1000": "1.00",
"bands": {}
}
],
"bonus_ladder": [
{
"min": "100.00",
"bonus_percent": 5
}
],
"min_topup": "10.00",
"subscription_amounts": [
"string"
]
}/v1/balanceYour organization's balance
Balance to the cent — paid and promotional money, held amounts — and whether the daily quota still has room.
Responses
curl https://api.prooftell.com/v1/balance \
-H "Authorization: Bearer pt_live_…"{
"currency": "USD",
"balance": "124.00",
"paid": "string",
"promotional": "string",
"held": "string",
"quota": "available"
}Objects
PhoneRequest
| Field | Type | Description |
|---|---|---|
| phonerequired | string | E.164 preferred; national format needs country. Up to 32 characters. Example +447700900123. |
| country | string | ISO 3166-1 alpha-2 hint for national-format phones. Example GB. |
PhoneResult
Veriphone's /v3/verify static response, unchanged, plus the three ProofTell fields below.
| Field | Type | Description |
|---|---|---|
| statusrequired | string | success: the number was judged (valid or not) and the run counts; error: it could not be parsed, free. One of success, error. |
| phone_valid | boolean | |
| e164 | string | Example +447700900123. |
| country_code | string | Example GB. |
| carrier | string | |
| phone_type | string | |
| costrequired | string | What this call cost, in currency, four decimals. Example 0.0000. |
| currencyrequired | string | One of USD. |
| signalsrequired | object | |
| ↳ static | SignalLine |
EmailRequest
| Field | Type | Description |
|---|---|---|
| emailrequired | string | Up to 254 characters. Example jane@example.com. |
| timeout | integer | Verimail's budget for the SMTP dialogue, milliseconds. 1000 to 60000. |
EmailResult
Verimail's /v3/verify response, unchanged, plus the three ProofTell fields below.
| Field | Type | Description |
|---|---|---|
| statusrequired | string | |
| result | string | |
| verdict | string | |
| reason | string | |
| billedrequired | boolean | Whether this run counted, as Verimail decides it. |
| costrequired | string | Example 0.0000. |
| currencyrequired | string | One of USD. |
| signalsrequired | object | |
| SignalLine |
Pricing
| Field | Type | Description |
|---|---|---|
| versionrequired | string | |
| currencyrequired | string | One of USD. |
| servicesrequired | array of object | |
| bonus_ladderrequired | array of object | |
| min_topuprequired | string | Example 10.00. |
| subscription_amountsrequired | array of string |
Balance
| Field | Type | Description |
|---|---|---|
| currencyrequired | string | One of USD. |
| balancerequired | string | paid + promotional − held Example 124.00. |
| paidrequired | string | |
| promotionalrequired | string | |
| heldrequired | string | Reserved by running files. |
| quotarequired | string | The daily quota of static runs, unnumbered. One of available, exhausted. |
IpRequest
| Field | Type | Description |
|---|---|---|
| iprequired | string | IPv4 or IPv6. Example 203.0.113.7. |
IpResult
| Field | Type | Description |
|---|---|---|
| statusrequired | string | One of success. |
| iprequired | string | |
| versionrequired | integer | One of 4, 6. |
| routablerequired | boolean | False for private, loopback, link-local and multicast addresses. |
| country_code | string | null | Example GB. |
| country | string | null | |
| region | string | null | |
| city | string | null | |
| latitude | number | |
| longitude | number | |
| accuracy_km | integer | |
| timezone | string | |
| asn | integer | null | Example 15169. |
| as_org | string | null | Example GOOGLE. |
| is_torrequired | boolean | |
| is_vpnrequired | boolean | |
| is_datacenterrequired | boolean | |
| is_anonymousrequired | boolean | Tor or VPN. |
| anycast | boolean | |
| data_version | string | Identifies the data set the answer came from. |
| costrequired | string | |
| currencyrequired | string | One of USD. |
| signalsrequired | object | |
| ↳ ip | SignalLine |
CreateFile
| Field | Type | Description |
|---|---|---|
| namerequired | string | Up to 200 characters. Example leads.csv. |
| content_type | string | Example text/csv. |
FileCreated
| Field | Type | Description |
|---|---|---|
| filerequired | FileJob | |
| upload_url | string (uri) | Signed flow only. |
| headers | object of string |
FileColumns
| Field | Type | Description |
|---|---|---|
| phonerequired | integer | 0-based column; -1 = none |
| emailrequired | integer | |
| iprequired | integer | |
| header_rowrequired | boolean |
VerifyFileRequest
| Field | Type | Description |
|---|---|---|
| signalsrequired | array of string | |
| columns | FileColumns |
FileJob
| Field | Type | Description |
|---|---|---|
| idrequired | string | |
| namerequired | string | |
| statusrequired | string | One of uploading, ready, verifying, complete, failed, stopped, expired. |
| stage | string | |
| format | object | null | kind csv|tsv|txt|xlsx, delimiter, encoding, sheet |
| columns | FileColumns | null | |
| header | array of string | |
| samples | array of array of string | |
| rowsrequired | integer | |
| unique_rowsrequired | integer | |
| signalsrequired | array of string | |
| holdrequired | string | Dollars held while running. |
| chargedrequired | string | |
| countersrequired | object of integer | done, unique, duplicates, unavailable, <signal>_runs. |
| error | string | null | |
| created_atrequired | string (date-time) | |
| expires_atrequired | string (date-time) | Files and results are deleted 30 days after upload. |
SignalLine
| Field | Type | Description |
|---|---|---|
| statusrequired | string | One of ok, unavailable. |
| costrequired | string | Example 0.0000. |
AssessRequest
| Field | Type | Description |
|---|---|---|
| phone | string | E.164 preferred; national format needs country. Example +447700900123. |
| string (email) | Example jane@example.com. | |
| ip | string | IPv4 or IPv6 of the end user. Example 203.0.113.7. |
| country | string | ISO 3166-1 alpha-2 hint for national-format phones. Example GB. |
| reference | string | Your own id for this check, echoed back. Up to 128 characters. |
Assessment
| Field | Type | Description |
|---|---|---|
| idrequired | string | Example asm_8f3k2m9q. |
| moderequired | string | test for sandbox calls made with a pt_test_ key. One of live, test. |
| reference | string | |
| scorerequired | integer | 0 = no risk found, 100 = highest risk. 0 to 100. |
| verdictrequired | string | Score compared with your organization's thresholds; at least review when a supplied signal was unavailable (unless the organization turned that off). One of allow, review, deny. |
| reasonsrequired | array of Reason | |
| signalsrequired | object | |
| ↳ phone | Signal | |
| Signal | ||
| ↳ ip | Signal | |
| rulesetrequired | string | Version of the scoring rules that produced this result. Example 2026-10-01. |
| costrequired | string | The sum of the signal lines, four decimals. Example 0.0045. |
| currencyrequired | string | One of USD. |
| createdAtrequired | string (date-time) |
Reason
| Field | Type | Description |
|---|---|---|
| coderequired | string | Example email_disposable. |
| pointsrequired | integer | Example 40. |
| messagerequired | string | Example The email address uses a disposable provider.. |
Signal
| Field | Type | Description |
|---|---|---|
| statusrequired | string | One of ok, unavailable. |
| costrequired | string | Example 0.0010. |
| result | object | The signal's own fields: /v1/phone, /v1/email and /v1/ip document them. |
Error
| Field | Type | Description |
|---|---|---|
| errorrequired | object | |
| ↳ coderequired | string | Stable: invalid_api_key, bad_request, invalid_phone, invalid_country, invalid_email, invalid_timeout, invalid_ip, daily_quota_reached, insufficient_balance, rate_limited, signals_unavailable, sandbox_not_available, not_implemented. |
| ↳ messagerequired | string | |
| ↳ retry_after | integer | Seconds until the daily quota resets (on daily_quota_reached; also the Retry-After header). |
| ↳ cost_required | string | On insufficient_balance: what the run costs. |
| ↳ cost_available | string | On insufficient_balance: your balance. |
| ↳ cost_missing | string | |
| ↳ topup_url | string (uri) | Where to top up. |