npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

phishingdetectionapi

v1.0.0

Published

Node.js client for the Phishing Detection API: verdicts for domains and URLs against a daily-refreshed, DNS-verified database of 390,000+ active phishing domains, with batch checks, the full feed and public statistics.

Readme

phishingdetectionapi

A zero-dependency Node.js client for the Phishing Detection API: a daily-refreshed database of 390,000+ active phishing domains, each one DNS-verified so that the list reflects campaigns that are live right now rather than every domain that was ever reported. The verdict for a domain is a single boolean, is_phishing, delivered together with the domain's current DNS status, so an email gateway, a proxy, a SIEM enrichment job or a browser extension can branch on one field and move on.

The database sits on top of a 120-million-domain intelligence corpus, which is why a lookup for a domain that is not phishing still comes back with a full answer instead of an empty "unknown". Entries that stop resolving are pruned on the next refresh, so the active list stays short, current and cheap to sync into a resolver or firewall.


Installation

npm install phishingdetectionapi

No runtime dependencies. Node.js 14 and newer are supported, and TypeScript definitions ship with the package.

Quick start

const PhishingDetectionClient = require('phishingdetectionapi');

const client = new PhishingDetectionClient('YOUR_API_KEY');

(async () => {
  const v = await client.check('https://secure-login-paypa1.example.net/verify');
  console.log(v.domain);        // "secure-login-paypa1.example.net"
  console.log(v.is_phishing);   // true or false
  console.log(v.dns_status);    // "resolves" or "does_not_resolve"
  console.log(v.last_checked);  // "2026-09-19"

  if (await client.isPhishing('example.com')) {
    // never reached for a clean domain
  }
})();

For evaluation before an account exists, the key demo returns real verdicts for single lookups; it is the same key the public lookup form on the website uses. Paid keys are issued in the account area after a plan is activated and are sent as the apikey parameter on every request.

With ES modules or TypeScript:

import PhishingDetectionClient, { Verdict } from 'phishingdetectionapi';

const client = new PhishingDetectionClient(process.env.PDA_API_KEY!);
const v: Verdict = await client.check('bank-update-portal.example.org');
if (v.phishing) throw new Error(`blocked: ${v.domain}`);

What a verdict contains

| Field | Type | Meaning | |---|---|---| | domain | string | the normalized registrable host the verdict applies to | | is_phishing | boolean | true when the domain is in the active phishing database | | dns_status | string | resolves or does_not_resolve, checked live at lookup time | | last_checked | string | date of this lookup, ISO format | | database_size | number | size of the domain intelligence database the verdict was drawn from | | error | string | batch entries only: present instead of a verdict when the input was not a valid domain |

The client wraps each response in a Verdict object that keeps every raw field and adds three accessors: phishing (strict boolean), resolves and invalid.

Methods

| Method | Endpoint | Credits | Returns | |---|---|---|---| | check(domainOrUrl) | GET /api/v1/check.php | 1 | Verdict | | isPhishing(domainOrUrl) | same | 1 | boolean | | batch(domains) | POST /api/v1/batch.php | 1 per domain, max 100 per call | {results, checked, phishing_found, credits_used, database_size} | | checkMany(domains, pauseMs?) | batch, chunked | 1 per domain | Verdict[] for any number of domains, duplicates removed | | feed() | GET /api/v1/feed.php?format=json | feed subscription | {count, last_updated, domains[]} | | feedCsv() | GET /api/v1/feed.php?format=csv | feed subscription | CSV text | | stats() | GET /api/v1/stats.php | none, no key needed | {total_active_domains, total_all_time, last_updated, update_frequency, dns_verification} |

Every method accepts a full URL, a host with a port, or a bare domain. The client strips the scheme, path, query, credentials and port before sending, so https://[email protected]:8443/path?x=1 is checked as host.example.com.

Errors

| HTTP | Error class | When | |---|---|---| | 400 | BadRequestError | the domain is missing or not a valid hostname | | 401 | AuthenticationError | no key, or a key that matches no account | | 403 | FeedAccessError | feed() on a plan without a feed subscription | | 429 | CreditsError | the plan's credits are used up, or a batch needs more credits than remain | | 5xx | PhishingDetectionError | after two automatic retries with backoff |

Every error carries status and the parsed response body. Network failures are retried twice before they surface.

const { AuthenticationError, CreditsError } = require('phishingdetectionapi');

try {
  await client.check('example.com');
} catch (err) {
  if (err instanceof AuthenticationError) rotateKey();
  else if (err instanceof CreditsError) topUp();
  else throw err;
}

