API Reference

Real-time phishing intelligence as JSON, CSV, or TXT. Open, free, no auth required.

758 Active
No auth Open access
10 req/s Per IP
Hourly Refresh

Building an AI agent? See For AI Agents for the MCP server and Agent Skills wrappers.

Introduction

The detection pipeline identifies active suspicious phishing sites in real time by monitoring multiple threat intelligence sources and Certificate Transparency logs. Data is enriched with IP geolocation, TLS certificate information, and detection verdicts.

Detection sources:

Pipeline schedule

ProcessFrequencyDescription
Detection pipelineEvery hourScans Certificate Transparency logs and checks threat intel sources for new suspicious phishing domains
Active site re-checkEvery 6 hoursRe-visits active sites, captures fresh screenshots, and updates detection verdicts
New registration scanDaily (00:30 UTC)Scans newly registered domains for suspicious keyword patterns
Data enrichmentOn detectionIP geolocation, ASN, TLS certificate, and hosting organization via ipinfo.io

Endpoint URL

https://phishunt.io/api/v1/domains

Get domains

GET /api/v1/domains Try it

Returns active suspicious phishing domains with enrichment data: IP geolocation, TLS certificate issuer, hosting org, and detection verdicts from Google Safe Browsing, OpenPhish, PhishTank, TweetFeed, and urlscan.io.

Population: active detections minus parked domains (high confidence) and hostname-intent clones; PaaS-hosted kits (pages.dev, vercel.app, github.io...) are included.

In the JSON output, each row has a service_range field: null, or the official provider IP range that contains ip (for example an AWS or Google Cloud range). It describes the network of the IP, not the indicator, and it is not a verdict. /api/v1/search.json rows and /api/v1/domains/{uuid}/related results carry it too. The feed files, the CSV output, the blocklists and the STIX bundle do not.

Query parameters

ParameterTypeDescription
limit integer Number of results to return. Omit to get all current results; max 1000 when set.
offset integer Number of results to skip for pagination. Default 0, max 10000.
format string Response format: json, csv, or txt. Default json.
company string Filter by targeted brand (e.g. amazon, paypal). See targeted brands.
since ISO 8601 Only entries after this date (e.g. 2026-03-01).
contains string Case-insensitive substring match on the stored URL (host and path). 3-80 chars, letters/digits/-._ only. search and q are accepted as aliases (compatibility; contains wins if several are sent).
tier string Liveness filter: all or verified (has a usable screenshot capture or a positive urlscan.io verdict). Default all.
asn string Exact-match pivot on asn. Digits, AS prefix accepted (e.g. 15169 or AS15169).
org string Exact-match pivot on org, value as returned by this endpoint.
registrar string Exact-match pivot on registrar, value as returned by this endpoint.
cert string Exact-match pivot on cert (certificate issuer), value as returned by this endpoint.
country string Exact-match pivot on country (e.g. Germany), value as returned by this endpoint.
ip string Exact-match pivot on ip, IPv4 string, exactly as returned by this endpoint.

Example request

curl "https://phishunt.io/api/v1/domains"

Example response

{
  "count": 2,
  "total": 2,
  "offset": 0,
  "limit": null,
  "results": [
    {
      "url": "https://amazon.example-phish.com/signin",
      "domain": "amazon.example-phish.com",
      "company": "amazon",
      "date": "2026-03-27T14:30:00+00:00",
      "first_seen": "2026-03-25T09:12:00+00:00",
      "uuid": "54889cb5-146d-484f-8b94-7a0b7385bff7",
      "ip": "198.51.100.42",
      "country": "United States",
      "asn": "64496",
      "org": "Example Hosting Inc.",
      "cert": "Let's Encrypt R3",
      "malicious_google": false,
      "malicious_openphish": true,
      "malicious_phishtank": false,
      "malicious_tweetfeed": false,
      "malicious_urlscan": true
    }
  ]
}

Feeds

Download the complete active suspicious phishing feed. Updated every hour. Full dataset with no filtering - use the domains endpoint for query parameters.

Feed files return all active entries as a download. The JSON feed is a flat array (no wrapper object). The TXT feed contains one URL per line. The STIX 2.1 bundle carries one indicator per detection, with confidence derived from that detection's own score - never a flat number across every indicator. Import the OpenAPI spec into Postman or Insomnia.

Blocklists

DNS/firewall blockers match by domain, not hostname - the feeds above ship full hostnames instead. These six formats collapse each detection to its registrable domain first, so a resolver or filter list can block it outright.

