Skip to content
ProofTell
Sign inSign up
Contents
ProofTell API · 0.1.0-draft

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.dev/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

POST/v1/assess

Assess 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

FieldTypeDescription
phonestringE.164 preferred; national format needs country. Example +447700900123.
emailstring (email)Example jane@example.com.
ipstringIPv4 or IPv6 of the end user. Example 203.0.113.7.
countrystringISO 3166-1 alpha-2 hint for national-format phones. Example GB.
referencestringYour 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

POST/v1/phone

Verify 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

FieldTypeDescription
phonerequiredstringE.164 preferred; national format needs country. Up to 32 characters. Example +447700900123.
countrystringISO 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"
    }
  }
}
POST/v1/email

Verify 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

FieldTypeDescription
emailrequiredstringUp to 254 characters. Example jane@example.com.
timeoutintegerVerimail'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"
    }
  }
}
POST/v1/ip

Look 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

FieldTypeDescription
iprequiredstringIPv4 or IPv6. Example 203.0.113.7.

Responses

  • 200The address, its geography, its network and the list flags, plus the billing fields. Body: IpResult.
  • 400An Error body.
  • 401An Error body.
  • 402An Error body.
  • 429An Error body.
  • 503An Error body.
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

GET/v1/files

Your organization's files

Responses

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"
  }
]
POST/v1/files

Start 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

FieldTypeDescription
namerequiredstringUp to 200 characters. Example leads.csv.
content_typestringExample text/csv.

Responses

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": {}
}
GET/v1/files/{fileId}

A file job and its progress

Parameters

FieldTypeDescription
fileIdrequiredstring · pathExample 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"
}
DELETE/v1/files/{fileId}

Delete a file job and its objects

Parameters

FieldTypeDescription
fileIdrequiredstring · pathExample fil_k2m9q7x3a8b4c5d6.

Responses

curl -X DELETE https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6 \
  -H "Authorization: Bearer pt_live_…"
POST/v1/files/{fileId}/finish

The signed upload landed — analyse it

Parameters

FieldTypeDescription
fileIdrequiredstring · pathExample fil_k2m9q7x3a8b4c5d6.

Responses

  • 200The analysed job (format, columns, rows, header, samples). Body: FileJob.
  • 400An Error body.
  • 401An Error body.
  • 404An Error body.
  • 409An Error body.
  • 501An Error body.
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"
}
POST/v1/files/{fileId}/verify

Run 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

FieldTypeDescription
fileIdrequiredstring · pathExample fil_k2m9q7x3a8b4c5d6.

Request body

FieldTypeDescription
signalsrequiredarray of string
columnsFileColumns

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"
}
POST/v1/files/{fileId}/stop

Stop a running file

Parameters

FieldTypeDescription
fileIdrequiredstring · pathExample fil_k2m9q7x3a8b4c5d6.

Responses

curl -X POST https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6/stop \
  -H "Authorization: Bearer pt_live_…"
GET/v1/files/{fileId}/download

A 15-minute link to the result

Parameters

FieldTypeDescription
fileIdrequiredstring · pathExample fil_k2m9q7x3a8b4c5d6.
whichstring · queryOne of result, xlsx, source. Default result.

Responses

  • 200The link (result is a CSV; while running, a partial result with unreached rows pending)
  • 401An Error body.
  • 404An Error body.
  • 409An Error body.
  • 501An Error body.
FieldTypeDescription
urlrequiredstring (uri)
curl https://api.prooftell.com/v1/files/fil_k2m9q7x3a8b4c5d6/download \
  -H "Authorization: Bearer pt_live_…"
{
  "url": "https://…"
}

Account

GET/v1/pricingPublic

The 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

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"
  ]
}
GET/v1/balance

Your 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

FieldTypeDescription
phonerequiredstringE.164 preferred; national format needs country. Up to 32 characters. Example +447700900123.
countrystringISO 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.

FieldTypeDescription
statusrequiredstringsuccess: the number was judged (valid or not) and the run counts; error: it could not be parsed, free. One of success, error.
phone_validboolean
e164stringExample +447700900123.
country_codestringExample GB.
carrierstring
phone_typestring
costrequiredstringWhat this call cost, in currency, four decimals. Example 0.0000.
currencyrequiredstringOne of USD.
signalsrequiredobject
↳ staticSignalLine

EmailRequest

FieldTypeDescription
emailrequiredstringUp to 254 characters. Example jane@example.com.
timeoutintegerVerimail's budget for the SMTP dialogue, milliseconds. 1000 to 60000.

EmailResult

Verimail's /v3/verify response, unchanged, plus the three ProofTell fields below.

FieldTypeDescription
statusrequiredstring
resultstring
verdictstring
reasonstring
billedrequiredbooleanWhether this run counted, as Verimail decides it.
costrequiredstringExample 0.0000.
currencyrequiredstringOne of USD.
signalsrequiredobject
↳ emailSignalLine

