API Documentation
Welcome to the CandycornDB API. Query real-time IP risk scores, screen IPs in bulk, submit community abuse reports, and pull ASN threat intelligence — all from a single credit-based surface. Every response exposes a transparent scoreAdjustments object so you can see exactly which signals drove an IP's score.
Base URL
https://candycorndb.com/api
Quickstart
Three steps to your first score. Grab a key from the Dashboard, pick your language, and paste. Everything else — the ASN classifier, the proxy heuristic, the community-abuse signal — is already running. Your integration just reads the JSON.
x-api-key header. Copy from any tab below.scorecurl -X GET "https://candycorndb.com/api/public/ip-score?ip=185.220.101.44" \
-H "x-api-key: $CANDYCORN_API_KEY"
const axios = require('axios');
const { data } = await axios.get(
'https://candycorndb.com/api/public/ip-score',
{
params: { ip: '185.220.101.44' },
headers: { 'x-api-key': process.env.CANDYCORN_API_KEY }
}
);
if (data.score >= 70 || data.isProxy) {
// block, CAPTCHA, or escalate to manual review
console.warn('High-risk IP', data.ip, data.scoreReasons);
}
import os, requests
r = requests.get(
"https://candycorndb.com/api/public/ip-score",
params={"ip": "185.220.101.44"},
headers={"x-api-key": os.environ["CANDYCORN_API_KEY"]},
timeout=5,
)
r.raise_for_status()
data = r.json()
if data["score"] >= 70 or data.get("isProxy"):
# block, CAPTCHA, or escalate to manual review
print("High-risk IP", data["ip"], data.get("scoreReasons", []))
Authentication
Authenticate every request by sending your API key in the x-api-key header (case-insensitive). Missing or invalid keys return 401 Unauthorized. You can generate, rotate, and revoke keys in the Dashboard — revocations propagate within the 10-minute Redis auth cache TTL.
All endpoints are credit-based. The developer tier is metered against a rolling 24-hour window (UTC day boundary). Paid tiers (production, scale, enterprise) are metered against a rolling calendar month (UTC, resets on the 1st). Every successful call atomically charges credits in Redis before the handler executes. Single-IP calls cost 1 credit, bulk calls cost N credits (one per valid IP), and abuse-report submissions cost 1 credit. Exceeding your budget returns 429 with limit, used, window ("day" or "month"), resets, and tier fields so you can gracefully back off.
curl -X GET "https://candycorndb.com/api/public/ip-score?ip=1.2.3.4" \
-H "x-api-key: YOUR_API_KEY"
Credit Limits by Tier
| Tier | Credit Pool | Reset Window | Bulk Cap / Call | Premium Endpoints |
|---|---|---|---|---|
| developer | 100 | Daily (UTC) | — | — |
| production | 100,000 | Monthly (UTC) | 100 | Submit Abuse Report |
| scale | 1,000,000 | Monthly (UTC) | 1,000 | Abuse History, High-Risk ASNs, ASN Clusters |
| enterprise | Unlimited | Monthly (UTC) | 10,000 | All premium endpoints |
Data Transparency
Data Sources
Every score we return is produced from five independent intelligence layers, merged, cross-validated, and re-scored continuously. No single source dominates the decision — that's how we stay ahead of IPs that slip past free blacklists and generic geolocation feeds. Every signal we use is surfaced back to you through scoreReasons / scoreAdjustments, so there are no "black box" components to audit around.
firstSeen timestamp, and every subsequent re-observation ticks lastSeen, giving you an unbroken activity ledger per IP.
/24 neighbor analysis to catch coordinated botnet clusters before individual IPs accumulate enough reports to trip a list. When a new IP shows up on a /24 where 31 of 256 neighbors are already flagged, we surface that as networkCluster: +25 — you block the attacker on the first request, not the fifty-first. Powered by an indexed integer range query against the ipLong field, so the cost is O(log N) no matter how many IPs we track. /22 aggregation is on the P1 roadmap.
asnType you don't have to guess. Backed by our own ASN database (200k+ ASNs indexed with risk scores, prefixes, and BGP relationships) plus a hostname-override classifier that catches proxy/VPN tells the ASN alone would miss.
communityAbuse in scoreAdjustments — you see exactly how many reports drove the adjustment and when the most recent one fired.
Why this beats a free blacklist
Free IP blacklists are single-layer: an IP is on the list or it isn't. Our five layers compose. A brand-new hosting IP with zero feed hits can still score 60 on asnHosting (+15) + proxyInferred (+20) + networkCluster (+25) alone — High band, actionable on day zero instead of waiting weeks for a blocklist to notice. Conversely, a clean residential IP on a single noisy feed stays < 40 because the v2.3 asnResidentialBonus (−10) offsets most of the feed contribution — so you stop false-positiving your paying customers.
/api/public/ip-score
The primary endpoint. Returns the risk score, threat signals (Tor/VPN), and location data for a single IPv4 address. Costs 1 credit per call. Available on all tiers (developer and up). Responses are cached for 1 hour. If the IP has never been observed, a "lite" score is returned immediately and the IP is enqueued for deep scan (de-duplicated for 1 hour per IP).
Parameters
| Name | Type | Description |
|---|---|---|
| ip | string (query) | The IPv4 address you want to score. Required. |
Example Request
curl -X GET "https://candycorndb.com/api/public/ip-score?ip=185.220.101.44" \
-H "x-api-key: $CANDYCORN_API_KEY"
const axios = require('axios');
async function scoreIp(ip) {
const { data } = await axios.get(
'https://candycorndb.com/api/public/ip-score',
{
params: { ip },
headers: { 'x-api-key': process.env.CANDYCORN_API_KEY },
timeout: 5000
}
);
return data;
}
const result = await scoreIp('185.220.101.44');
console.log(result.score, result.asnType, result.isProxy);
import os, requests
def score_ip(ip: str) -> dict:
r = requests.get(
"https://candycorndb.com/api/public/ip-score",
params={"ip": ip},
headers={"x-api-key": os.environ["CANDYCORN_API_KEY"]},
timeout=5,
)
r.raise_for_status()
return r.json()
result = score_ip("185.220.101.44")
print(result["score"], result["asnType"], result["isProxy"])
Response Comparison
The same schema is returned for every cached IP. What changes is the signal composition. The two examples below show the two ends of the spectrum — a clean residential IP vs. a known Tor exit running on a flagged hosting ASN. Every field is defined in the Data Dictionary.
{
"ip": "73.14.58.201",
"score": 0, // v2.3 base-zero: 0 = fully trusted
"asn": "AS7922 - Comcast",
"country": "US",
"isVPN": false,
"isTor": false,
"asnType": "residential",
"isProxy": false,
"firstSeen": "2025-11-04T09:11:22.000Z",
"lastSeen": "2026-04-22T06:48:03.910Z",
"scoreReasons": [
{ "component": "asnResidentialBonus", "delta": -10, "detail": "Residential ISP (non-proxy)" }
],
"scoreAdjustments": {
"asnResidentialBonus": -10 // -10 delta clamped to 0 (floor)
},
"scoreVersion": "v2.3-base-zero"
}
{
"ip": "185.220.101.44",
"score": 100, // 0 + 45 + 15 + 20 + 25 + 25 = 130 → clamped to 100
"asn": "AS9009 - M247 Ltd",
"country": "DE",
"isVPN": true,
"isTor": true,
"asnType": "hosting",
"isProxy": true,
"firstSeen": "2024-07-18T02:19:41.000Z",
"lastSeen": "2026-04-22T14:03:09.441Z",
"scoreReasons": [
{ "component": "tor", "delta": 45, "detail": "Tor Exit Node" },
{ "component": "asnHosting", "delta": 15, "detail": "Hosting/datacenter keyword: \"m247\"" },
{ "component": "proxyInferred", "delta": 20, "detail": "Proxy/VPN signal in ASN or hostname" },
{ "component": "networkCluster", "delta": 25, "detail": "High Risk Cluster: 185.220.101.0/24 (31 neighbors)" },
{ "component": "communityAbuse", "delta": 25, "detail": "Community abuse reports: 18 reports, weight=24" }
],
"scoreAdjustments": {
"tor": 45,
"asnHosting": 15,
"proxyInferred": 20,
"networkCluster": 25,
"communityAbuse": 25
},
"scoreVersion": "v2.3-base-zero"
}
Response — New IP (JIT)
If the IP has never been observed, we return a provisional "lite" score immediately (based on geo-risk only, starting from the v2.3 base-zero baseline of 0) and enqueue the full classifier for a background deep-scan. Subsequent calls for the same IP return the enriched cached shape above within minutes. firstSeen and lastSeen both collapse to "now" — this request is the first observation.
{
"ip": "203.0.113.17",
"score": 0, // v2.3 base-zero: no signals yet → 0 (Low)
"country": "US",
"status": "Analyzing",
"message": "New IP detected. Deep scan initiated.",
"asnType": "unknown",
"isProxy": false,
"firstSeen": "2026-04-22T14:22:09.413Z",
"lastSeen": "2026-04-22T14:22:09.413Z",
"scoreReasons": [],
"scoreAdjustments": {}
}
Risky-geo countries (CN, RU, KP, IR) bump the lite score to 20 (Medium) via a geoRisk adjustment; everything else starts at 0.
/api/public/bulk-score
Score many IPs in a single round-trip. The server performs exactly one indexed query against the cleaned-data collection (via the ipLong integer index) regardless of batch size. Cache-miss IPs are returned immediately with status: "Analyzing" and enqueued for background deep scan. Requires Production tier or higher.
Tier Limits & Billing
| Tier | Max IPs / Call | Credits Charged |
|---|---|---|
| developer | Not available (403) | — |
| production | 100 | 1 per valid, deduped IP |
| scale | 1,000 | 1 per valid, deduped IP |
| enterprise | 10,000 | 1 per valid, deduped IP |
Only valid and deduplicated IPs are charged. Malformed IPs are returned in the invalid array and never billed. Duplicate IPs inside the same request body are collapsed silently. The whole batch is rejected with 429 if it would push you over your plan's credit budget (monthly on paid tiers) — there is no partial processing.
Example Request
curl -X POST "https://candycorndb.com/api/public/bulk-score" \
-H "x-api-key: $CANDYCORN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "ips": ["1.1.1.1", "8.8.8.8", "185.220.101.44"] }'
const axios = require('axios');
async function bulkScore(ips) {
const { data } = await axios.post(
'https://candycorndb.com/api/public/bulk-score',
{ ips },
{
headers: {
'x-api-key': process.env.CANDYCORN_API_KEY,
'Content-Type': 'application/json'
},
timeout: 10000
}
);
return data.results;
}
const scored = await bulkScore(['1.1.1.1', '8.8.8.8', '185.220.101.44']);
const risky = scored.filter(r => r.score >= 70 || r.isProxy);
import os, requests
def bulk_score(ips: list[str]) -> list[dict]:
r = requests.post(
"https://candycorndb.com/api/public/bulk-score",
json={"ips": ips},
headers={
"x-api-key": os.environ["CANDYCORN_API_KEY"],
"Content-Type": "application/json",
},
timeout=10,
)
r.raise_for_status()
return r.json()["results"]
scored = bulk_score(["1.1.1.1", "8.8.8.8", "185.220.101.44"])
risky = [r for r in scored if r["score"] >= 70 or r.get("isProxy")]
Response — Transparent Score
Every result carries the same enriched shape as the single-IP endpoint: classifier labels (asnType, isProxy), temporal observability (firstSeen, lastSeen), and the full per-signal scoreAdjustments breakdown. Cache-miss IPs return status: "Analyzing" and are enqueued for a background deep-scan.
{
"submitted": 3,
"processed": 3,
"hits": 2,
"queued": 1,
"invalid": [],
"invalidCount": 0,
"creditsCharged": 3,
"tier": "scale",
"results": [
{
// CRITICAL: hosting + Tor + flagged /24 + recent abuse (clamped at 100)
"ip": "185.220.101.44",
"score": 100,
"asn": "AS9009 - M247 Ltd",
"country": "DE",
"city": "Frankfurt",
"isp": "M247 Ltd",
"isVPN": false,
"isTor": true,
"asnType": "hosting",
"isProxy": true,
"status": "Analyzed",
"scoreAdjustments": {
"tor": 45, // known Tor exit node
"asnHosting": 15, // hosting/datacenter ASN
"proxyInferred": 20, // ISP/hostname proxy signal
"networkCluster": 25, // /24 cluster risk > 60 (P1)
"communityAbuse": 25 // weighted abuse reports, 90d
},
"firstSeen": "2024-07-18T02:19:41.000Z",
"lastSeen": "2026-04-22T14:03:09.441Z",
"cleanedAt": "2026-04-22T14:03:09.441Z"
},
{
// CLEAN: residential ISP, v2.3 trust bonus fires
"ip": "73.14.58.201",
"score": 0,
"asn": "AS7922 - Comcast",
"country": "US",
"isVPN": false,
"isTor": false,
"asnType": "residential",
"isProxy": false,
"status": "Analyzed",
"scoreAdjustments": {
"asnResidentialBonus": -10 // clamped to 0 floor
},
"firstSeen": "2025-11-04T09:11:22.000Z",
"lastSeen": "2026-04-22T06:48:03.910Z",
"cleanedAt": "2026-04-22T06:48:03.910Z"
},
{
// NEW: cache-miss — v2.3 lite score (0 baseline), deep scan queued
"ip": "203.0.113.17",
"score": 0,
"country": "US",
"status": "Analyzing",
"asnType": "unknown",
"isProxy": false,
"firstSeen": "2026-04-22T14:22:09.413Z",
"lastSeen": "2026-04-22T14:22:09.413Z",
"message": "New IP detected. Deep scan initiated."
}
]
}
The scoreAdjustments object is the "why behind the score" — each key is the contribution (in points) of one signal toward the final score. An empty object ({}) means no risk signals fired. Missing keys mean the signal was not evaluated (e.g., pre-v2 historical records). Use this to build explainable fraud rules in your own system instead of treating the score as a black box.
Data Dictionary
Every field returned by /api/public/ip-score and /api/public/bulk-score, in one place. Fields marked RISK are the most useful for gating decisions; fields marked META are metadata for UX or analytics.
0 is fully trusted, 100 is maximum risk. The four canonical risk bands map one-to-one with production gating:
0–14 Low · allow. Clean residential / mobile / verified crawlers.
15–39 Medium · monitor or rate-limit. One mild signal firing (hosting, proxy, or subnet).
40–69 High · CAPTCHA / step-up auth / enrichment. Multiple stacking signals.
70–100 Critical · block or route to manual review.
Recommended predicates:
score ≥ 40 || isProxy for aggressive fraud-sensitive flows, score ≥ 70 for pure blocking. Pre-v2.3 consumers that gated on ≥ 50 should migrate to ≥ 40 || isProxy to preserve equivalent selectivity.
"hosting" is a high-risk indicator. Real user traffic almost never originates from a cloud/VPS/colo ASN (AWS, GCP, OVH, Hetzner, DigitalOcean, …), so a "hosting" classification on a login/checkout/signup request is nearly always a bot, scraper, proxy, or a sophisticated attacker routing through rented infrastructure. The classifier adds +15 to the score for any hosting IP. "residential" and "mobile" are the expected values for genuine human traffic; "unknown" means the ISP string didn't match any keyword list (treat as neutral).
true when either (a) the ISP/ASN name matches a hosting-infrastructure keyword, or (b) the reverse-DNS hostname contains an explicit tell (vps, server, node, proxy, vpn). Deliberately broader than asnType === "hosting" — a consumer ISP with a "*-vpn.example.net" PTR will flag isProxy: true but asnType: "residential". Adds +20 to the score. Prefer this over the legacy isVPN field in new integrations.
firstSeen + high score often indicates a brand-new botnet node still spinning up). May be null for historical rows that pre-date the v2.2 schema cut.
firstSeen to reason about IP lifespan. A stale lastSeen (e.g., months old) for a still-flagged IP means the threat signal came from long-term feeds rather than recent activity — useful for tuning false-positive tolerance.
component (the reason code, e.g. "tor"), delta (signed integer contribution, e.g. +45 or -10), and detail (human-readable context). Render detail verbatim in fraud-review UIs, or iterate on component/delta to build custom rule logic. Empty array means no risk signals fired — the IP is clean.
scoreReasons. Each key is a reason code, each value is the signed point contribution. Common keys: tor (+45), fireholListed (+35), riskStackFireholUnknownIsp (+30), asnHighRisk (+25), networkCluster (+25), blocklistDeListed (+25), proxyInferred (+20), hostnameSuspicious (+20), asnHosting (+15), communityAbuse (+5…+40), asnMobileBonus (−5), asnResidentialBonus (−10), hostnameLegitimate (−15), asnTrusted (−30), trustedCrawler (−50). The object is sparse — only keys that fired are present. An empty object means no signals fired.
isTor is a hard boolean (the IP appears on the Tor Project's exit-node list); isVPN is a narrow heuristic that fires on "vpn" in the ISP string. For new integrations prefer isProxy (broader) and asnType === "hosting". Kept in the response for backwards compatibility.
asn is the "AS12345 - Org Name" form; country is an ISO-3166-1 alpha-2 code. city and isp are present on the /bulk-score response; the single-IP endpoint omits them to keep cached payloads tight.
"Analyzed" — full scoring complete, every field populated from the classifier output. "Analyzing" — first-ever observation; you got a provisional lite score (geo-risk only) and a background deep-scan was enqueued. The same IP on the next call will return "Analyzed".
v2.3-base-zero. Pin your integration tests against this value so a silent scoring-model change surfaces in your CI instead of production.
Subnet Intelligence
Subnet-clustering signals are folded directly into the scoreAdjustments.networkCluster component of every bulk-score response — no separate endpoint is required. During the cleaner pass, each IP's /24 neighborhood is scanned via an indexed integer range query (ipLong >= low AND ipLong < high). Neighbor count is bucketed into a clusterRisk tier (0 / 50 / 70 / 85); anything above 60 contributes a flat +25 to the score. /22 aggregation is on the P1 roadmap. The example below shows how this appears in a bulk response for a flagged neighborhood.
{
"ip": "185.220.101.44",
"score": 95,
"scoreAdjustments": {
"tor": 45,
"asnHighRisk": 25,
"networkCluster": 25 // /24 clusterRisk > 60 (31 of 256 neighbors flagged)
}
}
/api/public/report
Submit a community abuse report for an IP. Reports feed the shared threat corpus and influence the scoreAdjustments.communityAbuse signal (a graduated +5 / +15 / +25 / +40 contribution based on reporter-weighted totalWeight over the last 90 days) for all subscribers. Costs 1 credit per submission. Requires Production tier or higher (the developer tier cannot contribute).
24-hour deduplication: the same API key cannot file more than one report against the same IP in a rolling 24-hour window. Duplicate submissions return 409 Conflict with a dedupTtlSeconds field indicating the remaining cool-down. Dedup is enforced atomically in Redis (SET NX EX) so concurrent submissions are safe.
Body Parameters
| Name | Type | Description |
|---|---|---|
| ip | string (required) | The offending IPv4 address. |
| category | integer or int[] (required) | One or more category codes in [1, 23]. See table below. |
| comment | string (optional) | Free-text context, max 1024 characters. |
| attackedHost | string (optional) | Hostname or domain that was targeted. |
Valid Category Codes (1–23)
| Code | Category | Code | Category |
|---|---|---|---|
| 1 | DNS Compromise | 13 | Spoofing |
| 2 | DNS Poisoning | 14 | Brute-Force Attack |
| 3 | Fraud Orders | 15 | Bad Web Bot |
| 4 | DDoS Attack | 16 | Exploited Host |
| 5 | FTP Brute-Force | 17 | Web App Attack |
| 6 | Ping of Death | 18 | SSH |
| 7 | Phishing | 19 | IoT Targeted |
| 8 | Fraud VoIP | 20 | Credential Stuffing |
| 9 | Open Proxy | 21 | Scraping / Abuse |
| 10 | Web Spam | 22 | Carding / Payment Fraud |
| 11 | Email Spam | 23 | Account Takeover |
| 12 | Blog Spam | — | — |
Request
curl -X POST "https://candycorndb.com/api/public/report" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ip": "203.0.113.17",
"category": [18, 14],
"comment": "SSH brute-force against prod bastion",
"attackedHost": "bastion.example.com"
}'
Response (201 Created)
{
"success": true,
"reportId": "663f2a1c8e2d4b0012a9f7b1",
"ip": "203.0.113.17",
"category": [18, 14],
"reportedAt": "2026-04-22T14:22:09.413Z",
"creditsCharged": 1
}
/api/public/abuse/:ip
Retrieve an aggregated community abuse summary for an IP. Returns up to 100 of the most recent reports along with category counts and the most-recent timestamp. Costs 1 credit per call. Requires Scale tier or higher. Reporter identities are stripped from the response.
Request
curl -X GET "https://candycorndb.com/api/public/abuse/203.0.113.17" \
-H "x-api-key: YOUR_API_KEY"
Response
{
"ip": "203.0.113.17",
"totalReports": 27,
"truncated": false,
"mostRecent": "2026-04-22T14:22:09.413Z",
"categories": {
"14": 12,
"18": 11,
"17": 4
},
"reports": [
{
"ipLong": 3405803801,
"ipAddress": "203.0.113.17",
"reporterTier": "scale",
"category": [18, 14],
"comment": "SSH brute-force against prod bastion",
"attackedHost": "bastion.example.com",
"reportedAt": "2026-04-22T14:22:09.413Z"
}
]
}
ASN Intelligence
Query Autonomous System metadata — names, countries, risk scores, and relationships — under the /api/asn/* prefix. All ASN endpoints share the same credit-based surface (1 credit per call) and respect your plan's reset window (daily for developer, monthly for paid tiers). Basic lookups (:asnNumber, /ip/:ip, /related, /search, /stats) are available to every paying tier.
Available Endpoints
| Method | Path | Description | Tier |
|---|---|---|---|
| GET | /api/asn/asn/:asnNumber | Lookup one ASN by number (e.g. AS9009). |
Production+ |
| GET | /api/asn/ip/:ipAddress | Resolve an IP to its owning ASN. | Production+ |
| GET | /api/asn/asn/:asnNumber/related | Peer / upstream / downstream ASNs. | Production+ |
| GET | /api/asn/search | Search by name / country / risk range. | Production+ |
| GET | /api/asn/stats | Global ASN database statistics. | Production+ |
| GET | /api/asn/high-risk | Rank ASNs by risk score. | Scale / Enterprise only |
| GET | /api/asn/clusters | Coordinated ASN clusters (fraud rings). | Scale / Enterprise only |
Example — ASN Lookup
curl -X GET "https://candycorndb.com/api/asn/asn/AS9009" \
-H "x-api-key: YOUR_API_KEY"
/api/asn/stats
Returns global ASN database statistics — total ASNs indexed, risk-score distribution, top countries, and cluster summaries. Useful for building dashboards or monitoring database growth. Costs 1 credit per call. Available on all paying tiers.
curl -X GET "https://candycorndb.com/api/asn/stats" \
-H "x-api-key: YOUR_API_KEY"
Errors & Rate Limits
We use standard HTTP status codes. Every error response is a JSON object with at minimum an error field; rate-limit and tier-gate responses include additional diagnostic fields so you can react programmatically.
POST /api/public/report on a new abuse report.
ips).
x-api-key header.
currentTier and requiredTier (or upgradeTo) fields.
dedupTtlSeconds.
limit, used, cost, tier, window ("day" or "month"), and resets so you can back off or upgrade.