Skip to content
CVE Tracker

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/v1

Machine-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 (and HEAD, OPTIONS). There is nothing to write.
  • Format: JSON (application/json), except GET /v1/export?format=csv.
  • Times are ISO 8601 in UTC with milliseconds (2026-11-03T14:18:29.656Z). Days are YYYY-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-44228 works.
  • Severity is one of CRITICAL, HIGH, MEDIUM, LOW, NONE, UNKNOWN (not scored yet). It and score are 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 send strict=true: then any of this is a 400 VALIDATION_FAILED that names the problem.
  • Versioning: everything is under /v1. Fields may be added within v1; none is removed or changes meaning. Ignore fields you do not know. See Versioning and changelog.
  • Request id: every response has an X-Request-Id header (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:

HeaderMeaning
RateLimit-LimitRequests allowed in the window
RateLimit-RemainingRequests left in the current window
RateLimit-ResetSeconds until the window starts again
Retry-AfterOn 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:

EndpointsFresh for
/summary, /cves, /timeline, /export1 minute
/cves/{id}, /vendors, /products, /ecosystems, /cwes, /assigners, /stats5 minutes
/cves/{id}/epss1 hour
/healthnot 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"
    }
}
StatuscodeWhenWhat to do
400VALIDATION_FAILEDA malformed CVE id, an unknown format on /export, any filter problem with strict=trueFix the request
404NOT_FOUNDThe CVE is not in the database, or there is no such routeDo not retry
429RATE_LIMITEDOver the rate limitRetry after Retry-After
503UNAVAILABLEThe server is shedding load, a query ran out of time, or FIRST is down (EPSS history)Retry after Retry-After (5 s)
500INTERNALA bug on our sideReport 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.

ParameterValuesDefault
qA CVE id or its start (CVE-2024-3), or words found in the title, description, vendor or product. 3–200 charactersnone
severityComma-separated: CRITICAL,HIGH,MEDIUM,LOW,NONE,UNKNOWN (any case)all
kevtrue (or 1): only CVEs in CISA's Known Exploited Vulnerabilities catalogfalse
vendorA vendor among the record's affected products, exact name in any case (Microsoft). See /v1/vendorsnone
productA product among the affected, of vendor when one is given (Windows 11 version 24H2). See /v1/productsnone
ecosystemnpm, PyPI, Maven, NuGet, Go, RubyGems, crates.io, Packagist, Pub, Hex, SwiftURL, GitHub Actions (any case)none
cweA weakness the record or NVD names: CWE-79 or 79. See /v1/cwesnone
assignerThe CNA that assigned the CVE, by its short name in any case (GitHub_M, microsoft). See /v1/assignersnone
rangePublication date: all, today, 24h, week, 7d, month, 30d, 90d, year, 6m, 12m, customall
from, toFirst and last day of a custom range (YYYY-MM-DD, both inclusive, UTC). Either may be left outopen
sortpublished, updated, id, score, epss, kevAdded (when CISA added it to KEV; CVEs not in KEV last)published
dirdesc, ascdesc
pagePage number from 1 (/v1/cves only)1
pageSizeRows per page, 1–100 (/v1/cves only)50
stricttrue (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: 24h is the last 24 hours, today and week (from Monday) and month and year are calendar periods so far, 7d/30d/90d/6m/12m are rolling.

  • from and to are read only with range=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; page above maxPage is clamped to it. For more, narrow the filter (a custom range per month, for example) or reverse the order.

  • strict=true checks everything the lenient reading would drop: unknown parameter names (sevrity), values outside the lists above, a q under 3 characters, a malformed day, from/to without range=custom, a page or pageSize that is not a whole number. The message lists 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, a page past maxPage) 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 with state: "REJECTED".

Endpoints

EndpointReturns
GET /v1/cvesOne page of CVEs matching a filter
GET /v1/cves/{id}Everything known about one CVE
GET /v1/cves/{id}/epssIts EPSS day by day over about the last 30 days
GET /v1/exportEvery CVE of a filter (up to 10,000) as CSV or JSON
GET /v1/timelineMatching CVEs per time bucket and severity
GET /v1/summaryHeadline counts and when each source was last read
GET /v1/vendorsVendor names for the vendor filter, with CVE counts
GET /v1/productsProduct names for the product filter, with CVE counts
GET /v1/ecosystemsPackage ecosystems with CVE counts
GET /v1/cwesWeaknesses (CWE) for the cwe filter, with CVE counts
GET /v1/assignersCNAs for the assigner filter, with CVE counts
GET /v1/statsSeverity, top weaknesses, vendors and CNAs of a recent window
GET /v1/healthWhether the API is up, and which build
GET /v1/openapi.jsonThe 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
}
FieldTypeDescription
items[]arrayThe rows, see below
totalnumberRows matching the filter across all pages, counted up to 10,000
totalCappedbooleantrue when more than 10,000 match: total is then a lower bound (/v1/timeline has the exact count)
pagenumberThe page returned (after clamping)
pageSizenumberRows per page
maxPagenumberThe last page that can be asked for at this page size

