FraudNova API
Real-time fraud scoring for IP addresses, email addresses, and phone numbers over a simple REST API. Create an API key from your dashboard, then authenticate with a Bearer token.
Authentication
Send your API key as a Bearer token on every request:
Authorization: Bearer fn_your_api_key_here
PHP SDK
The official PHP SDK
(fraudnova/fraudnova-php, PHP 8.2+)
wraps every endpoint with typed results and exceptions. It uses any PSR-18 HTTP client; Guzzle is detected automatically.
composer require fraudnova/fraudnova-php guzzlehttp/guzzle
$client = new FraudNova\Client('fn_your_api_key_here');
$result = $client->ip('8.8.8.8');
if ($result->flagged()) {
// review or block; $result->flagReasons() lists why
}
echo $result->fraudScore;
email(), phone(), batchIp(), batchEmail(), batchPhone() and balance() cover the other endpoints;
each code sample below has a "PHP SDK" tab. In Laravel the package registers itself: put FRAUDNOVA_API_KEY in .env and call the FraudNova facade
(FraudNova::ip($request->ip())). The README covers options, retries and testing.
Check an IP
GET /api/v1/check — 1 credit per call. Rate limit: 600 requests/minute per key. Up to 50 checks per account may run at the same time (higher per-account limits are available on request); every 429 response carries a Retry-After header with the number of seconds to wait before retrying.
Query parameters:
ip(required) — the IPv4/IPv6 address to score.strictness(optional, 0–3, default 2) — higher = stricter.mobile(optional, true/false) — treat as a mobile network.user_agent(optional) — the visitor's user-agent; unlocks device, browser & OS detection.user_language(optional) — the visitor's Accept-Language; improves scoring accuracy.
curl -H "Authorization: Bearer fn_..." \
"https://fraudnova.com/api/v1/check?ip=8.8.8.8&strictness=2"
import requests
r = requests.get(
"https://fraudnova.com/api/v1/check",
headers={"Authorization": "Bearer fn_..."},
params={"ip": "8.8.8.8", "strictness": 2},
)
print(r.json()["data"]["fraud_score"])
const res = await fetch(
"https://fraudnova.com/api/v1/check?ip=8.8.8.8&strictness=2",
{ headers: { Authorization: "Bearer fn_..." } }
);
const body = await res.json();
console.log(body.data.fraud_score);
$client = new GuzzleHttp\Client();
$res = $client->get("https://fraudnova.com/api/v1/check", [
"headers" => ["Authorization" => "Bearer fn_..."],
"query" => ["ip" => "8.8.8.8", "strictness" => 2],
]);
$body = json_decode((string) $res->getBody(), true);
echo $body["data"]["fraud_score"];
$client = new FraudNova\Client("fn_...");
$result = $client->ip("8.8.8.8", new FraudNova\IpOptions(strictness: 2));
echo $result->fraudScore;
Success response:
{
"success": true,
"data": {
"fraud_score": 20,
"proxy": false,
"vpn": false,
"tor": false,
"active_vpn": false,
"active_tor": false,
"recent_abuse": false,
"bot_status": false,
"country_code": "US",
"region": "California",
"city": "Mountain View",
"ISP": "Google LLC",
"organization": "AS15169 Google LLC",
"ASN": 15169,
"operating_system": "macOS",
"browser": "Chrome",
"device_brand": "Apple",
"device_model": "Mac"
},
"credits_remaining": 4
}
Device, browser & OS fields populate when you pass user_agent.
Errors use { "success": false, "message": "..." } with status 401 (bad token), 402 (insufficient credits), 422 (invalid input), 429 (rate limited), or 503 (upstream unavailable).
Check an email
GET /api/v1/check/email — 1 credit per call. Rate limit: 600 requests/minute per key. Up to 50 checks per account may run at the same time (higher per-account limits are available on request); every 429 response carries a Retry-After header with the number of seconds to wait before retrying.
Query parameters:
email(required) — the email address to score.
curl -H "Authorization: Bearer fn_..." \
"https://fraudnova.com/api/v1/check/email?email=user@example.com"
import requests
r = requests.get(
"https://fraudnova.com/api/v1/check/email",
headers={"Authorization": "Bearer fn_..."},
params={"email": "user@example.com"},
)
print(r.json()["data"]["fraud_score"])
const res = await fetch(
"https://fraudnova.com/api/v1/check/email?email=user@example.com",
{ headers: { Authorization: "Bearer fn_..." } }
);
const body = await res.json();
console.log(body.data.fraud_score);
$client = new GuzzleHttp\Client();
$res = $client->get("https://fraudnova.com/api/v1/check/email", [
"headers" => ["Authorization" => "Bearer fn_..."],
"query" => ["email" => "user@example.com"],
]);
$body = json_decode((string) $res->getBody(), true);
echo $body["data"]["fraud_score"];
$client = new FraudNova\Client("fn_...");
$result = $client->email("user@example.com");
echo $result->fraudScore;
Success response:
{
"success": true,
"data": {
"valid": true,
"fraud_score": 15,
"disposable": false,
"deliverability": "high",
"smtp_score": 4,
"overall_score": 4,
"catch_all": false,
"generic": false,
"common": true,
"dns_valid": true,
"honeypot": false,
"frequent_complainer": false,
"suspect": false,
"recent_abuse": false,
"leaked": false,
"spam_trap_score": "low",
"risky_tld": false,
"spf_record": true,
"dmarc_record": true,
"first_seen": { "human": "7 years ago", "timestamp": 1554940800, "iso": "2019-04-11T00:00:00+00:00" },
"domain_age": { "human": "9 years ago", "timestamp": 1491264000, "iso": "2017-04-04T00:00:00+00:00" },
"result_type": "email",
"sanitized_email": "user@example.com"
},
"credits_remaining": 4
}
Errors use { "success": false, "message": "..." } with status 401 (bad token), 402 (insufficient credits), 422 (invalid input), 429 (rate limited), or 503 (upstream unavailable).
Check a phone number
GET /api/v1/check/phone — 1 credit per call. Rate limit: 600 requests/minute per key. Up to 50 checks per account may run at the same time (higher per-account limits are available on request); every 429 response carries a Retry-After header with the number of seconds to wait before retrying.
Query parameters:
phone(required) — the phone number to score.country(optional) — ISO-3166 alpha-2 country hint, e.g.US.
curl -H "Authorization: Bearer fn_..." \
"https://fraudnova.com/api/v1/check/phone?phone=18176791808&country=US"
import requests
r = requests.get(
"https://fraudnova.com/api/v1/check/phone",
headers={"Authorization": "Bearer fn_..."},
params={"phone": "18176791808", "country": "US"},
)
print(r.json()["data"]["fraud_score"])
const res = await fetch(
"https://fraudnova.com/api/v1/check/phone?phone=18176791808&country=US",
{ headers: { Authorization: "Bearer fn_..." } }
);
const body = await res.json();
console.log(body.data.fraud_score);
$client = new GuzzleHttp\Client();
$res = $client->get("https://fraudnova.com/api/v1/check/phone", [
"headers" => ["Authorization" => "Bearer fn_..."],
"query" => ["phone" => "18176791808", "country" => "US"],
]);
$body = json_decode((string) $res->getBody(), true);
echo $body["data"]["fraud_score"];
$client = new FraudNova\Client("fn_...");
$result = $client->phone("18176791808", "US");
echo $result->fraudScore;
Success response:
{
"success": true,
"data": {
"valid": true,
"fraud_score": 10,
"recent_abuse": false,
"VOIP": false,
"prepaid": false,
"risky": false,
"active": true,
"carrier": "AT&T Wireless",
"line_type": "Wireless",
"country": "US",
"region": "Texas",
"city": "Fort Worth",
"timezone": "America/Chicago",
"zip_code": "76177",
"dialing_code": 1,
"do_not_call": false,
"leaked": false,
"spammer": false,
"active_status": "Active",
"formatted": "+1 817-679-1808",
"local_format": "8176791808"
},
"credits_remaining": 3
}
Errors use { "success": false, "message": "..." } with status 401 (bad token), 402 (insufficient credits), 422 (invalid input), 429 (rate limited), or 503 (upstream unavailable).
Batch check
POST /api/v1/check/batch — score up to 100 IPs in one request. 1 credit per successful check. Options (strictness, mobile, user_agent, user_language) apply to the whole batch.
curl -X POST -H "Authorization: Bearer fn_..." \
-H "Content-Type: application/json" \
-d '{"ips":["8.8.8.8","1.1.1.1"],"strictness":2}' \
"https://fraudnova.com/api/v1/check/batch"
import requests
r = requests.post(
"https://fraudnova.com/api/v1/check/batch",
headers={"Authorization": "Bearer fn_..."},
json={"ips": ["8.8.8.8", "1.1.1.1"], "strictness": 2},
)
for item in r.json()["results"]:
print(item["ip"], item.get("data", {}).get("fraud_score"))
const res = await fetch("https://fraudnova.com/api/v1/check/batch", {
method: "POST",
headers: {
"Authorization": "Bearer fn_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ ips: ["8.8.8.8", "1.1.1.1"], strictness: 2 }),
});
const body = await res.json();
console.log(body.results);
$client = new GuzzleHttp\Client();
$res = $client->post("https://fraudnova.com/api/v1/check/batch", [
"headers" => ["Authorization" => "Bearer fn_..."],
"json" => ["ips" => ["8.8.8.8", "1.1.1.1"], "strictness" => 2],
]);
$body = json_decode((string) $res->getBody(), true);
print_r($body["results"]);
$client = new FraudNova\Client("fn_...");
$batch = $client->batchIp(["8.8.8.8", "1.1.1.1"], new FraudNova\IpOptions(strictness: 2));
foreach ($batch as $item) {
echo $item->subject, ": ", $item->result?->fraudScore ?? $item->error, PHP_EOL;
}
Success response (HTTP 200):
{
"success": true,
"results": [
{ "ip": "8.8.8.8", "success": true, "data": { "fraud_score": 0, "proxy": false } },
{ "ip": "1.1.1.1", "success": false, "error": "upstream_failed" }
],
"credits_remaining": 98
}
Per-IP error is one of invalid_ip, insufficient_credits, or upstream_failed. The request is 422 only on a bad envelope (missing/empty ips or more than 100), or 402 if your balance is already zero, or 401 if your Bearer token is missing/invalid.
Batch check emails
POST /api/v1/check/email/batch — score up to 100 emails in one request. 1 credit per successful check.
curl -X POST -H "Authorization: Bearer fn_..." \
-H "Content-Type: application/json" \
-d '{"emails":["user@example.com","test@example.org"]}' \
"https://fraudnova.com/api/v1/check/email/batch"
import requests
r = requests.post(
"https://fraudnova.com/api/v1/check/email/batch",
headers={"Authorization": "Bearer fn_..."},
json={"emails": ["user@example.com", "test@example.org"]},
)
for item in r.json()["results"]:
print(item["email"], item.get("data", {}).get("fraud_score"))
const res = await fetch("https://fraudnova.com/api/v1/check/email/batch", {
method: "POST",
headers: {
"Authorization": "Bearer fn_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ emails: ["user@example.com", "test@example.org"] }),
});
const body = await res.json();
console.log(body.results);
$client = new GuzzleHttp\Client();
$res = $client->post("https://fraudnova.com/api/v1/check/email/batch", [
"headers" => ["Authorization" => "Bearer fn_..."],
"json" => ["emails" => ["user@example.com", "test@example.org"]],
]);
$body = json_decode((string) $res->getBody(), true);
print_r($body["results"]);
$client = new FraudNova\Client("fn_...");
$batch = $client->batchEmail(["user@example.com", "test@example.org"]);
foreach ($batch as $item) {
echo $item->subject, ": ", $item->result?->fraudScore ?? $item->error, PHP_EOL;
}
Success response (HTTP 200):
{
"success": true,
"results": [
{ "email": "user@example.com", "success": true, "data": { "fraud_score": 15, "disposable": false } },
{ "email": "bad@invalid", "success": false, "error": "invalid_email" }
],
"credits_remaining": 97
}
Per-email error is one of invalid_email, insufficient_credits, or upstream_failed. The request is 422 only on a bad envelope (missing/empty emails or more than 100), or 402 if your balance is already zero, or 401 if your Bearer token is missing/invalid.
Batch check phone numbers
POST /api/v1/check/phone/batch — score up to 100 phone numbers in one request. 1 credit per successful check. An optional country applies to the whole batch.
curl -X POST -H "Authorization: Bearer fn_..." \
-H "Content-Type: application/json" \
-d '{"phones":["18176791808","14155552671"],"country":"US"}' \
"https://fraudnova.com/api/v1/check/phone/batch"
import requests
r = requests.post(
"https://fraudnova.com/api/v1/check/phone/batch",
headers={"Authorization": "Bearer fn_..."},
json={"phones": ["18176791808", "14155552671"], "country": "US"},
)
for item in r.json()["results"]:
print(item["phone"], item.get("data", {}).get("fraud_score"))
const res = await fetch("https://fraudnova.com/api/v1/check/phone/batch", {
method: "POST",
headers: {
"Authorization": "Bearer fn_...",
"Content-Type": "application/json",
},
body: JSON.stringify({ phones: ["18176791808", "14155552671"], country: "US" }),
});
const body = await res.json();
console.log(body.results);
$client = new GuzzleHttp\Client();
$res = $client->post("https://fraudnova.com/api/v1/check/phone/batch", [
"headers" => ["Authorization" => "Bearer fn_..."],
"json" => ["phones" => ["18176791808", "14155552671"], "country" => "US"],
]);
$body = json_decode((string) $res->getBody(), true);
print_r($body["results"]);
$client = new FraudNova\Client("fn_...");
$batch = $client->batchPhone(["18176791808", "14155552671"], "US");
foreach ($batch as $item) {
echo $item->subject, ": ", $item->result?->fraudScore ?? $item->error, PHP_EOL;
}
Success response (HTTP 200):
{
"success": true,
"results": [
{ "phone": "18176791808", "success": true, "data": { "fraud_score": 10, "VOIP": false } },
{ "phone": "123", "success": false, "error": "invalid_phone" }
],
"credits_remaining": 95
}
Per-phone error is one of invalid_phone, insufficient_credits, or upstream_failed. The request is 422 only on a bad envelope (missing/empty phones or more than 100), or 402 if your balance is already zero, or 401 if your Bearer token is missing/invalid.
Check balance
GET /api/v1/balance — requires a Bearer token; free, does not consume a credit. Returns your account's current credit balance.
curl -H "Authorization: Bearer fn_..." \
"https://fraudnova.com/api/v1/balance"
{ "success": true, "credits_remaining": 4200 }
My IP
GET /api/v1/my-ip — free, no auth. Returns the caller's IP (Cloudflare-aware).
{ "success": true, "ip": "203.0.113.45" }
OpenAPI & SDKs
Download the machine-readable OpenAPI 3.1 spec to import into Postman or Insomnia, or to generate a client SDK in your language.
Runnable examples in curl, Python, Node.js and PHP, a ready-made Postman collection, webhook signature verifiers and three importable n8n workflows live in the fraudnova-examples repository on GitHub.
PHP projects can use the official PHP SDK (fraudnova/fraudnova-php on Packagist) instead of raw HTTP calls.
If you use n8n, add the verified FraudNova node (n8n-nodes-fraudnova) instead of HTTP Request nodes: on n8n Cloud it is in the node picker, on self-hosted n8n install it under Settings, Community nodes.
Webhooks
Register HTTPS endpoints under Dashboard → Webhooks to receive signed, server-to-server event notifications. Each endpoint receives every event.
| Event | Fires when |
|---|---|
| balance.low | Your credit balance drops below the low-balance threshold. |
| balance.depleted | Your credit balance reaches zero. |
| credits.purchased | A credit purchase is confirmed and applied. |
Every delivery is a POST with this JSON body:
{
"event": "balance.low",
"data": { "user_id": 42, "balance": 80, "threshold": 100 },
"occurred_at": "2026-06-27T14:03:11+00:00"
}
Verify authenticity with the X-FraudNova-Signature header (sha256=<hex>), an HMAC-SHA256 of the raw request body keyed with your endpoint's signing secret. Always compare in constant time.
// PHP
$payload = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
$signature = $_SERVER['HTTP_X_FRAUDNOVA_SIGNATURE'] ?? '';
if (! hash_equals($expected, $signature)) {
http_response_code(400);
exit;
}
// Node.js (Express, raw body)
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get('X-FraudNova-Signature') || ''));
if (!ok) return res.status(400).end();
Respond with a 2xx status quickly. Non-2xx responses are retried up to 4 times with backoff (10s, 60s, 300s).