Free Public CVE API

JSON + NDJSON access to the same vulnerability intelligence behind /pulse: NVD CVSS, EchelonGraph synthesised score, CISA KEV flag, FIRST EPSS percentile, GHSA links, and vendor advisory cross-walks (Microsoft, Red Hat, Cisco, AWS, GCP, GitLab).

No API key required. Two limits apply to the endpoints on this page, and the tighter one is the one you meet. Our WAF counts every request you make as one caller (defined below) to the API in a 60-second window that starts with your first request — all paths together, across every serving instance — and refuses a request once that count passes the ceiling for the path it is on:

  • 12,000 requests per minute on /api/v1/public/cves and every path under it (200 per second sustained)
  • 6,000 requests per minute on /api/v1/public/vendor-advisories, /api/v1/public/cwes, /api/v1/public/ecosystems, /api/v1/public/vendors, /api/v1/public/shadow-ai-radar and /api/v1/public/kev/recent (100 per second sustained)
  • 600 requests per minute on any other /api/v1/public path (10 per second sustained)

Because the count is shared, a heavy run on /api/v1/public/cves also uses up the lower ceilings: after 6,000 requests in one window, /api/v1/public/vendor-advisories refuses you too.

On top of that ceiling, 500 requests per second per caller is a burst cap, counted by each serving instance, so it bounds a burst rather than your sustained rate. For both limits, one caller is one client address as our platform records it on arrival — an IPv4 address, or for IPv6 the /64 network the address is in — and not a value you send in a header, so other callers' traffic does not spend your allowance. Clients behind one shared IPv4 address or one IPv6 /64 (an office NAT, a proxy relaying for others, a hosting provider that puts several customers in one /64) share that one allowance, and a host rotating its IPv6 privacy addresses stays one caller. One exception: a request that reaches the API from inside Google Cloud without a public address (for example, from a VM with no external IP, using Private Google Access) arrives with no client address our platform records, so every such caller is counted as the same caller and all of them share one per-second allowance. Cloud NAT does not change this; to get an allowance of your own, give the workload an external IP address.

What you see: an admitted response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Window: 1s and X-RateLimit-Reset for the per-second limit only, and X-RateLimit-Remaining there describes the instance that answered. The WAF's per-minute count is not shown until it refuses you. Over either limit you get a 429 with Retry-After in seconds. A WAF refusal carries X-RateLimit-Window: 60s, X-RateLimit-Limit set to that path's per-minute ceiling, and a JSON body whose code is RATE_LIMIT_EXCEEDED; a refusal by the per-second limit carries X-RateLimit-Window: 1s. Need higher throughput? Get in touch.

Quickstart

Every endpoint is GET-only and returns JSON. Try this Log4Shell lookup:

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228 | jq .

Endpoints

GET/api/v1/public/cves