FormatConsumerSetup
Domains Pi-hole Group Management → Adlists → https://phishunt.io/blocklist/domains.txt
Hosts /etc/hosts-style blockers curl -fsSL https://phishunt.io/blocklist/hosts.txt | sudo tee -a /etc/hosts
Adblock uBlock Origin / AdGuard Filter subscription URL: https://phishunt.io/blocklist/adblock.txt
dnsmasq dnsmasq conf-file=/etc/dnsmasq.d/phishunt.conf
unbound pfSense / OPNsense include: "/etc/unbound/phishunt.conf"
RPZ BIND / Knot response-policy { zone "phishunt-rpz"; };
dnsmasq, unbound and RPZ are config directives, not full setups: fetch the matching file to the path they reference (cron, every 15 minutes or less) before adding the line above. Every format collapses shared/multi-tenant hosting (Blogspot, GitBook, Zapier Interfaces...) to the individual phishing host, never to the bare provider domain.

Analyze URL

GET /api/v1/analyze Try it

Passive analysis of any URL: checks it against phishunt's stored detections (active feed, prior detections, new-registration feed) and runs live URL-shape heuristics (brand match, typosquat/homograph distance, TLD abuse, PaaS hosting, clone intent). The target URL is never contacted. Suspicious unknown domains are queued for full pipeline analysis.

Query parameters

ParameterTypeDescription
url string The URL to analyze. Required. http/https only, max 2048 characters.

Example request

curl "https://phishunt.io/api/v1/analyze?url=https://amazon-secure-login.example.com/signin"

Example response

{
  "query": { "url": "...", "host": "...", "normalized_host": "...", "apex": "..." },
  "known": {
    "in_active_feed": false,
    "previously_active": false,
    "in_new_registration_feed": false,
    "record": null
  },
  "live_analysis": {
    "allowlisted": false,
    "brand_match": "amazon",
    "kw_score": 85,
    "impersonation": false,
    "url_signals": { "kw_score_norm": 0.85, "idn_homograph": 0.0, "tld_abuse": 0.3, "clone_intent": 0.0, "paas_host": 0.0, "leet_brand": 0.0 },
    "path_signals": { "brand_off_host": 0.0, "suspicious_path": 1.0, "exec_page": 0.0 },
    "url_risk_score": 72,
    "url_risk": "high",
    "why": [
      { "signal": "kw_score_norm", "value": 0.85, "weight": 0.26, "points": 22.1, "kind": "weighted" },
      { "signal": "suspicious_path", "value": 1.0, "weight": 0.0, "points": 12.0, "kind": "path_boost" }
    ]
  },
  "history": { "apex_prior_detections": 0, "apex_candidates_seen": 0, "scope": "apex", "brands_targeted": [] },
  "action": { "queued_for_analysis": false, "reason": "reserved_domain" },
  "external_feeds": { "openphish": false, "phishtank": false, "tweetfeed": false, "listed": false, "listed_scope": null, "note": null, "status": "ok", "feeds_ok": true },
  "verdict": "likely_phishing",
  "verdict_confidence": "medium",
  "verdict_basis": ["the URL shape scores 72/100"],
  "probability": {
    "status": "ok",
    "p_malicious": 0.127,
    "percent": 13,
    "interval_80": [0.079, 0.198],
    "band": "unlikely",
    "relative_risk": 9.5,
    "coverage": "passive_url_only",
    "prior": 0.01342,
    "evidence": [
      { "class": "url_shape", "value": 72, "llr": 2.74, "direction": "toward_malicious" },
      { "class": "external_feed", "value": "none", "llr": -0.39, "direction": "toward_benign" },
      { "class": "trusted_zone", "value": "untrusted", "llr": 0.03, "direction": "neutral" }
    ],
    "not_evaluated": [],
    "model_version": "2026-09-28.2",
    "note": "At this API's ~1.3% base rate ... URL-only estimate; the page was not fetched. ..."
  },
  "meta": { "analyzed_at": "...", "passive_only": true, "note": "...", "docs": "https://phishunt.io/api/", "license": "CC0-1.0" }
}
verdict is the one adjudicated answer: read it first. verdict_confidence and verdict_basis say how far to trust it and why. live_analysis.url_risk is a URL-shape heuristic on its own scale, not a verdict. action.reason says why the URL was or was not queued (for example brand_match_unknown_domain when queued, reserved_domain for example.com, ip_literal for an IP address). external_feeds looks up the apex in OpenPhish/PhishTank/TweetFeed (local cache, the target is never contacted). listed_scope tells you if the analyzed host itself is listed (host) or only another host of the same apex (apex). status/feeds_ok show how fresh that cache is. history.apex_prior_detections counts stored rows with verdict medium, high or critical. apex_candidates_seen counts all stored rows. scope is host when the apex is shared hosting or a trusted platform: then history covers the exact host only. live_analysis.why lists the top-5 score contributors and is consistent with url_risk_score. Full field reference in the OpenAPI spec (AnalyzeResponse schema).
A Unicode host is normalized, and query.host returns it in its xn-- form. A host that cannot exist in DNS (over 253 characters, a label over 63 characters, an empty label, invalid IDNA) returns 400. We log each query to recalibrate the probability model: host, path (no query string, no fragment, no credentials), scores, verdict and probability. We keep this log for 90 days.

