One score. Every reason.
One call takes a phone number, an email address and an IP address, in any combination, and answers with a score from 0 to 100, a verdict and the reason for every point. $4.50 per 1,000 full checks.
No model, no black box: the same signal results always give the same score.
- The IP address is a Tor exit node.+40
- The email address uses a disposable provider.+35
- The number is a VoIP line, easy to obtain anonymously.+20
Three steps, none of them hidden.
- 1
The signals run in parallel
Phone, email and IP are checked at the same time, within a time budget. A source that does not answer in time is reported as unavailable instead of holding up the call.
- 2
Rules turn facts into points
Each rule that matches adds a fixed number of points and one sentence saying why. The score is the sum, capped at 100.
- 3
Your thresholds give the verdict
A score under your review threshold is an allow; at or above your deny threshold, a deny; in between, a review. The defaults are 30 and 70, and both are yours to change.
Every tell the score can find.
Each reason in a response carries a stable code, the points it added and a sentence a person can read. The codes are yours to branch on; the sentences are yours to show an analyst.
| Phone | What the response says |
|---|---|
| phone_invalid | The phone number is not a valid number. |
| phone_voip | The number is a VoIP line, easy to obtain anonymously. |
| phone_non_personal | The number is a toll-free, premium-rate, shared-cost or pager line, not a personal one. |
| phone_type_unknown | The line type could not be determined. |
| What the response says | |
|---|---|
| email_invalid | The email address is not a valid address. |
| email_undeliverable | The mailbox does not exist or rejects mail. |
| email_disposable | The email address uses a disposable provider. |
| email_catch_all | The domain accepts any address, so the mailbox could not be confirmed. |
| email_risky | The mailbox is risky, for instance over quota. |
| email_unverifiable | The mail server would not say whether the mailbox exists. |
| IP | What the response says |
|---|---|
| ip_tor | The IP address is a Tor exit node. |
| ip_vpn | The IP address belongs to a VPN provider. |
| ip_datacenter | The IP address belongs to a datacenter, not a residential or mobile network. |
| ip_not_routable | The IP address is private or reserved, not an internet address. |
| Across signals | What the response says |
|---|---|
| geo_mismatch | The phone number’s country differs from the IP address’s. |
| signal_unavailable_<signal> | A signal you supplied could not run. Zero points; the verdict is raised to at least review. |
The rules are ours. The decision is yours.
Set per organization, by an owner or an admin, in the dashboard.
Thresholds
Where review starts and where deny starts, for your organization: 30 and 70 until you change them in the dashboard.
The outage rule
A supplied signal that cannot run raises the verdict to at least review. You can switch the raise off if your own fallback covers it.
Retention
Assessments are kept 90 days by default, for review and audit. Choose another period up to 365 days, or zero, and no assessment is stored.
History
Your most recent assessments are in the dashboard, each with its inputs, score, reasons and every signal’s raw result. Live and sandbox are kept apart.
Run it now, with a sandbox key.
This request uses the sandbox’s documented inputs: a VoIP number, a disposable address and a Tor exit node. With apt_test_ key it returns exactly the response shown, free.
With a live key, each signal that returns a result is charged at its own rate, and the response carries the cost of the call and of each signal.
curl https://api.prooftell.com/v1/assess \ -H "Authorization: Bearer $PROOFTELL_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone": "+15005550002", "email": "disposable@sandbox.prooftell.com", "ip": "203.0.113.66", "reference": "signup-48213" }'
const res = await fetch("https://api.prooftell.com/v1/assess", { method: "POST", headers: { "Authorization": `Bearer ${process.env.PROOFTELL_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ phone: "+15005550002", email: "disposable@sandbox.prooftell.com", ip: "203.0.113.66", reference: "signup-48213", }), }) const { score, verdict, reasons } = await res.json()
import os, requests
res = requests.post(
"https://api.prooftell.com/v1/assess",
headers={"Authorization": f"Bearer {os.environ['PROOFTELL_KEY']}"},
json={
"phone": "+15005550002",
"email": "disposable@sandbox.prooftell.com",
"ip": "203.0.113.66",
"reference": "signup-48213",
},
)
result = res.json()
print(result["verdict"], result["score"]){
"id": "asm_oxyq6rmz74bzsrxm",
"mode": "test",
"reference": "signup-48213",
"score": 95,
"verdict": "deny",
"reasons": [
{ "code": "phone_voip", "points": 20,
"message": "The number is a VoIP line, easy to obtain anonymously." },
{ "code": "email_disposable", "points": 35,
"message": "The email address uses a disposable provider." },
{ "code": "ip_tor", "points": 40,
"message": "The IP address is a Tor exit node." }
],
"signals": {
"phone": { "status": "ok", "cost": "0.0000", "result": { … } },
"email": { "status": "ok", "cost": "0.0000", "result": { … } },
"ip": { "status": "ok", "cost": "0.0000", "result": { … } }
},
"ruleset": "2026-10-01",
"cost": "0.0000",
"currency": "USD",
"createdAt": "2026-10-01T07:55:41Z"
}The sum of the signals that answered.
There is no price for the score itself. A call costs the signals that returned a result, each at its own rate. A signal that was unavailable costs nothing.
See the rate card| You send | Per check | Per 1,000 |
|---|---|---|
| Phone intelligence | $0.0010 | $1.00 |
| Email verification | $0.0030 | $3.00 |
| IP intelligence | $0.0005 | $0.50 |
| Phone, email and IP together | $0.0045 | $4.50 |
About the score.
Do I have to send all three inputs?
Is the score machine learning?
Can I change the weights?
What happens when a signal is unavailable?
unavailable, costs nothing, and adds a zero-point reason such as signal_unavailable_email. The verdict is raised to at least review, so an outage can never turn a deny into an allow. If none of the supplied signals ran, the call answers 503 signals_unavailable and nothing is charged.Should I block automatically on “deny”?
How do I tie an assessment to my own records?
reference, your own identifier for the check (an order number, a signup id). It is echoed in the response and stored with the assessment. Every assessment also has its own id, such as asm_8f3k2m9q.Can I test without paying?
pt_test_ key makes every call a sandbox call: simulated, deterministic and free. Documented inputs produce the main outcomes, an unavailable signal among them, so you can build and test your integration before a real key is involved.Find the tell before it costs you.
Create an account, take a sandbox key, make your first call in minutes.