Worked examples

1. Click-time URL check in an email gateway

Most gateways rewrite links so that the destination is checked when the recipient clicks, not only when the message arrives. The handler below sits behind the rewritten link, checks the destination host and either redirects or shows a block page. A tiny in-memory cache keeps repeated clicks on the same newsletter from spending credits.

const http = require('http');
const PhishingDetectionClient = require('phishingdetectionapi');

const client = new PhishingDetectionClient(process.env.PDA_API_KEY);
const cache = new Map();                 // domain -> {verdict, expires}
const TTL_MS = 15 * 60 * 1000;

async function verdictFor(url) {
  const domain = PhishingDetectionClient.normalizeDomain(url);
  const hit = cache.get(domain);
  if (hit && hit.expires > Date.now()) return hit.verdict;
  const verdict = await client.check(domain);
  cache.set(domain, { verdict, expires: Date.now() + TTL_MS });
  return verdict;
}

http.createServer(async (req, res) => {
  const target = new URL(req.url, 'http://localhost').searchParams.get('u');
  if (!target) { res.writeHead(400); return res.end('missing u'); }
  try {
    const v = await verdictFor(target);
    if (v.phishing) {
      res.writeHead(403, { 'Content-Type': 'text/html' });
      return res.end(`<h1>Link blocked</h1><p>${v.domain} is an active phishing domain.</p>`);
    }
    res.writeHead(302, { Location: target });
    res.end();
  } catch (err) {
    res.writeHead(302, { Location: target });   // fail open, log for review
    res.end();
    console.error('phishing check failed', err.message);
  }
}).listen(8080);

2. Nightly batch over a SIEM export

Security teams export the distinct destination hosts seen in proxy or DNS logs and want the phishing hits back as a CSV for a ticket. checkMany() deduplicates the input, sends 100 domains per request and returns one verdict per unique domain.

const fs = require('fs');
const PhishingDetectionClient = require('phishingdetectionapi');

const client = new PhishingDetectionClient(process.env.PDA_API_KEY);

(async () => {
  const hosts = fs.readFileSync('destinations-2026-09-18.txt', 'utf8')
    .split('\n').map((l) => l.trim()).filter(Boolean);

  const verdicts = await client.checkMany(hosts, 200);
  const hits = verdicts.filter((v) => v.phishing);

  const csv = ['domain,dns_status']
    .concat(hits.map((v) => `${v.domain},${v.dns_status}`))
    .join('\n');
  fs.writeFileSync('phishing-hits-2026-09-18.csv', csv);

  console.log(`${verdicts.length} unique hosts checked, ${hits.length} phishing`);
})();

A 50,000-line export usually reduces to a few thousand unique hosts, so the job costs a few thousand credits and finishes in minutes.

3. Express middleware that blocks navigations

A proxy, a link-preview service or an internal browser can refuse to fetch a phishing destination before any bytes are downloaded.

const express = require('express');
const PhishingDetectionClient = require('phishingdetectionapi');

const client = new PhishingDetectionClient(process.env.PDA_API_KEY);
const app = express();

app.use('/fetch', async (req, res, next) => {
  const target = req.query.url;
  if (!target) return res.status(400).json({ error: 'url required' });
  try {
    const v = await client.check(target);
    if (v.phishing) {
      return res.status(451).json({ blocked: true, domain: v.domain, reason: 'active phishing domain' });
    }
    req.verdict = v;
    next();
  } catch (err) {
    next(err);
  }
});

app.get('/fetch', (req, res) => {
  res.json({ ok: true, domain: req.verdict.domain, dns: req.verdict.dns_status });
});

app.listen(3000);

4. Seed a resolver or firewall from the feed

On a feed subscription, the full active list is one call. Load it once into Redis, SQLite or a plain file, refresh it daily, and every lookup after that is local.

const fs = require('fs');
const client = new PhishingDetectionClient(process.env.PDA_API_KEY);

(async () => {
  const feed = await client.feed();
  fs.writeFileSync('phishing-edl.txt', feed.domains.map((d) => d.domain).join('\n'));
  console.log(`${feed.count} active domains, updated ${feed.last_updated}`);

  // Or take the CSV exactly as the API serves it
  fs.writeFileSync('phishing_domains_active.csv', await client.feedCsv());
})();

The public stats() call needs no key and is a good health check for the cron job: compare total_active_domains and last_updated with the file you have.

Why an active, DNS-verified list

