Scam Detector API
The same scanner that powers scamdetector.eu: four-pillar trust scores (scams, weirdness, data-mining, misinformation) with explainable evidence, for any URL. JSON over HTTPS, live progress over Server-Sent Events, and HMAC-signed webhooks.
Quick start
API keys come with the Pro and Business plans. After checkout your key is shown once on the dashboard. The free tier is the scanner on scamdetector.eu itself. Calls without a key are refused unless they come from the website.
curl -s https://scanner.scamdetector.eu/api/v1/scan \
-H "X-API-Key: sd_live_…" -H "Content-Type: application/json" \
-d '{"url": "hxxps://suspicious-site[.]com/login", "mode": "fast"}' | jq '.report.scores | {trustScore, verdict, confidence}'Your scans are private. Scans made with your API key belong to your organisation. They never appear on scamdetector.eu, in the public database or to other customers, and other people's scans are never visible to your key.
Authentication & limits
Send your key as X-API-Key: sd_live_… or Authorization: Bearer sd_live_…. Keys are stored only as SHA-256 hashes; we cannot show a lost key again, but you can create and revoke keys in the dashboard.
| Plan | Scans / month | Per hour | Per minute | Batch |
|---|---|---|---|---|
| Website (free) | – | 10 | 6 | no API access |
| Pro | 5,000 | 2,000 | 60 | ✓ |
| Business | 50,000 | 10,000 | 300 | ✓ |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and, for keys, X-Quota-Used / X-Quota-Limit. A fresh cached report (under 6 hours old) is returned instantly; pass "forceRescan": true to scan again.
Scanning
POST /api/v1/scan accepts a URL, a bare domain, or a defanged indicator (hxxps://example[.]com). mode is deep (default: adds an isolated headless browser for runtime trackers, wallet requests and keyloggers, ~15 s) or fast (network, threat intelligence and page source, ~5 s).
{
"url": "https://example.com", // required
"mode": "deep", // "deep" | "fast"
"forceRescan": false, // ignore a cached report
"async": false, // true → 202 { scanId, eventsUrl, resultUrl }
"webhookUrl": "https://you/hook" // API keys only: signed POST when done
}Live progress (Server-Sent Events)
Send Accept: text/event-stream to the scan request (or open /api/v1/scan/{scanId}/events) to receive events as each check finishes. A dropped connection resumes with Last-Event-ID or ?lastEventId=.
event: stage_started data: {"stage":"fast-network","analyzers":[{"name":"dns-rdap","label":"…","pillar":"weirdness"},…]}
event: analyzer_completed data: {"analyzer":"threat-intel","status":"ok","findings":1,"durationMs":412}
event: finding_detected data: {"finding":{"id":"SCAM_THREAT_INTEL_…","severity":"critical","title":"…"}}
event: stage_completed data: {"stage":"fast-network","status":"ok","durationMs":2234}
event: scan_complete data: {"report":{…}}The report
report.scores.trustScore is 0–100 with a verdict of trusted (≥80), low_risk (≥65), suspicious (≥50), high_risk (≥25) or critical_scam. Each of the four pillars has its own score. confidence (0–1) reflects how many checks could run and how much they agree. Every finding has a stable id, severity, scoreImpact, plain-English title/description, a tip, structured evidence (secrets redacted) and links to the matching scam guide via scamCategories.
{
"scanId": "6a0c…", "status": "completed", "engineVersion": "2026.11.0",
"scores": {
"trustScore": 15, "verdict": "critical_scam", "confidence": 0.98,
"pillars": { "scams": { "score": 5, "status": "ok" }, "weirdness": {…}, "datamining": {…}, "misinformation": {…} },
"knockouts": [{ "rule": "CRITICAL_SCAM_SIGNAL", "cap": 15, "description": "…" }],
"topReasons": [{ "findingId": "SCAM_THREAT_INTEL_OPENPHISH_COMMUNITY_FEED", "title": "Listed by OpenPhish…", "impact": -80 }],
"scamCategories": [{ "slug": "phishing", "weight": 1 }]
},
"facts": { "domainAgeDays": 3, "registrar": "…", "asnOrg": "…", "tlsIssuer": "…", "trackerCount": 0 },
"findings": [ … ], "evidence": { "redirectChain": […], "dns": {…}, "tls": {…}, "trackers": […], "threatIntel": […] }
}Webhooks
With an API key, pass webhookUrl (HTTPS) and we POST { event: "scan.completed", scanId, report } when the scan finishes. Verify the X-ScamDetector-Signature: t=<unix>,v1=<hex> header: HMAC-SHA256 of `${t}.${rawBody}` with your webhook secret (from GET /api/v1/account), and reject timestamps older than 5 minutes.
import { createHmac, timingSafeEqual } from "node:crypto"
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]))
const t = Number(parts.t)
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}All endpoints
| POST | /api/v1/scan | Scan a URL. Waits for the report (≤60 s), streams progress with Accept: text/event-stream, or returns 202 with async: true. |
| GET | /api/v1/scan/{scanId} | Result (or progress) of one of your scans. |
| GET | /api/v1/scan/{scanId}/events | Server-Sent Events progress stream; resumable with Last-Event-ID. |
| GET | /api/v1/scan/{scanId}/screenshot | JPEG screenshot from the isolated browser (deep scans). |
| POST | /api/v1/batch-scan | Up to 100 URLs in one request (Pro and Business). |
| GET | /api/v1/scans | Your organisation's scans, newest first (search by hostname, filter by verdict). |
| GET | /api/v1/domain/{domain} | Your latest scan of a domain and its score history. Never starts a scan. |
| GET | /api/v1/feeds/scams | Recently verified scam detections (public data, last 100). |
| POST | /api/v1/reports | Submit a community scam report. |
| GET | /api/v1/account | Your plan, quota, usage, keys and webhook secret. |
| GET | /api/v1/status | Engine version, threat-feed freshness and capacity. |
Errors
Errors are JSON: { "error": { "code": "RATE_LIMITED", "message": "…" }, "requestId": "…" }. Common codes: INVALID_URL, PRIVATE_ADDRESS (we never scan internal networks), RATE_LIMITED / QUOTA_EXCEEDED (with Retry-After), SCANNER_BUSY (503, retry shortly), API_KEY_REQUIRED, INVALID_API_KEY, PLAN_REQUIRED (the key's plan is no longer active), NOT_SCANNED.
Fair use
The scanner fetches pages from its own servers and never from your users' devices. It does not submit forms, log in, or follow destructive links; exposed-file checks only request a fixed list of well-known paths and never store their contents. Please do not use the API to scan sites you are not allowed to test at high volume. Reports are automated risk assessments, not legal findings.