Probability

The probability block sits beside verdict. It does not replace it. p_malicious is a calibrated estimate that the host is malicious, for a typical URL sent to this API. About 1.3% of those URLs are malicious (prior), so most hosts stay near that number until evidence moves them.

FieldMeaning
p_malicious, percentChance that the host is malicious, from 0 to 1 (3 decimals), and as a whole percent.
interval_8080% range for p_malicious. It shows the error of the estimate.
bandvery_unlikely (under 5%), unlikely (5-30%), uncertain (30-70%), likely (70-95%), very_likely (over 95%).
relative_riskp_malicious divided by prior. A value of 9.5 means 9.5 times the base rate.
coverageWhat the estimate uses: passive_url_only, known_record, active_no_render or active_rendered.
evidence, not_evaluatedEach class that moved the estimate, with its llr (log-likelihood ratio: positive means toward malicious), and each class that we could not check.
A low probability is not a statement that a site is safe. The same is true for the verdict no_evidence. Passive estimates stay low by design: we do not fetch the page, and phishing with no brand in the URL is invisible to URL analysis. For more evidence, use deep analysis (GET /api/v1/analyze/deep or the MCP tool analyze_url_deep). It fetches the page and, when the render lane is free, renders it in a browser: the response then has coverage active_rendered or active_no_render. It takes 15-45 seconds (50 at most). The REST endpoint needs a token, and the budget is 50 analyses per day. See the OpenAPI spec.

Campaigns

Groups of indicators that share infrastructure or content signals (same TLS certificate, IP, hosting, page content, etc.), computed by the same daily correlation job as related infrastructure. This is a shared-infrastructure grouping of public detections, not an attribution claim - clusters are labeled "possible campaign" or "suspected cluster" only, never an actor or group. Low-confidence ("weak") clusters are never surfaced through these endpoints or the /campaigns/ pages.

key is the stable identifier - use it for permalinks and caching. The numeric id is deprecated: it changes on every daily rebuild (versioned per daily rebuild), so a bookmarked /api/v1/campaigns/41 link still resolves (the sidecar remembers the mapping for 180 days), but only key is guaranteed to keep naming the same campaign tomorrow. It's kept in responses only for backward compatibility - use key.
data_status (on the list endpoint and on GET /api/v1/campaigns/{key}) is ok, stale, or missing - it reports whether the correlation sidecar rebuilt on schedule, so a client can tell "genuinely zero live campaigns" apart from "the nightly rebuild didn't run". GET /api/v1/campaigns/{key} also carries state: live for a currently-live cluster, or archived for a key that is no longer live but whose history is still retained (200, not 404 - see below).
confidence is one of two labels, thresholded off the underlying confidence_score (0-1, only exposed on GET /api/v1/campaigns/{key}): >= 0.70 is "possible campaign", >= 0.40 is "suspected cluster". Below 0.40 ("weak") is never surfaced by this API or the /campaigns/ pages at all.
total (list endpoint only, distinct from count) is how many clusters match brand/status/min_size before limit/offset are applied - page through everything by incrementing offset until offset + count >= total.
GET /api/v1/campaigns Try it

List possible campaigns / suspected clusters.

Query parameters

ParameterTypeDescription
limit integer Number of results to return. Default 50, max 200.
offset integer Number of results to skip for pagination. Default 0, max 10000.
brand string Filter to clusters with at least one member targeting this brand slug.
status string active (at least one currently-active member) or all. Default all.
min_size integer Only return clusters with at least this many members. Default 0.

Example request

curl "https://phishunt.io/api/v1/campaigns?status=active&limit=10"

Example response

{
  "count": 1,
  "total": 1,
  "offset": 0,
  "limit": 50,
  "algorithm_version": "corr-1.2.0",
  "generated_at": "2026-07-18T06:10:00Z",
  "results": [
    {
      "id": 41,
      "key": "0c1b79ab9b24",
      "size": 9,
      "active_count": 6,
      "brands": ["coinbase", "kraken"],
      "confidence": "suspected cluster",
      "first_seen": "2026-07-10T08:00:00Z",
      "last_activity": "2026-07-17T10:00:00Z",
      "top_evidence": { "type": "cert_fingerprint_exact", "coverage": "7/9 members" },
      "url": "https://phishunt.io/campaigns/0c1b79ab9b24/"
    }
  ]
}

0c1b79ab9b24 above is an example identifier, not necessarily a live campaign - use the "Try it" link on each endpoint below for a currently-live example.

