CVE Tracker API
A public, read-only JSON API over every public CVE: the CVE List, CVSS from the assigner, CISA and NVD, CISA KEV, FIRST EPSS and the packages named by the GitHub Advisory Database. It is what trackcve.app itself runs on. No account and no key are needed.
https://api.trackcve.app/v1Machine-readable description: /v1/openapi.json (OpenAPI 3.1), for generating a client in any language.
Quick start
# One CVE
curl -s https://api.trackcve.app/v1/cves/CVE-2021-44228
# Critical, known exploited CVEs of the last 7 days, newest first
curl -s 'https://api.trackcve.app/v1/cves?range=7d&severity=CRITICAL&kev=true'
# Everything published today as CSV
curl -s -o today.csv 'https://api.trackcve.app/v1/export?range=today&format=csv'
# CVEs with a reviewed advisory for an npm package, highest EPSS first
curl -s 'https://api.trackcve.app/v1/cves?ecosystem=npm&sort=epss&dir=desc&pageSize=20'Conventions
- Methods: only
GET(andHEAD,OPTIONS). There is nothing to write. - Format: JSON (
application/json), exceptGET /v1/export?format=csv. - Times are ISO 8601 in UTC with milliseconds (
2026-11-03T14:18:29.656Z). Days areYYYY-MM-DD, also UTC. - Missing values are
null, never left out. Lists are[]when empty. - CVE ids are
CVE-YYYY-NNNN(four or more digits). In a path they are matched in any case:cve-2021-44228works. - Severity is one of
CRITICAL,HIGH,MEDIUM,LOW,NONE,UNKNOWN(not scored yet). It andscoreare the best CVSS base score the record has: the assigner's, then CISA's (ADP), then NVD's. - EPSS is a probability from 0 to 1 that the CVE is exploited in the next 30 days.
- Lenient query parsing: a filter parameter with a value it does not know (
severity=SEVERE,range=2w,pageSize=abc) is ignored and its default used, not rejected, so a typo widens the result instead of failing. A search (q) shorter than 3 characters is ignored as well. Scripts should sendstrict=true: then any of this is a400 VALIDATION_FAILEDthat names the problem. - Versioning: everything is under
/v1. Fields may be added withinv1; none is removed or changes meaning. Ignore fields you do not know. See Versioning and changelog. - Request id: every response has an
X-Request-Idheader (also in error bodies). You may send your own ([A-Za-z0-9._-], up to 128 characters) to match your logs with ours. Include it when reporting a problem.
Rate limit and caching
Every address may make 120 requests a minute to /v1/* (/v1/health does not count; IPv6 addresses are counted by their /64). Each response says where you stand:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the window |
RateLimit-Remaining | Requests left in the current window |
RateLimit-Reset | Seconds until the window starts again |
Retry-After | On 429 and 503 only: seconds to wait |
Past the limit the API answers 429 RATE_LIMITED. Wait for Retry-After seconds, then continue.
Answers carry Cache-Control. The data changes every few minutes, so please cache on your side and honor these times:
| Endpoints | Fresh for |
|---|---|
/summary, /cves, /timeline, /export | 1 minute |
/cves/{id}, /vendors, /products, /ecosystems, /cwes, /assigners, /stats | 5 minutes |
/cves/{id}/epss | 1 hour |
/health | not cached |
If you need much more than the limit allows (a mirror, bulk analysis), use /v1/export for whole filters at once, or open an issue and say what you are building.
Errors
Every error has the same body, with a stable code. The message is English for developers and may change; branch on code.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Not a CVE id (CVE-YYYY-NNNN)",
"requestId": "959f7882-7d29-4623-83be-3fee699c5fff"
}
}| Status | code | When | What to do |
|---|---|---|---|
| 400 | VALIDATION_FAILED | A malformed CVE id, an unknown format on /export, any filter problem with strict=true | Fix the request |
| 404 | NOT_FOUND | The CVE is not in the database, or there is no such route | Do not retry |
| 429 | RATE_LIMITED | Over the rate limit | Retry after Retry-After |
| 503 | UNAVAILABLE | The server is shedding load, a query ran out of time, or FIRST is down (EPSS history) | Retry after Retry-After (5 s) |
| 500 | INTERNAL | A bug on our side | Report it with the requestId |
A CVE id that is well formed but unknown (reserved and never published, or newer than the last sync, at most about 10 minutes) is a 404.
Filter parameters
GET /v1/cves, GET /v1/timeline and GET /v1/export take the same filter. All parameters are optional and combine with AND.
| Parameter | Values | Default |
|---|---|---|
q | A CVE id or its start (CVE-2024-3), or words found in the title, description, vendor or product. 3–200 characters | none |
severity | Comma-separated: CRITICAL,HIGH,MEDIUM,LOW,NONE,UNKNOWN (any case) | all |
kev | true (or 1): only CVEs in CISA's Known Exploited Vulnerabilities catalog | false |
vendor | A vendor among the record's affected products, exact name in any case (Microsoft). See /v1/vendors | none |
product | A product among the affected, of vendor when one is given (Windows 11 version 24H2). See /v1/products | none |
ecosystem | npm, PyPI, Maven, NuGet, Go, RubyGems, crates.io, Packagist, Pub, Hex, SwiftURL, GitHub Actions (any case) | none |
cwe | A weakness the record or NVD names: CWE-79 or 79. See /v1/cwes | none |
assigner | The CNA that assigned the CVE, by its short name in any case (GitHub_M, microsoft). See /v1/assigners | none |
range | Publication date: all, today, 24h, week, 7d, month, 30d, 90d, year, 6m, 12m, custom | all |
from, to | First and last day of a custom range (YYYY-MM-DD, both inclusive, UTC). Either may be left out | open |
sort | published, updated, id, score, epss, kevAdded (when CISA added it to KEV; CVEs not in KEV last) | published |
dir | desc, asc | desc |
page | Page number from 1 (/v1/cves only) | 1 |
pageSize | Rows per page, 1–100 (/v1/cves only) | 50 |
strict | true (or 1): an unknown parameter or a value the filter would ignore is a 400 instead (see below) | false |
Notes:
Ranges are resolved when the request runs, in UTC:
24his the last 24 hours,todayandweek(from Monday) andmonthandyearare calendar periods so far,7d/30d/90d/6m/12mare rolling.fromandtoare read only withrange=custom. Days given the wrong way round are swapped.The result window is 10,000 rows. Paging (and
/export) reaches the first 10,000 rows of a filter in its order;pageabovemaxPageis clamped to it. For more, narrow the filter (acustomrange per month, for example) or reverse the order.strict=truechecks everything the lenient reading would drop: unknown parameter names (sevrity), values outside the lists above, aqunder 3 characters, a malformed day,from/towithoutrange=custom, apageorpageSizethat is not a whole number. Themessagelists every problem, separated by;:curl -s 'https://api.trackcve.app/v1/cves?severity=critcal&strict=true'{ "error": { "code": "VALIDATION_FAILED", "message": "severity is a comma-separated list of CRITICAL, HIGH, MEDIUM, LOW, NONE, UNKNOWN", "requestId": "…" } }Values that are only clamped to a limit (
pageSize=500, apagepastmaxPage) are not problems: the response says what was used.Rejected CVEs are not in the list, the timeline or the export;
/v1/cves/{id}still returns them withstate: "REJECTED".
Endpoints
| Endpoint | Returns |
|---|---|
GET /v1/cves | One page of CVEs matching a filter |
GET /v1/cves/{id} | Everything known about one CVE |
GET /v1/cves/{id}/epss | Its EPSS day by day over about the last 30 days |
GET /v1/export | Every CVE of a filter (up to 10,000) as CSV or JSON |
GET /v1/timeline | Matching CVEs per time bucket and severity |
GET /v1/summary | Headline counts and when each source was last read |
GET /v1/vendors | Vendor names for the vendor filter, with CVE counts |
GET /v1/products | Product names for the product filter, with CVE counts |
GET /v1/ecosystems | Package ecosystems with CVE counts |
GET /v1/cwes | Weaknesses (CWE) for the cwe filter, with CVE counts |
GET /v1/assigners | CNAs for the assigner filter, with CVE counts |
GET /v1/stats | Severity, top weaknesses, vendors and CNAs of a recent window |
GET /v1/health | Whether the API is up, and which build |
GET /v1/openapi.json | The OpenAPI 3.1 description of all of the above |
GET /v1/cves
One page of CVEs for a filter, in its order.
curl -s 'https://api.trackcve.app/v1/cves?severity=CRITICAL&kev=true&pageSize=1'{
"items": [
{
"id": "CVE-2026-114625",
"published": "2026-10-22T20:47:56.427Z",
"updated": "2026-10-24T03:55:14.884Z",
"title": "Security Advisory 0199",
"severity": "CRITICAL",
"score": 9.4,
"vendor": "Arista Networks",
"product": "VeloCloud Orchestrator",
"kev": true,
"epss": 0.01025,
"kevAdded": "2026-10-23"
}
],
"total": 622,
"totalCapped": false,
"page": 1,
"pageSize": 1,
"maxPage": 10000
}| Field | Type | Description |
|---|---|---|
items[] | array | The rows, see below |
total | number | Rows matching the filter across all pages, counted up to 10,000 |
totalCapped | boolean | true when more than 10,000 match: total is then a lower bound (/v1/timeline has the exact count) |
page | number | The page returned (after clamping) |
pageSize | number | Rows per page |
maxPage | number | The last page that can be asked for at this page size |
A row (CveListItem):
| Field | Type | Description |
|---|---|---|
id | string | CVE-2026-114625 |
published | string | null | When the CVE was published |
updated | string | null | When the record last changed |
title | string | The assigner's title, or the start of the description when it has none |
severity | string | See conventions |
score | number | null | CVSS base score, 0–10 |
vendor | string | null | The first affected vendor of the record |
product | string | null | The first affected product |
kev | boolean | In CISA's Known Exploited Vulnerabilities catalog |
epss | number | null | EPSS probability, 0–1 |
kevAdded | string | null | The day CISA added it to KEV (YYYY-MM-DD), null when it is not in KEV |
To walk all pages, increase page until it equals maxPage or a page comes back with fewer than pageSize items. For more than a few pages, prefer /v1/export: one request instead of up to 100.
GET /v1/cves/{id}
Everything known about one CVE. Also returns rejected CVEs.
curl -s https://api.trackcve.app/v1/cves/CVE-2021-44228{
"id": "CVE-2021-44228",
"state": "PUBLISHED",
"published": "2021-12-10T00:00:00.000Z",
"updated": "2025-10-21T23:25:23.121Z",
"reserved": "2021-11-26T00:00:00.000Z",
"assigner": "apache",
"title": "Apache Log4j2 JNDI features do not protect against attacker controlled LDAP and other JNDI related endpoints",
"description": "Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases …",
"severity": "CRITICAL",
"score": 10,
"cvss": [
{ "source": "adp", "provider": "CISA-ADP", "version": "3.1", "score": 10, "severity": "CRITICAL", "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" },
{ "source": "nvd", "provider": "NVD", "version": "3.1", "score": 10, "severity": "CRITICAL", "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H" }
],
"weaknesses": [{ "id": "CWE-502", "description": "CWE-502 Deserialization of Untrusted Data" }],
"affected": [
{
"vendor": "Apache Software Foundation",
"product": "Apache Log4j2",
"defaultStatus": null,
"versions": [{ "version": "2.0-beta9", "status": "affected", "lessThan": "log4j-core*", "lessThanOrEqual": null, "versionType": "custom" }]
}
],
"references": [{ "url": "https://logging.apache.org/log4j/2.x/security.html", "name": null, "tags": ["x_transferred"] }],
"solutions": [],
"workarounds": [],
"exploits": [],
"rejectedReasons": [],
"kev": {
"name": "Apache Log4j2 Remote Code Execution Vulnerability",
"dateAdded": "2021-12-10",
"dueDate": "2021-12-24",
"requiredAction": "For all affected software assets for which updates exist, the only acceptable remediation actions are: …",
"knownRansomware": true,
"notes": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228"
},
"epss": { "score": 0.99999, "percentile": 1, "date": "2026-11-02" },
"ssvc": { "exploitation": "active", "automatable": "yes", "technicalImpact": "total" },
"nvdStatus": "Analyzed",
"packages": [{ "ecosystem": "Maven", "name": "org.apache.logging.log4j:log4j-core", "advisory": "GHSA-jfh8-c2jp-5v3q" }],
"sources": [
{ "name": "CVE.org record", "url": "https://www.cve.org/CVERecord?id=CVE-2021-44228" },
{ "name": "NVD", "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" }
]
}(Shortened: the real record has more weaknesses, references, packages and sources.)
| Field | Type | Description |
|---|---|---|
id | string | The CVE id |
state | string | PUBLISHED or REJECTED |
published | string | null | When it was published |
updated | string | null | When the record last changed |
reserved | string | null | When the id was reserved |
assigner | string | null | Short name of the CNA that assigned it |
title | string | null | The CNA's title |
description | string | English description |
severity, score | string, number | null | The best base score, as in the list |
cvss[] | array | Every score: source (cna, adp = CISA Vulnrichment, nvd), provider, version (2.0, 3.0, 3.1, 4.0), score, severity, vector (or null) |
weaknesses[] | array | id (CWE-79, or null when described in words only) and description |
affected[] | array | vendor, product, defaultStatus, versions[] (version, status, lessThan, lessThanOrEqual, versionType) as the CNA wrote them |
references[] | array | url, name (or null), tags (patch, vendor-advisory, exploit…) |
solutions[], workarounds[], exploits[] | string[] | Text the CNA gave for each |
rejectedReasons[] | string[] | Why it was rejected (when state is REJECTED) |
kev | object | null | The CISA KEV entry: name, dateAdded, dueDate, requiredAction, knownRansomware, notes |
epss | object | null | Today's EPSS: score (0–1), percentile (0–1), date |
ssvc | object | null | CISA's SSVC decision points: exploitation, automatable, technicalImpact |
nvdStatus | string | null | NVD's analysis status (Analyzed, Awaiting Analysis…), when NVD has the record |
packages[] | array | Packages named by reviewed GitHub advisories: ecosystem, name, advisory (GHSA-…) |
sources[] | array | Links to the record at its primary sources: name, url |
Errors: 400 VALIDATION_FAILED for a malformed id, 404 NOT_FOUND for an unknown one.
GET /v1/cves/{id}/epss
The CVE's EPSS day by day for about the last 30 days, as FIRST publishes it.
curl -s https://api.trackcve.app/v1/cves/CVE-2021-44228/epss{
"points": [
{ "date": "2026-10-04", "score": 0.99999, "percentile": 1 },
{ "date": "2026-10-05", "score": 0.99999, "percentile": 1 }
]
}Fetched from FIRST's API on demand and cached for an hour. When FIRST does not answer, this is 503 UNAVAILABLE; the current EPSS is still in /v1/cves/{id}. 404 when the CVE is not in the database.
GET /v1/export
Every CVE matching a filter, up to 10,000 rows, in the filter's order, as one download. page and pageSize are ignored.
| Parameter | Values | Default |
|---|---|---|
format | csv or json | csv |
curl -s 'https://api.trackcve.app/v1/export?range=24h&severity=HIGH,CRITICAL'id,published,updated,severity,score,epss,kev,vendor,product,title
CVE-2026-121513,2026-11-03T14:18:29.656Z,2026-11-03T14:18:29.656Z,HIGH,7.5,,false,D3TN GmbH,µD3TN,Out-of-bounds Write in µD3TN- CSV: UTF-8, CRLF line ends, a header row, the columns of the header row above (every
CveListItemfield butkevAdded, which only JSON has). Cells with a comma, quote or line break are quoted (RFC 4180);nullis an empty cell. A cell starting with=,+,-,@, tab or carriage return gets a leading'so spreadsheets do not run it as a formula. - JSON:
{ "items": [CveListItem, …] }. - Sent with
Content-Disposition: attachment; filename="cves-YYYY-MM-DD.csv". - Any other
formatis400 VALIDATION_FAILED.
GET /v1/timeline
How many CVEs matching a filter were published per time bucket, split by severity. sort, dir, page and pageSize are ignored. Unlike /v1/cves, the counts are exact, with no 10,000 cap.
curl -s 'https://api.trackcve.app/v1/timeline?range=7d&severity=CRITICAL'{
"granularity": "day",
"before": 38415,
"points": [
{ "key": "2026-10-27", "total": 37, "critical": 37, "high": 0, "medium": 0, "low": 0, "none": 0, "unknown": 0 },
{ "key": "2026-10-28", "total": 36, "critical": 36, "high": 0, "medium": 0, "low": 0, "none": 0, "unknown": 0 }
]
}| Field | Description |
|---|---|
granularity | Bucket width, chosen from the span: hour (up to 2 days), day (up to 92 days), week (up to 200 days), month (up to 15 years), year |
before | Matching CVEs published before the first bucket (where a cumulative line starts) |
points[] | Every bucket of the span in order, empty ones included |
points[].key | The bucket's start in UTC: 2026-10-26T16 (hour), 2026-10-26 (day; for week, its Monday), 2026-10 (month), 2026 (year) |
The total of a filter is before plus the sum of points[].total.
GET /v1/summary
Headline numbers and when each source was last read. Good for a status line.
{
"total": 399452,
"rejected": 18863,
"last24h": 402,
"last7d": 2914,
"last30d": 16826,
"critical30d": 1501,
"kev": 1766,
"sources": [
{ "name": "cvelist", "syncedAt": "2026-11-03T14:30:47.169Z" },
{ "name": "nvd", "syncedAt": "2026-11-03T14:19:47.600Z" },
{ "name": "kev", "syncedAt": "2026-11-03T10:19:36.558Z" },
{ "name": "epss", "syncedAt": "2026-11-03T10:19:39.979Z" },
{ "name": "ghsa", "syncedAt": "2026-11-03T10:20:16.913Z" }
]
}total is published CVEs, rejected the rejected ones; last24h, last7d, last30d count CVEs published in those windows, critical30d the critical ones of the last 30 days, kev the CVEs in CISA KEV. syncedAt is null for a source never read.
GET /v1/vendors
Vendor names for the vendor filter, most CVEs first, at most 20. Names are spelled as the CNAs write them, so one company can appear several times (Microsoft, Microsoft Corporation).
| Parameter | Values |
|---|---|
q | Part of the name (with one or two letters, its start). Without it, the vendors with the most CVEs |
curl -s 'https://api.trackcve.app/v1/vendors?q=micro'{ "items": [{ "name": "Microsoft", "count": 12296 }, { "name": "Microsoft Corporation", "count": 865 }, { "name": "Micro Focus", "count": 127 }] }GET /v1/products
The same for products.
| Parameter | Values |
|---|---|
vendor | Only products of this vendor (exact, any case) |
q | Part of the name |
curl -s 'https://api.trackcve.app/v1/products?vendor=Microsoft&q=windows'GET /v1/ecosystems
Package ecosystems with how many CVEs have a reviewed GitHub advisory in each, most first.
{ "items": [{ "name": "Maven", "count": 6988 }, { "name": "npm", "count": 6300 }, { "name": "PyPI", "count": 6153 }] }GET /v1/cwes
Weaknesses for the cwe filter, most CVEs first, up to 20. q is part of the number: 79 finds CWE-79 first, then CWE-798, CWE-799 and the rest.
curl -s 'https://api.trackcve.app/v1/cwes?q=79'{ "items": [{ "name": "CWE-79", "count": 49113 }, { "name": "CWE-798", "count": 1907 }, { "name": "CWE-799", "count": 95 }] }GET /v1/assigners
CNAs (the organizations that assign CVE ids) for the assigner filter, by their short name in the CVE record, most CVEs first, up to 20. q is matched like the vendor's.
curl -s 'https://api.trackcve.app/v1/assigners?q=git'{ "items": [{ "name": "GitHub_M", "count": 22083 }, { "name": "GitLab", "count": 1490 }, { "name": "Gitea", "count": 195 }] }GET /v1/stats
What the CVEs published in a window were. The window ends now.
| Parameter | Values | Default |
|---|---|---|
range | 7d, 30d, 90d, 12m | 30d |
curl -s 'https://api.trackcve.app/v1/stats?range=7d'{
"range": "7d",
"from": "2026-10-27T17:25:36.232Z",
"total": 2957,
"scored": 2509,
"kev": 2,
"kevAdded": 8,
"severity": { "critical": 244, "high": 1028, "medium": 1120, "low": 115, "none": 2, "unknown": 448 },
"cwes": [{ "name": "CWE-79", "count": 325 }, { "name": "CWE-862", "count": 217 }, { "name": "CWE-22", "count": 97 }],
"vendors": [{ "name": "Google", "count": 214 }, { "name": "Linux", "count": 171 }, { "name": "Drupal", "count": 106 }],
"assigners": [{ "name": "VulnCheck", "count": 457 }, { "name": "GitHub_M", "count": 330 }, { "name": "Wordfence", "count": 215 }]
}| Field | Description |
|---|---|
total | Published CVEs in the window |
scored | Of them, with a CVSS score from anyone |
kev | Of them, in CISA KEV now |
kevAdded | CVEs of any age CISA added to KEV in the window |
severity | Of total, per severity |
cwes, vendors, assigners | The ten largest, most first (vendors leaves out n/a); arrays shortened above |
Cached for 5 minutes. The same figures, as a page: trackcve.app/stats.
GET /v1/health
{ "status": "ok", "version": "d4bfebc" }version is the commit the server was built from (dev on a local build). Not rate limited, never cached. Use it to check that the API is reachable, not to poll for new data (/v1/summary says when each source was last read).
GET /v1/openapi.json
The OpenAPI 3.1 description of every endpoint, parameter and response. It is generated from the same schemas the API's answers are checked against, so it cannot drift from what is sent; info.version is the contract version. Objects are left open (no additionalProperties: false), so a field added later never breaks a generated client.
# A typed Go client, for example
curl -s https://api.trackcve.app/v1/openapi.json -o cve-tracker.json
oapi-codegen -package cvetracker -generate types,client cve-tracker.json > client.goAlso linked from /.well-known/api-catalog as its service-desc.
Beyond the JSON API
These live on the website, trackcve.app, not on the API host, and take the same filter parameters:
| URL | What |
|---|---|
https://trackcve.app/cve/{id}.md | One CVE as Markdown (or its page with Accept: text/markdown): handy to print in a terminal |
https://trackcve.app/feed.xml?… | Atom feed of the 50 newest CVEs of a filter, e.g. ?severity=CRITICAL&kev=true; with sort=kevAdded, the 50 CISA added to KEV most recently |
https://trackcve.app/badge/{id}.svg | A badge with the CVE's severity, score and KEV status, for a README or a ticket |
https://trackcve.app/?… | The same filter in the browser, for an "open in browser" link |
https://trackcve.app/.well-known/api-catalog | Machine-readable pointer to this API (RFC 9727) |
Tips for CLI tools and scripts
- Send a
User-Agentthat names your tool and a way to reach you (mycli/1.2 (+https://github.com/you/mycli)). It helps if something needs sorting out instead of blocking. - Watch
RateLimit-Remainingand slow down before you hit0; on429and503, sleep forRetry-Afterseconds and try again, with a cap on retries. - Cache locally for the times in Rate limit and caching. A detail fetched a minute ago is still current.
- Use
/v1/exportfor bulk: one request for up to 10,000 rows rather than walking pages. - Check freshness with
/v1/summary:sources[].syncedAtis when each source was last read. The CVE List is read every 10 minutes, NVD hourly, KEV and EPSS every 6 hours, GitHub advisories every 12 hours; the sources page shows which, if any, is delayed. - "What's new since I last looked": ask for
sort=published&dir=descwithrange=24h(or acustomrange from your last run's day) and stop at the first id you have already seen. To catch changed records too, usesort=updated. - Checking a list of ids (from a scanner, an SBOM): one
/v1/cves/{id}each, with a small concurrency (2–4) so you stay under the limit; treat404as "not published (yet)". - Send
strict=truewith every filter, so a typo is an error instead of a wider result, and validate ids before sending: a CVE id matches^CVE-\d{4}-\d{4,}$. - Exit codes: map
NOT_FOUNDandVALIDATION_FAILEDto a user error,RATE_LIMITEDandUNAVAILABLEto "try again", and print therequestIdforINTERNAL.
A minimal example in shell:
api=https://api.trackcve.app/v1
ua='mycli/0.1 (+https://github.com/you/mycli)'
# Critical CVEs of the last day, one line each
curl -sf -A "$ua" "$api/cves?range=24h&severity=CRITICAL&pageSize=100" \
| jq -r '.items[] | "\(.id)\t\(.score)\t\(.vendor // "-")\t\(.title)"'
# Is it known exploited, and how likely is exploitation?
curl -sf -A "$ua" "$api/cves/CVE-2021-44228" \
| jq '{id, severity, score, kev: (.kev != null), epss: .epss.score}'Recipes
Shell with curl and jq, each tried against the live API. They set api and ua as in the example above.
Critical, known exploited CVEs of the last 7 days, as tab-separated lines:
curl -sf -A "$ua" "$api/cves?range=7d&severity=CRITICAL&kev=true&pageSize=100&strict=true" \
| jq -r '.items[] | [.id, (.score | tostring), (.vendor // "-"), .title] | @tsv'New high and critical CVEs since the last run, for a cron job. The day of the last run narrows the request, the exact time filters the rows; with more than 100 a day, page through with page:
state=~/.cache/cve-last-run
since=$(cat "$state" 2>/dev/null || date -u +%Y-%m-%dT00:00:00.000Z)
now=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
curl -sf -A "$ua" "$api/cves?range=custom&from=${since%%T*}&severity=CRITICAL,HIGH&pageSize=100&strict=true" \
| jq -r --arg since "$since" '.items[] | select(.published > $since) | "\(.id)\t\(.severity)\t\(.title)"'
echo "$now" > "$state"Fail a CI job when a vendor you depend on has a known exploited CVE from the last week (jq -e sets the exit code):
curl -sf -A "$ua" "$api/cves?vendor=Microsoft&range=7d&kev=true&strict=true" | jq -e '.total == 0' > /dev/nullCheck a list of ids (from a scanner or an SBOM, one per line): severity, KEV and EPSS of each, or why there is none. One at a time with a pause stays well under the limit:
while read -r id; do
curl -s -A "$ua" "$api/cves/$id" | jq -r --arg id "$id" \
'if .error then "\($id)\t\(.error.code)" else [.id, .severity, (if .kev then "KEV" else "-" end), (.epss.score // "-" | tostring)] | @tsv end'
sleep 0.5
done < ids.txtCVE-2021-44228 CRITICAL KEV 0.99999
CVE-2024-3094 CRITICAL - 0.909
CVE-2099-0001 NOT_FOUNDnpm packages most likely to be exploited, from the last 30 days' CVEs:
curl -sf -A "$ua" "$api/cves?ecosystem=npm&range=30d&sort=epss&dir=desc&pageSize=10&strict=true" \
| jq -r '.items[] | "\(.epss)\t\(.id)\t\(.product // "-")"'CVEs per month this year, with the critical ones (exact counts, no 10,000 cap):
curl -sf -A "$ua" "$api/timeline?range=year&strict=true" | jq -r '.points[] | "\(.key)\t\(.total)\t\(.critical)"'A month of one vendor's CVEs into a spreadsheet:
curl -sf -A "$ua" -o microsoft.csv "$api/export?vendor=Microsoft&range=30d&format=csv&strict=true"What CISA added to KEV this week, whatever the CVE's age:
curl -sf -A "$ua" "$api/cves?kev=true&sort=kevAdded&pageSize=50&strict=true" \
| jq -r --arg since "$(date -u -v-7d +%F 2>/dev/null || date -u -d '7 days ago' +%F)" '.items[] | select(.kevAdded >= $since) | "\(.kevAdded)\t\(.id)\t\(.title)"'SQL injection in the last 30 days, from one CNA:
curl -sf -A "$ua" "$api/cves?cwe=89&assigner=VulnCheck&range=30d&strict=true" | jq '.total'Subscribe instead of polling: the Atom feed takes the same filter, so a feed reader, Slack's RSS app or a chat bot can follow https://trackcve.app/feed.xml?severity=CRITICAL&kev=true without a line of code.
Versioning and changelog
The /v1 contract is versioned semantically; its version is info.version of /v1/openapi.json. Within /v1 only additions are made (an endpoint, a parameter, a field, an enum value): ignore what you do not know. A breaking change gets a new path, /v2, with /v1 kept running alongside it.
Every change to the API and the site is in the changelog, with the full versioning policy.
OpenAPI and typed clients
/v1/openapi.json generates a client in most languages:
# TypeScript types
npx openapi-typescript https://api.trackcve.app/v1/openapi.json -o cve-tracker.d.ts
# A Python package
openapi-python-client generate --url https://api.trackcve.app/v1/openapi.jsonBehind it, the response shapes are defined once, as zod schemas, in packages/core/src/contract, and packages/core/src/client/client.ts is the typed client the site itself uses. They are the source of truth if this page and the API ever disagree. The package is not published to npm; copy the schemas if you write a client in TypeScript.