Limited access. Spam reputation is available to selected customers on request. Mention it in your access request.
Limited access: this service is available to internal customers only for now.
The spam reputation check tells you whether a phone number appears in spam and nuisance-call reports, and why. Every answer includes a level (high, medium, low or no_reports), a 0–100 score and the classes of signal behind it. It covers numbers from the United States, Canada and Germany, in real time or in bulk.
What does the spam reputation check tell you?
It tells you whether a number has a record of complaints, and how strong that record is. It is a reputation signal, not a verdict. It summarizes what others reported about a number. It doesn't prove who is calling.
The answer is built from these classes of signal, described here by type only:
- Regulator actions: a telecom regulator took action against the number.
- Government complaint data: the number appears in government nuisance-call complaint data.
- Community reports: people reported the number on public spam-report sites.
- Unassigned-number signal: the number was recently offered for sale as unassigned. This is a sign of a spoofed caller ID or a made-up lead.
- VoIP-range hint: the number range belongs to a VoIP carrier. This is shown for context only and adds no points.
Reports are refreshed daily. Report texts, reporter details and names are never returned.
How are levels and scores decided?
The risk_score runs from 0 to 100. More reports and stronger reports raise it. Reports last seen more than 12 months ago count half, so a number that was abused years ago and has been quiet since slowly drifts down.
| Level | Rule |
|---|---|
high | Score 80 or more and either a regulator action or at least two independent signal classes |
medium | Score 50–79 |
low | Score 20–49 |
no_reports | We hold no reports for the number |
sources counts the independent signal classes behind the answer. top_category names the most frequent kind of report: debt_relief, impersonation, robocall, medical, home_services, warranty, sms_spam, dialer, fraud_hacking or other. first_seen and last_seen give the months (YYYY-MM) when the number first and last appeared in our data.
no_reports is not a guarantee a number is safe. It means we have no negative signals, nothing more.
Who uses it, and why?
- Call centers and inbound teams use it to decide whether to answer, flag or route an incoming call, and to check that their own outbound numbers haven't collected complaints. See call-center screening.
- Lead verification. A web lead whose phone number was recently offered as unassigned (
reason_unassigned) is likely fake. That is worth knowing before a sales team spends time on it. See lead verification. - Fraud teams add the level to sign-up and payment risk scoring. A
highnumber with animpersonationorfraud_hackingcategory deserves a manual review.
Coverage is limited to the US, Canada and Germany because report data is dense enough there to give meaningful answers. We don't offer the check in countries where a thin data set would produce misleading no_reports answers.
What do you get back?
| Field | Type | Meaning |
|---|---|---|
attributes.risk_level | enum | high, medium, low or no_reports |
attributes.risk_score | integer 0–100 | Higher means more and stronger reports |
attributes.reason_regulator | boolean | A telecom regulator took action against the number |
attributes.reason_government | boolean | Listed in government nuisance-call complaint data |
attributes.reason_community | boolean | Reported on community spam-report sites |
attributes.reason_unassigned | boolean | Recently offered for sale as an unassigned number |
attributes.voip_range | boolean | Hint only: the range belongs to a VoIP carrier (no points) |
attributes.top_category | enum | Most frequent report category (absent when none) |
attributes.first_seen / last_seen | YYYY-MM | First and last month the number appeared in our data |
attributes.sources | integer 0–10 | Number of independent signal classes |
registered | boolean or null | true for every conclusive answer ("data found"); null otherwise |
In summaries, high, medium and low count as "registered" (reports found) and no_reports counts as "not registered".
How is it billed?
Every conclusive answer is billed, including no_reports. The check was carried out and answered. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). An unknown answer, for example while reference data is temporarily unavailable, is free, and so is any number outside the US, Canada and Germany. See pricing.
Answers come from our own daily-refreshed reference data rather than a live call to another service, so real-time answers are fast. Repeat checks of the same number within 24 hours are served from your account's cache and are free.
What are the limits?
- Countries: US, CA and DE. Other numbers return
unsupported_country, which is free. - Modes: real time (
POST /v1/lookup, up to 100 numbers) and bulk jobs (POST /v1/jobs, up to 50,000). The MCP toolcheck_spam_reputationtakes up to 100 numbers per call. - Up to 20 checks per request. Numbers × checks is capped at 2,000 per lookup and 100,000 per job.
- Requests that look like sequential number ranges or generated e-mail lists are rejected (20 or more consecutive numbers →
suspected_enumeration). - Scores change over time. Store
checked_atwith any decision you make.
How do I use it responsibly?
Treat the level as one signal among several. Don't treat it as proof that a person is a spammer. A number can be spoofed by someone else, and a number reported years ago can have a new owner. Don't use the result to deny someone credit, a job, housing or insurance. The acceptable use policy forbids that.
We process report data about numbers as a controller. If your number appears in our data and you think it shouldn't, or you want it removed, use the opt-out form. The data-subject notice explains your rights.
Example request
With a test key, the whole test range +44 7700 9xxxxx works for this service, although live checks cover only US, CA and DE. +447700900001 returns high, …002 returns no_reports, …003 returns unknown, …004 is pending and then medium, and …005 returns unsupported_country. See test mode.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["spam"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.lookup({
numbers: ["+447700900001"],
checks: ["spam"],
});
if (error) console.error(error.code, error.message);
else console.dir(data.results, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({ numbers: ["+447700900001"], checks: ["spam"] }),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.lookup(numbers=["+447700900001"], checks=["spam"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/lookup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['spam'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["spam"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/lookup", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/lookup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["spam"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.bodyRuns as pasted with the public sandbox key, which answers the test values only. In the SDKs, omit the key to use MOBILEVALIDATE_API_KEY.
Response (excerpt, test mode: the first item of results):
{
"kind": "phone",
"input": "+447700900001",
"e164": "+447700900001",
"country": "GB",
"number_status": "valid",
"checks": {
"number.spam": {
"service": "number.spam",
"status": "completed",
"registered": true,
"attributes": {
"risk_level": "high",
"risk_score": 95,
"reason_regulator": true,
"reason_government": false,
"reason_community": true,
"reason_unassigned": false,
"voip_range": false,
"top_category": "robocall",
"first_seen": "2025-11",
"last_seen": "2026-08",
"sources": 2
},
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:31.435Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}
},
"test": true
}Try it now
Realtime lookup via POST /v1/lookup. The example uses a test value; this check needs an account with access to it.
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" \
-H "Content-Type: application/json" \
-d '{"numbers":["+447700900001"],"checks":["number.spam"]}'// npm install mobilevalidate · Node.js 20+ · save as check.mjs and run: node check.mjs
import { MobileValidate } from "mobilevalidate";
// Omit apiKey to read MOBILEVALIDATE_API_KEY from the environment.
const mv = new MobileValidate({ apiKey: "mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym" });
const { data, error } = await mv.lookup({
numbers: ["+447700900001"],
checks: ["number.spam"],
});
if (error) console.error(error.code, error.message);
else console.dir(data.results, { depth: null });// Node.js 18+, Deno, Bun or the browser console (ES module: save as .mjs or use "type": "module").
const res = await fetch("https://api.mobilevalidate.com/v1/lookup", {
method: "POST",
headers: {
Authorization: "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym",
"Content-Type": "application/json",
},
body: JSON.stringify({
numbers: ["+447700900001"],
checks: ["number.spam"],
}),
});
console.log(res.status, res.headers.get("x-request-id"));
console.dir(await res.json(), { depth: null });# pip install mobilevalidate-sdk
from mobilevalidate import MobileValidate
# Omit api_key to read MOBILEVALIDATE_API_KEY from the environment.
mv = MobileValidate(api_key="mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
result = mv.lookup(numbers=["+447700900001"], checks=["number.spam"])
print(result)<?php
$ch = curl_init('https://api.mobilevalidate.com/v1/lookup');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numbers' => ['+447700900001'],
'checks' => ['number.spam'],
]),
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL, $response, PHP_EOL;package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"numbers":["+447700900001"],"checks":["number.spam"]}`)
req, err := http.NewRequest("POST", "https://api.mobilevalidate.com/v1/lookup", body)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require "net/http"
require "json"
uri = URI("https://api.mobilevalidate.com/v1/lookup")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer mv_test_publicSandboxn9ZgneuhR1B9CRfKG3fulym"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
"numbers" => ["+447700900001"],
"checks" => ["number.spam"]
})
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, res.body// .mcp.json (Claude Code), ~/.cursor/mcp.json (Cursor) or .vscode/mcp.json ("servers" instead of "mcpServers")
{
"mcpServers": {
"mobilevalidate": {
"type": "http",
"url": "https://mcp.mobilevalidate.com/mcp",
"headers": { "Authorization": "Bearer ${MOBILEVALIDATE_API_KEY}" }
}
}
}
// Then ask your agent, e.g.:
// "Check +447700900001 with number.spam" → tool: check_spam_reputationThe hosted MCP server is live for customers with an agent key (mv_agent_…) or a test key. Or run it locally with npx -y @mobilevalidate/mcp.
Real test-mode result for this check
{
"service": "number.spam",
"status": "completed",
"registered": true,
"attributes": {
"risk_level": "high",
"risk_score": 95,
"reason_regulator": true,
"reason_government": false,
"reason_community": true,
"reason_unassigned": false,
"voip_range": false,
"top_category": "robocall",
"first_seen": "2025-11",
"last_seen": "2026-08",
"sources": 2
},
"confidence": "high",
"confidence_score": 0.99,
"checked_at": "2026-09-25T14:25:29.004Z",
"cached": false,
"age_seconds": 0,
"billed": false,
"reason": null,
"poll_after_ms": null
}What you get
Fields of checks["number.spam"], generated from the public service catalog.
| Field | Type | Meaning |
|---|---|---|
| registered | boolean | null | true when data was found; null when unknown (not charged). |
| status | enum | completed, pending, unknown, unsupported_country or failed. |
| checked_at | timestamp | When the answer was obtained. |
| attributes.risk_level | enum | Overall reputation level.high · medium · low · no_reports |
| attributes.risk_score | integer (0–100) | Score 0–100 (higher = more reports). |
| attributes.reason_regulator | boolean | A telecom regulator took action against the number. |
| attributes.reason_government | boolean | Listed in government nuisance-call complaint data. |
| attributes.reason_community | boolean | Reported on community spam-report sites. |
| attributes.reason_unassigned | boolean | Recently offered for sale as an unassigned number (possible spoofed caller ID or fake lead). |
| attributes.voip_range | boolean | Hint only: the number range belongs to a VoIP carrier (not a risk by itself). |
| attributes.top_category | enum | Most frequent report category.debt_relief · impersonation · robocall · medical · home_services · warranty · sms_spam · dialer · fraud_hacking · other |
| attributes.first_seen | string | Month the number first appeared in our data (YYYY-MM). |
| attributes.last_seen | string | Month the number last appeared in our data (YYYY-MM). |
| attributes.sources | integer (0–10) | Number of independent signal classes. |
Frequently asked questions
Who can use the spam reputation check today?
It is in limited access: available to internal customers only for now. If you need it, say so in your access request and we will let you know when it opens up.
Does no_reports mean the number is safe?
No. no_reports means we hold no reports for the number. A new number, a rarely used number or a spoofed caller ID can still be abusive. Treat it as the absence of negative signals and combine it with other checks.
Why is a no_reports answer charged?
Because the check was carried out and answered conclusively. Every risk level, including no_reports, is billed. Unknown answers and numbers outside the US, Canada and Germany are free.
Can I see the report texts or who reported a number?
No. The check returns levels, reasons and categories only. Report texts, reporter details and names are never returned.
Can a number's score change?
Yes. Scores are recomputed as new reports arrive and old ones age. Reports last seen more than 12 months ago count half. checked_at shows when an answer was computed.