The main feed — paginated JSON list, newest first. Params: limit (default 50, max 200), offset, search, severity, min_cvss, kev_only, year, sort (published | epss | severity | cvss_v3_score), skip_count. reverse=true reads the default listing (sort=published or no sort, with no status other than active, and no severity, min_cvss, since, until, year, kev_only or search filter) from its far end — offset and limit count back from the last row, and the page comes back in the usual order with "reverse": true in the response — so a page near the end costs about what a page near the start does; with any other sort or filter, or with the before_published / before_cve_id cursor, it is refused with a 400. Every list response carries reverse_available, true exactly when the same request with reverse=true would be served, so a pager can ask the server instead of copying that rule; take the page count from a counted response (not one sent with skip_count). kev_only=true restricts to CVEs CISA lists as known-exploited; year filters on UTC publication year. There is no vendor filter on this endpoint — vendor lives in CPE match data, so use /api/v1/public/cves/match?vendor=&product= for that; passing vendor here returns 400 rather than silently ignoring it. severity filters on NVD's CVSS v3 label by default; pass severity_basis=eg to filter on the EchelonGraph severity instead — that is the basis the /pulse counts use, and it is the only way to reach the ~72,000 CVEs NVD never rated under v3. min_cvss matches the highest CVSS we hold (v3.1, v4.0 or v2.0). Response: cves for this page, plus total and two booleans about it, total_is_lower_bound and total_counted. total is how many CVEs match the filter across every page, not how many this page holds. Both booleans are always present, in both polarities — branch on their value, never on whether the field exists. total_is_lower_bound=true means total is a proven floor, not a count: at least that many match, and the exact figure could not be computed inside the query's execution budget. That happens on a free-text search matching a very large share of the corpus; a narrower search gets an exact total. total_counted=false means no count is in hand — because you sent skip_count=1 (for bulk paginators that never show a total), because you used the keyset cursor path (before_published + before_cve_id), or because the server could not count the matches inside the query's execution budget, not even to a floor — and total is 0 and means nothing. With skip_count or the cursor, page with the next_published / next_cve_id the response carries instead; when the budget was the cause the response is an ordinary offset page with its rows and no cursor, and a narrower search gets a total. search_relaxed=true means the rows are for a widened search, not the one you sent: the search term was a phrase — a quoted string, or a hyphenated compound such as cross-site scripting, which the parser reads as adjacent words — and its exact match could not finish inside the query's execution budget, so it was relaxed to a conjunction of the same words in any order; the rows, and total when total_counted is true, are that conjunction's. Always present, in both polarities; false means the search ran as sent. Rate limits: 12,000 requests per minute on /api/v1/public/cves and every path under it (200 per second sustained), counted by our WAF for each caller across all paths; on top of that, 500 requests/second per caller is a burst cap on each serving instance. For both limits one caller is one IPv4 address, or for IPv6 one /64 network, as our platform records it, never a header you send. Successful responses show only the per-second limit's X-RateLimit-* headers; the paragraph above this list says how both limits are counted and what a 429 from each carries.

curl "https://app.echelongraph.io/api/v1/public/cves?severity=HIGH&severity_basis=eg&min_cvss=7&limit=20"
GET/api/v1/public/cves/summary

Overall counts and the live NVD poller's health snapshot, updated continuously. Severity buckets (critical/high/medium/low/unscored) use the EchelonGraph severity — the band we publish — and always sum to total. NVD's own CVSS v3 classification is returned alongside as nvd_critical/nvd_high/nvd_medium/nvd_low/nvd_none, so you can compare the two bases.

curl https://app.echelongraph.io/api/v1/public/cves/summary
GET/api/v1/public/cves/{id}

Single CVE record — the merged view: NVD CVSS, EchelonGraph synthesised score, KEV/EPSS signals, GHSA references, vendor advisory cross-links. For the ~72,000 pre-2016 CVEs that NVD only ever rated under CVSS v2, cvss_v3_score is 0 and the real rating is in cvss_v2_score / cvss_v2_severity / cvss_v2_vector — v2 has three bands only (LOW 0.0-3.9 / MEDIUM 4.0-6.9 / HIGH 7.0-10.0, no CRITICAL) and is NOT comparable to a v3 score. exploit_poc_available is derived from verified exploit records (Exploit-DB, Metasploit, GitHub PoC, Nuclei), not self-reported.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228
GET/api/v1/public/cves/{id}/related

Three lists of related CVEs, up to 10 each. same_product: CVEs sharing an affected package, most specific shared package first. same_vendor: CVEs sharing a vendor advisory, most shared advisories first. same_cwe: CVEs sharing a CWE, nearest in publish date first. EchelonGraph score only breaks ties. basis says why each list is what it is: ok, no_data (this CVE has no rows for that relation), no_siblings (it has rows, but no other CVE shares them), no_specific_key (same_product only: every package it shares is carried by too many CVEs to count as evidence), capped (same_vendor and same_cwe: siblings exist, but only through keys beyond the first 50 this lookup reads), or unknown (the reason was not established in time; the list itself is still correct). Powers the sidebar on every /pulse/{id} page.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228/related
GET/api/v1/public/cves/{id}/references

