FraudNova

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.

EventFires when
balance.lowYour credit balance drops below the low-balance threshold.
balance.depletedYour credit balance reaches zero.
credits.purchasedA 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).