Pricing

FieldTypeDescription
versionrequiredstring
currencyrequiredstringOne of USD.
servicesrequiredarray of object
bonus_ladderrequiredarray of object
min_topuprequiredstringExample 10.00.
subscription_amountsrequiredarray of string

Balance

FieldTypeDescription
currencyrequiredstringOne of USD.
balancerequiredstringpaid + promotional − held Example 124.00.
paidrequiredstring
promotionalrequiredstring
heldrequiredstringReserved by running files.
quotarequiredstringThe daily quota of static runs, unnumbered. One of available, exhausted.

IpRequest

FieldTypeDescription
iprequiredstringIPv4 or IPv6. Example 203.0.113.7.

IpResult

FieldTypeDescription
statusrequiredstringOne of success.
iprequiredstring
versionrequiredintegerOne of 4, 6.
routablerequiredbooleanFalse for private, loopback, link-local and multicast addresses.
country_codestring | nullExample GB.
countrystring | null
regionstring | null
citystring | null
latitudenumber
longitudenumber
accuracy_kminteger
timezonestring
asninteger | nullExample 15169.
as_orgstring | nullExample GOOGLE.
is_torrequiredboolean
is_vpnrequiredboolean
is_datacenterrequiredboolean
is_anonymousrequiredbooleanTor or VPN.
anycastboolean
data_versionstringIdentifies the data set the answer came from.
costrequiredstring
currencyrequiredstringOne of USD.
signalsrequiredobject
↳ ipSignalLine

CreateFile

FieldTypeDescription
namerequiredstringUp to 200 characters. Example leads.csv.
content_typestringExample text/csv.

FileCreated

FieldTypeDescription
filerequiredFileJob
upload_urlstring (uri)Signed flow only.
headersobject of string

FileColumns

FieldTypeDescription
phonerequiredinteger0-based column; -1 = none
emailrequiredinteger
iprequiredinteger
header_rowrequiredboolean

VerifyFileRequest

FieldTypeDescription
signalsrequiredarray of string
columnsFileColumns

FileJob

FieldTypeDescription
idrequiredstring
namerequiredstring
statusrequiredstringOne of uploading, ready, verifying, complete, failed, stopped, expired.
stagestring
formatobject | nullkind csv|tsv|txt|xlsx, delimiter, encoding, sheet
columnsFileColumns | null
headerarray of string
samplesarray of array of string
rowsrequiredinteger
unique_rowsrequiredinteger
signalsrequiredarray of string
holdrequiredstringDollars held while running.
chargedrequiredstring
countersrequiredobject of integerdone, unique, duplicates, unavailable, <signal>_runs.
errorstring | null
created_atrequiredstring (date-time)
expires_atrequiredstring (date-time)Files and results are deleted 30 days after upload.

SignalLine

FieldTypeDescription
statusrequiredstringOne of ok, unavailable.
costrequiredstringExample 0.0000.

AssessRequest

FieldTypeDescription
phonestringE.164 preferred; national format needs country. Example +447700900123.
emailstring (email)Example jane@example.com.
ipstringIPv4 or IPv6 of the end user. Example 203.0.113.7.
countrystringISO 3166-1 alpha-2 hint for national-format phones. Example GB.
referencestringYour own id for this check, echoed back. Up to 128 characters.

Assessment

FieldTypeDescription
idrequiredstringExample asm_8f3k2m9q.
moderequiredstringtest for sandbox calls made with a pt_test_ key. One of live, test.
referencestring
scorerequiredinteger0 = no risk found, 100 = highest risk. 0 to 100.
verdictrequiredstringScore 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.
reasonsrequiredarray of Reason
signalsrequiredobject
↳ phoneSignal
↳ emailSignal
↳ ipSignal
rulesetrequiredstringVersion of the scoring rules that produced this result. Example 2026-10-01.
costrequiredstringThe sum of the signal lines, four decimals. Example 0.0045.
currencyrequiredstringOne of USD.
createdAtrequiredstring (date-time)

Reason

FieldTypeDescription
coderequiredstringExample email_disposable.
pointsrequiredintegerExample 40.
messagerequiredstringExample The email address uses a disposable provider..

Signal

FieldTypeDescription
statusrequiredstringOne of ok, unavailable.
costrequiredstringExample 0.0010.
resultobjectThe signal's own fields: /v1/phone, /v1/email and /v1/ip document them.

Error

FieldTypeDescription
errorrequiredobject
↳ coderequiredstringStable: 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.
↳ messagerequiredstring
↳ retry_afterintegerSeconds until the daily quota resets (on daily_quota_reached; also the Retry-After header).
↳ cost_requiredstringOn insufficient_balance: what the run costs.
↳ cost_availablestringOn insufficient_balance: your balance.
↳ cost_missingstring
↳ topup_urlstring (uri)Where to top up.
staging