Per-reference enrichment for one CVE — vendor advisory cross-walks, patch URLs, exploit POC indicators where surfaced.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228/references
GET/api/v1/public/cves/{id}/enrichment

One CVE's enrichment sections in one answer, among them vendor advisories, patches, fixed versions, affected packages, CWEs, public exploit references and the enrichment timeline. failed_sections names any section that could not be read, served empty. Each affected_packages row carries fixed_branches, every affected range of the package with its fix ({introduced, fixed, last_affected, source, advisory_id}: fixed null where no fix is on record, last_affected null where the range ends at a fix, advisory_id the OSV record that published the range, in that record's order; fixed_branches null where the package's ranges were never loaded, [] where no version range is on record, which is not a finding that no fix exists). fixed_version on that row is one range's fix, kept for compatibility: it is not the fix for every affected version (CVE-2021-44228's log4j-core: 2.12.2, while 2.14.1 is in the range fixed in 2.15.0), so pick the range that holds your version.

curl https://app.echelongraph.io/api/v1/public/cves/CVE-2021-44228/enrichment
GET/api/v1/public/cves/trends

Weekly + monthly volume + week-over-week delta + severity distribution. Backs the dashboard cards on /pulse.

curl https://app.echelongraph.io/api/v1/public/cves/trends
GET/api/v1/public/cves/export.ndjson

Streaming NDJSON bulk export — one CVE per line. Filters: year, severity, kev_only, min_cvss. min_cvss matches the HIGHEST CVSS we hold for a CVE (v3.1, v4.0 or v2.0), so a v2-only 10.0 is included where a v3-only filter would have missed it. Hard cap 50,000 rows per call (iterate via the year param for the full dataset).

curl "https://app.echelongraph.io/api/v1/public/cves/export.ndjson?year=2024&severity=CRITICAL&kev_only=true" | jq -c .
GET/api/v1/public/vendor-advisories

Vendor-disclosed security advisories (Microsoft MSRC, Red Hat RHSA, GitHub GHSA, Cisco PSIRT, AWS, GCP, GitLab). Many appear here before NVD assigns a CVE-ID.

curl https://app.echelongraph.io/api/v1/public/vendor-advisories?has_cve=true&limit=50
GET/api/v1/public/vendor-advisories/{vendor}/{advisory_id}

Single vendor-advisory detail — title, description, CVSS, affected products, remediation, references, linked CVE IDs.

curl https://app.echelongraph.io/api/v1/public/vendor-advisories/github/GHSA-99gv-2m7h-3hh9

Bulk NDJSON Export

For researchers + security teams who want offline analysis. The export streams one CVE per line as JSON (NDJSON / JSON Lines), so you can pipe it through jq -c . or process incrementally without buffering the whole dataset.

# All KEV-listed CVEs published in 2024
curl "https://app.echelongraph.io/api/v1/public/cves/export.ndjson?year=2024&kev_only=true" \
  | jq -c '{cve_id, severity, echelongraph_score, kev_added: .kev_added_date}'

# Full 2023 CRITICAL dataset
curl "https://app.echelongraph.io/api/v1/public/cves/export.ndjson?year=2023&severity=CRITICAL" \
  > cves-2023-critical.ndjson

Hard cap: 50,000 rows per call. Iterate via the year param for the full dataset. The endpoint streams from a DB cursor, so memory pressure stays flat regardless of result size.

Scoring fields

Every CVE we have assessed carries two independent EchelonGraph numbers: EG Score answers “how severe?” and EG Risk answers “how urgently should I act?”. When score_assessed is false, neither number is on the wire — no placeholder 0 stands in for an assessment we have not made. Each score also ships its own machine-readable breakdown in score_factors(including a plain-English score_factors.eg_risk.about / .summary), so the response is self-documenting.

FieldMeaning
echelongraph_score0–10 severity (CVSS-comparable): NVD/GHSA CVSS with honest KEV/EPSS escalation and reject suppression. READ score_assessed FIRST — when that is false this key is ABSENT: no rating exists, and no placeholder 0 is published in its place.
score_assessedALWAYS PRESENT. false = we could NOT assess this CVE; echelongraph_score, echelongraph_severity and echelongraph_risk are then ABSENT — not 0, not NONE — and their absence is NOT a claim that the CVE is harmless. Never threshold, sort or alert on any of them without checking this. Any EchelonGraph number that IS present came from a real assessment, including a genuine 0.
score_unassessed_reasonWhy there is no assessment. "no_signal" = UNDECIDED, no source has published severity data yet (revisited automatically when one does). "rejected" = DECIDED, the record was withdrawn by its CNA/NVD. Absent when assessed.
vendor_severityCRITICAL / HIGH / MEDIUM / LOW stated by the VENDOR in its own advisory prose, for CNAs that publish no CVSS vector (e.g. Chromium). This is the vendor's taxonomy, NOT a CVSS band, and the two do not line up: a Chromium "Low" carries a median NVD CVSS of 5.4 (MEDIUM). Never compare it to a CVSS band or across vendors.
vendor_severity_sourceWhich vendor taxonomy vendor_severity came from, e.g. "chromium". Bands are only comparable within one source.
vendor_severity_evidenceThe exact advisory substring the band was parsed from, e.g. "(Chromium security severity: Low)" — so any published number is traceable to the text it came from.
echelongraph_severityCRITICAL / HIGH / MEDIUM / LOW — the band derived from echelongraph_score. ABSENT when score_assessed is false; the placeholder "NONE" it used to carry there is no longer published, because a word that reads as a rating is the same false claim as a 0. Measured 2026-09-20 over 2,383 assessed rows: the band is only ever one of the four above.
echelongraph_risk0–100 PRIORITY: fuses severity (45%) + exploitation — KEV/eg_kev/EPSS (40%) + automatability — SSVC (15%). Separates equal-severity CVEs so the most dangerous surface first. Higher = act sooner. ABSENT when score_assessed is false: an un-assessed CVE has nothing to fuse, so no priority is published in its place.
echelongraph_ssvc_decisionCISA SSVC decision (Act / Attend / Track* / Track) at Mission & Well-being = MEDIUM, from CISA's published tree (SSVC Guide, Nov 2022, Table 9) with the SSVC decision points (+ KEV/EPSS/CVSS fallback). SSVC is stakeholder-specific, so this is one column of the answer, not the answer for every reader: score_factors.ssvc.by_mission gives the decision at low, medium and high mission impact, and score_factors.ssvc names where each input came from (an input no source supplied is "unknown", and every decision it allows is listed).
score_confidenceHIGH / MEDIUM / LOW / NONE — how much corroborating enrichment backs the score. A CONFIDENCE, not a severity: the words overlap with the severity bands, so render it labelled ("medium confidence"), never where a severity goes.
score_factorsFull evidence blob: rule_triggered, sources_contributing, cvss/epss/kev/eg_kev inputs, eg_risk { risk, severity_norm, exploitation, automatable, weights, about, summary }, and (algorithm v3.2+) ssvc { tree, exploitation, automatable, technical_impact — each { value, source } — by_mission { low, medium, high }, decision, decision_mission }.

Want to be notified when critical CVEs hit?

Subscribe to real-time or digest alerts covering NVD + vendor-disclosed advisories.

Subscribe via /pulse →

Notes

  • All endpoints are CORS-enabled for browser use.
  • Responses are versioned under /api/v1/. Breaking changes will land under /api/v2/ with a deprecation window.
  • Where two sources disagree on CVSS (CNA vs NVD analyst), the freshest modified timestamp wins. See Why is the EG score different from NVD?
  • Powered by direct feeds from MITRE cvelistV5 (fast), NVD API (deep), GHSA, CISA KEV, FIRST EPSS, and per-vendor advisory pollers.