GET /api/v1/campaigns/{key} Try it

Full campaign object: the list summary fields plus an evidence breakdown, a per-pair relationships drill-down (which specific member pairs are linked, by what evidence, capped at the 50 strongest), and every member indicator.

{key} accepts the stable key (preferred) or the legacy numeric id, resolved for the last 180 days. A key that is no longer live but whose history is still retained returns 200 with "state": "archived" and a thinner shape: key, label, confidence_score, size, first_tracked, last_seen, end_state, successors, history, members (no evidence_summary, and members is the last recorded snapshot only - no live enrichment). end_state is dissolved, merged, split, or unknown; successors lists the key(s) a merged/split lineage continues under, when resolvable. 404 with {"error": "Unknown campaign id", "list_url": "..."} only for an identifier the sidecar has never seen, a manually-suppressed key, or a "weak" campaign (never surfaced).

GET /api/v1/campaigns/{key}/export Try it

Same data as the endpoint above, served as a downloadable attachment.

ParameterTypeDescription
format string json, csv, or txt. Default json.
CSV columns: uuid,domain,url,company,status,first_seen,relationship_score,evidence_types (semicolon-joined signal ids). TXT is one member URL per line. Content-Disposition filenames use only the resolved campaign key, never a domain and never the raw identifier supplied by the caller. For an archived key, only format=json has an honest answer (the same "state": "archived" object as GET /api/v1/campaigns/{key}) - csv and txt stay 404, since both formats promise live enrichment (URL, status, relationship score) that an archived snapshot's last-known member set does not retain. Full field reference (evidence summary, history, members) in the OpenAPI spec (Campaign schema).
GET /campaigns/archive/ Try it

Server-rendered, paginated browser (?page=N, ~25 per page) over every archived campaign - not a JSON endpoint. noindex,follow.

Response fields

Each object in the results array (or feed entries) contains these fields, grouped by purpose.

Identity

FieldTypeDescription
urlstringFull URL of the suspicious site
domainstringDomain name including subdomains
companystringTargeted brand slug (e.g. amazon, paypal)
uuidstringUnique identifier for this entry (UUID v4)

Time

FieldTypeDescription
first_seendatetimeWhen the site was first detected (ISO 8601)
datedatetimeLast check timestamp (ISO 8601)

Hosting

FieldTypeDescription
ipstringResolved IPv4 address
countrystringHosting country name
asnstringAutonomous System Number (e.g. 13335)
orgstringHosting organization
certstringTLS certificate issuer

Detection sources

These five flags are positive assertions only. false means the source did not flag the URL when we checked, or was never asked (timeout or quota); it is not a clean verdict.

FieldTypeDescription
malicious_googlebooleantrue if flagged by Google Safe Browsing
malicious_openphishbooleantrue if present in OpenPhish feed
malicious_phishtankbooleantrue if present in PhishTank
malicious_tweetfeedbooleantrue if present in TweetFeed
malicious_urlscanbooleantrue if flagged by urlscan.io

phishunt score

FieldTypeDescription
scoreintegerphishunt's own 0-100 detection score from its explainable 5-layer scoring engine.
verdictstringCalibrated verdict tier for score: critical, high, medium, low, or noise.
top_signalsarrayUp to 5 highest-weighted signals behind score, as {signal, label, points}, sorted by points descending.

score is a heuristic risk score, not a calibrated probability - like url_risk_score on Analyze URL, score here is not a probability of phishing. For a calibrated estimate, see Probability.

Pagination

Use offset and limit to paginate through results. The response includes both values so you can compute the next page.

total (distinct from count) is how many rows match company/since/contains/tier/the pivot filters before limit/offset are applied - page through everything by incrementing offset until offset + count >= total.
# Page 1
curl "https://phishunt.io/api/v1/domains?limit=100&offset=0"

# Page 2
curl "https://phishunt.io/api/v1/domains?limit=100&offset=100"

# Page 3
curl "https://phishunt.io/api/v1/domains?limit=100&offset=200"

When count is less than limit, you've reached the last page.

Status codes

CodeDescription
200 Success. Response body contains the requested data.
400 Bad request. Invalid parameter value (e.g. malformed since date).
429 Rate limited. Back off and retry.
403 Feeds, blocklists and /api/v1/* skip Cloudflare's Browser Integrity Check, so any User-Agent works there. HTML pages still reject a bare Python-urllib or libwww-perl User-Agent - set a descriptive User-Agent anyway.

Notes

CORS - All API responses include Access-Control-Allow-Origin: *, so you can call the API from browser applications.
Terms - Data is provided on a best-effort basis. False positives may occur. See Terms of Service.
IP geolocation data powered by ipinfo.io.