NEW: Group Profiler — instant APT intel lookup. Try it →

API reference

ThreatFilter exposes a small JSON API at https://api.threatfilter.dev. Every endpoint is unauthenticated, free, CORS-enabled, and cached for a few minutes at the Cloudflare edge. Rate-limit: be reasonable — there is no key and no per-IP enforcement today.

Tags returned in vendors, severity_label, exploitation_status, and sectors come from the upstream feeds. The dedicated ML classifier is currently unfunded, so most items ship with vendors: [] and these are filled in heuristically in the browser (and at /feed.xml) using the keyword catalog in src/lib/classify.ts.

GET /health

Health check

Lightweight liveness probe. Returns 200 with a constant payload — useful for uptime monitors.

Example request

curl https://api.threatfilter.dev/health

Example response

{
  "status": "ok",
  "service": "threatfilter-api",
  "ts": "2026-07-18T11:25:24.976Z"
}

GET /sources

Source catalog + last fetch status

List every ingested feed source with its kind (rss/json/atom), category, last fetched_at, and last status.

Example request

curl https://api.threatfilter.dev/sources

Example response

{
  "sources": [
    {
      "id": "cisa-kev",
      "name": "CISA Known Exploited Vulnerabilities",
      "kind": "json",
      "category": "government",
      "enabled": true,
      "last_fetched_at": "2026-05-20T11:02:14Z",
      "last_status": "ok"
    },
    {
      "id": "nvd-recent",
      "name": "NVD Recent CVEs",
      "kind": "json",
      "category": "government",
      "enabled": true,
      "last_fetched_at": "2026-05-20T11:00:08Z",
      "last_status": "ok"
    }
  ]
}

GET /freshness

Most-recent ingest timestamp

Single timestamp of the most recently fetched item across all sources. Drives the freshness dot in the header.

Example request

curl https://api.threatfilter.dev/freshness

Example response

{
  "latest": "2026-07-18T11:02:29.282Z",
  "ts": "2026-07-18T11:24:56.371Z",
  "per_source": {
    "cisa-kev": {
      "name": "CISA Known Exploited Vulnerabilities",
      "last_fetched_at": "2026-07-18T11:01:19.819Z",
      "last_status": "not_modified"
    }
  }
}

GET /vendors

Vendor catalog + counts

Static vendor catalog (slug, label, category) annotated with the count of items currently linked to each vendor (when the materialized vendor_counts table is populated).

Example request

curl https://api.threatfilter.dev/vendors

Example response

{
  "vendors": [
    {
      "slug": "microsoft",
      "count": 1448
    },
    {
      "slug": "google",
      "count": 661
    },
    {
      "slug": "oracle",
      "count": 266
    }
  ]
}

GET /items

Paginated advisory feed

Keyset-paginated advisory feed ordered by published_at DESC, id DESC. Cursor is returned in next_cursor — feed it back as ?cursor= to get the next page. All filter params combine with AND.

Parameters

Name Type Description Example
limit int Page size (default 50, max 250). 50
cursor string Opaque cursor from a previous response's next_cursor field.
vendors csv Comma-separated vendor slugs. Items matching any of these vendors are returned (OR). cisco,fortinet
severity csv One or more of critical, high, medium, low, info (OR). critical,high
exploitation csv exploited | poc_available | unproven (OR). exploited
geo csv ISO-3166 alpha-2 country codes (OR). US,EU
q string Full-text search query (SQLite FTS5). Plain whitespace-separated terms; AND-combined. cve-2024

Example request

curl https://api.threatfilter.dev/items?limit=2&severity=critical

Example response

{
  "items": [
    {
      "id": 482301,
      "source_id": "cisa-kev",
      "title": "CISA adds CVE-2026-12345 to Known Exploited Vulnerabilities",
      "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
      "published_at": "2026-05-20T10:45:00Z",
      "fetched_at": "2026-05-20T11:02:14Z",
      "raw_summary": "A critical authentication bypass in a popular VPN appliance is being actively exploited in the wild…",
      "severity_cvss": 9.8,
      "severity_label": "critical",
      "vendors": [],
      "products": [],
      "target_countries": [],
      "exploitation_status": null
    }
  ],
  "next_cursor": "eyJ0cyI6IjIwMjYtMDUtMjBUMTA6NDU6MDBaIiwiaWQiOjQ4MjMwMX0="
}

POST /digest/subscribe

Subscribe to the daily email digest

Body is JSON: { email, filter_state? }. Triggers a double-opt-in email — subscription is only active once /digest/verify is hit with the token. The API never returns email addresses or PII.

Parameters

Name Type Description Example
email (required) string (body) Recipient address. Validated with a permissive RFC-5322 regex. [email protected]
filter_state object (body) Saved filter selection. ARRAY-valued keys only: vendors, severity, sectors, geos (alias geo, upper-cased). Unknown keys are dropped; each list is capped at 400 entries. CSV strings are NOT accepted and yield an empty filter.

Example request

curl https://api.threatfilter.dev/digest/subscribe

Example response

{
  "status": "pending_verification",
  "email_status": "sent"
}

GET /digest/unsubscribe

Check an unsubscribe token (read-only)

Reports whether an unsubscribe token is recognised. This endpoint is deliberately NON-DESTRUCTIVE: mail scanners and link prefetchers follow GETs, so opting out on a GET would remove subscribers who never clicked. Use POST to actually unsubscribe.

Parameters

Name Type Description Example
token (required) string Opaque per-subscriber unsubscribe token from the digest footer.

Example request

curl https://api.threatfilter.dev/digest/unsubscribe?token=…

Example response

{
  "status": "confirm_required"
}

POST /digest/unsubscribe

Unsubscribe from the daily digest

Performs the opt-out and deletes the stored email address. Token may be given as a query param or in a JSON body. The site's /digest/unsubscribe page calls this after an explicit confirmation click.

Parameters

Name Type Description Example
token (required) string (query or body) Opaque per-subscriber unsubscribe token from the digest footer.

Example request

curl https://api.threatfilter.dev/digest/unsubscribe

Example response

{
  "status": "unsubscribed"
}