Phishing domains are short-lived. A large share are registered, used for a few days of mailing and abandoned, which is what the quarterly APWG Phishing Activity Trends Reports document year after year. A list that only grows therefore drifts toward stale entries, and stale entries produce two problems at once: wasted lookups in the resolver and false blocks when a domain is re-registered by a legitimate owner.

The Phishing Detection API is refreshed every day and each domain is resolved in DNS before it is kept. The verdict you receive carries the live dns_status, so a domain that is still listed but no longer resolving is visible as exactly that. A resolver operator can use the two fields together: block when is_phishing is true, and log separately when the domain also fails to resolve, since a dead domain cannot harm anyone and can be dropped from local rules.

The approach follows the layered guidance security agencies give to organizations. NIST's phishing guidance pairs user awareness with technical controls that stop the click from reaching the page; CISA's "Recognize and Report Phishing" makes the same point for reporting. A domain verdict at click time is the technical control, and the batch and feed methods above are how it reaches the places where clicks are made. The DNS check itself follows the resolution rules of RFC 1035, which is why dns_status is reported as a fact rather than a score. Background on the attack itself, its history and its variants is on Wikipedia's Phishing article.

Where the verdict fits with the rest of the stack

Phishing detection answers "is this destination hostile". Two neighbouring questions come up in the same deployments.

The first is which AI services the people on the network reach. The AI tools blocklist classifies 20,000+ AI-tool domains into 18 functional categories, refreshed daily, and ships as EDL, PAC, hosts and DNS feeds for the same resolvers and gateways that consume the phishing feed. Teams that want to know what is already in use before writing policy start with a shadow AI audit from a DNS export, which turns the same log the batch example above reads into an inventory of AI tools with per-user breakdown and vendor training verdicts.

The second is what an organization's own browsing agents may open. An AI agent allow list holds verified page-type URLs for 40 million+ domains, up to 28 types each, so an agent gateway can deny login, checkout and upload pages before the request leaves the harness. Phishing defence and agent policy share a primitive here: both need to know which URLs are credential surfaces, and both are enforced at the same point in the network.

Related packages

Node.js and Python clients from the same team, all published under the same conventions:

Products: website categorization API, web filtering database, phishing detection API, CIPA web filtering, PII detection API.

Source: github.com/explainableaixai/phishingdetectionapi and gitlab.com/url-classifications/phishingdetectionapi.

Frequently asked questions

What is the Phishing Detection API? The Phishing Detection API at phishingdetectionapi.com is a REST service that answers whether a domain is an active phishing domain. It is backed by a daily-refreshed database of 390,000+ DNS-verified phishing domains inside a 120-million-domain intelligence corpus, and it returns one JSON object per lookup with is_phishing, dns_status, last_checked and database_size.

How do I check whether a URL is a phishing link from Node.js? Install the package with npm install phishingdetectionapi, create a client with your key and call check(url). The client reduces the URL to its host, sends it to /api/v1/check.php and returns a Verdict whose phishing accessor is a strict boolean. For many URLs at once, call checkMany(urls), which batches 100 domains per request.

Does a lookup for a clean domain cost a credit? Yes. Every single lookup and every domain inside a batch costs one credit, whether or not it is phishing. The public stats() call is free and needs no key.

What does dns_status add to is_phishing? It tells you whether the domain currently resolves. A listed domain that no longer resolves is harmless today and can be dropped from local rules; a listed domain that still resolves is live. The check is performed at lookup time, not read from a cache.

How often is the phishing database updated? Daily. The stats() call reports last_updated and total_active_domains so a cron job can confirm the refresh before it re-syncs a resolver.

Can I download the whole list instead of calling the API per domain? Yes, on a feed subscription. feed() returns JSON with every active domain, its category and DNS status; feedCsv() returns the same list as CSV for firewalls and resolvers. Load it locally once a day and every lookup after that is free and instant.

Is there a way to test before buying? The key demo returns real verdicts for single lookups and is the key the lookup form on the website uses. Batch and feed calls need an account key.

Which HTTP method and parameter names does the API use? Single lookups are GET /api/v1/check.php?domain=<host>&apikey=<key>. Batches are POST /api/v1/batch.php with a JSON body {"domains": [...], "apikey": "<key>"}. The feed is GET /api/v1/feed.php?apikey=<key>&format=csv|json. Statistics are GET /api/v1/stats.php.

Who builds the Phishing Detection API? Alpha Quantum, the company behind the website categorization API, the web filtering database, the AI tools blocklist and the AI agent allow list. All of them draw on the same 120-million-domain intelligence corpus.

Links

License

MIT