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.
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
Want to be notified when critical CVEs hit?
Subscribe to real-time or digest alerts covering NVD + vendor-disclosed advisories.
Subscribe via /pulse →