A row (CveListItem):

FieldTypeDescription
idstringCVE-2026-114625
publishedstring | nullWhen the CVE was published
updatedstring | nullWhen the record last changed
titlestringThe assigner's title, or the start of the description when it has none
severitystringSee conventions
scorenumber | nullCVSS base score, 0–10
vendorstring | nullThe first affected vendor of the record
productstring | nullThe first affected product
kevbooleanIn CISA's Known Exploited Vulnerabilities catalog
epssnumber | nullEPSS probability, 0–1
kevAddedstring | nullThe 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.)

FieldTypeDescription
idstringThe CVE id
statestringPUBLISHED or REJECTED
publishedstring | nullWhen it was published
updatedstring | nullWhen the record last changed
reservedstring | nullWhen the id was reserved
assignerstring | nullShort name of the CNA that assigned it
titlestring | nullThe CNA's title
descriptionstringEnglish description
severity, scorestring, number | nullThe best base score, as in the list
cvss[]arrayEvery score: source (cna, adp = CISA Vulnrichment, nvd), provider, version (2.0, 3.0, 3.1, 4.0), score, severity, vector (or null)
weaknesses[]arrayid (CWE-79, or null when described in words only) and description
affected[]arrayvendor, product, defaultStatus, versions[] (version, status, lessThan, lessThanOrEqual, versionType) as the CNA wrote them
references[]arrayurl, 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)
kevobject | nullThe CISA KEV entry: name, dateAdded, dueDate, requiredAction, knownRansomware, notes
epssobject | nullToday's EPSS: score (0–1), percentile (0–1), date
ssvcobject | nullCISA's SSVC decision points: exploitation, automatable, technicalImpact
nvdStatusstring | nullNVD's analysis status (Analyzed, Awaiting Analysis…), when NVD has the record
packages[]arrayPackages named by reviewed GitHub advisories: ecosystem, name, advisory (GHSA-…)
sources[]arrayLinks 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.

ParameterValuesDefault
formatcsv or jsoncsv
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 CveListItem field but kevAdded, which only JSON has). Cells with a comma, quote or line break are quoted (RFC 4180); null is 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 format is 400 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 }
    ]
}
FieldDescription
granularityBucket 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
beforeMatching CVEs published before the first bucket (where a cumulative line starts)
points[]Every bucket of the span in order, empty ones included
points[].keyThe 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).

ParameterValues
qPart 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.

ParameterValues
vendorOnly products of this vendor (exact, any case)
qPart 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.

ParameterValuesDefault
range7d, 30d, 90d, 12m30d
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 }]
}
FieldDescription
totalPublished CVEs in the window
scoredOf them, with a CVSS score from anyone
kevOf them, in CISA KEV now
kevAddedCVEs of any age CISA added to KEV in the window
severityOf total, per severity
cwes, vendors, assignersThe 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.go

Also 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:

URLWhat
https://trackcve.app/cve/{id}.mdOne 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}.svgA 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-catalogMachine-readable pointer to this API (RFC 9727)

Tips for CLI tools and scripts

  • Send a User-Agent that 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-Remaining and slow down before you hit 0; on 429 and 503, sleep for Retry-After seconds 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/export for bulk: one request for up to 10,000 rows rather than walking pages.
  • Check freshness with /v1/summary: sources[].syncedAt is 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=desc with range=24h (or a custom range from your last run's day) and stop at the first id you have already seen. To catch changed records too, use sort=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; treat 404 as "not published (yet)".
  • Send strict=true with 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_FOUND and VALIDATION_FAILED to a user error, RATE_LIMITED and UNAVAILABLE to "try again", and print the requestId for INTERNAL.

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/null

Check 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.txt
CVE-2021-44228	CRITICAL	KEV	0.99999
CVE-2024-3094	CRITICAL	-	0.909
CVE-2099-0001	NOT_FOUND

npm 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.json

Behind 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.