Skip to main content
Tutorial

CVE API for AI Agents: NVD, EPSS, KEV and OSV, One Key

Triage a CVE with an agent: NVD details, EPSS exploit probability, CISA KEV status and OSV advisories through one API key, each with its source.

fastcrw
By RecepNovember 7, 20267 min read

By the fastCRW team · Catalogue figures checked 2026-11-05 against GET /v1/data · Verify independently before relying on any figure.

A CVE API for an AI agent is the set of lookups a triage agent needs before it can rank a vulnerability: the CVE record and its severity, an exploit prediction score, whether the issue is known to be exploited, and any package advisories that point at it. NVD, EPSS, CISA KEV, the CVE record and OSV are all on Pergamon, so one fastCRW key covers the set. Every response keeps its own attribution. The calls in this post were run on 2026-11-05.

This is a tutorial for building a triage step. It does not decide what to patch first for you.

Sources and what each one answers

SourceCapabilityWhat it tells the agent
NVDnvd/cve, option cveIdThe CVE entry, including CVSS metrics and the CISA KEV fields when the CVE is listed.
EPSSepss/score, option cveAn exploit prediction score and percentile for the CVE, with a date.
CISA KEVcisakev/catalogThe full catalogue of CVEs with known exploitation. Large, so use it sparingly.
CVE recordcve/record, option idRecord metadata such as the publish and update dates and the assigning organisation.
OSVosv/vulnerability, option idPackage-level advisories, with their aliases, such as the CVE id they map to.

NVD and OSV sit in the Security category next to cisakev, cve and epss, so a find call filtered to Security covers all five sources.

NVD and its rolling window

NVD publishes its own limits. In its start-here guide it states: "The public rate limit (without an API key) is 5 requests in a rolling 30 second window; the rate limit with an API key is 50 requests in a rolling 30 second window" (nvd.nist.gov). The key goes in an apiKey request header. The nvdlib documentation puts the practical effect plainly: "a request will take 6 seconds with no API key" (nvdlibdocs).

Do not assume that Pergamon raises those limits. Plan the agent around NVD's published window: one NVD call per CVE, cached for the run, and a pause between batches. Here is the NVD call for Log4Shell, CVE-2021-44228:

curl "https://api.fastcrw.com/v1/data/nvd/cve?cveId=CVE-2021-44228" \
  -H "Authorization: Bearer $CRW_API_KEY"

Trimmed response:

{
  "success": true,
  "provider": "nvd",
  "capability": "cve",
  "credits_used": 1,
  "data": {
    "...": "...",
    "vulnerabilities": [
      {
        "cve": {
          "id": "CVE-2021-44228",
          "metrics": {
            "cvssMetricV31": [
              { "cvssData": { "baseSeverity": "CRITICAL" } }
            ]
          },
          "cisaExploitAdd": "2021-12-10",
          "cisaActionDue": "2021-12-24",
          "cisaVulnerabilityName": "Apache Log4j2 Remote Code Execution Vulnerability"
        }
      }
    ]
  }
}

The severity sits at vulnerabilities[0].cve.metrics.cvssMetricV31[0].cvssData.baseSeverity. For this CVE it reads CRITICAL.

EPSS score and history

EPSS gives a score for how likely the CVE is to be exploited, plus the percentile of that score across all CVEs. The epss/score capability returns the current value for one CVE:

curl "https://api.fastcrw.com/v1/data/epss/score?cve=CVE-2021-44228" \
  -H "Authorization: Bearer $CRW_API_KEY"

The entry you need is data.data[0]. It carries cve, epss, percentile and date. On 2026-11-05 the score for CVE-2021-44228 was 0.999990000 with a percentile of 1.000000000. EPSS returns these values as strings, so convert them with float() before you compare them.

For a trend, epss/history returns the daily values for the same CVE in data.data[0]["time-series"], newest first, each with epss, percentile and date. An agent can use it to tell a rising score from a flat one.

Read the EPSS score as an estimate of exploitation activity, not as proof that a system is under attack. Its value lies in ordering a long list of CVEs, and it works best next to the KEV fields, which record known exploitation rather than predicted exploitation.

Is it in KEV?

The CISA Known Exploited Vulnerabilities list is the question most triage agents care about. For a CVE that is listed, you do not need the whole list. The NVD record carries the KEV fields itself. For CVE-2021-44228 they read:

  • cisaExploitAdd: 2021-12-10
  • cisaActionDue: 2021-12-24
  • cisaVulnerabilityName: Apache Log4j2 Remote Code Execution Vulnerability

The full catalogue is available as cisakev/catalog. Over REST it returned 200 with a body of about 1.4 MB. Over MCP the same call is refused with [result_too_large] The result is 1398 KiB, over the 256 KiB limit. For a server source that refused call is refunded, but the agent still gets no data. Check KEV through the NVD record in the agent, and download the catalogue only in a batch job. The KEV fields appear only for a CVE that is listed, so a missing field means the NVD record does not list the CVE. It is not a statement that the CVE is safe.

OSV for package-level advisories

NVD describes the CVE. OSV describes the package advisory that fixes or explains it, and it is the better source when you triage a dependency. Look it up by its advisory id:

curl "https://api.fastcrw.com/v1/data/osv/vulnerability?id=GHSA-jfh8-c2jp-5v3q" \
  -H "Authorization: Bearer $CRW_API_KEY"

Trimmed response:

{
  "id": "GHSA-jfh8-c2jp-5v3q",
  "aliases": ["CVE-2021-44228"],
  "summary": "Remote code injection in Log4j"
}

If OSV has no advisory for a CVE, record that absence in the triage output. Do not treat it as a clean bill of health, because it only means this source has no package advisory for that identifier.

The aliases list is how you connect the advisory to a CVE. The same call with id=CVE-2021-44228 also returned the advisory on 2026-11-05, with the GHSA id in its aliases. The OSV data is licensed CC BY 4.0 and published by the OSV project, and that is the licence line to show.

A triage function with the Python SDK

The function below makes the four lookups for one CVE and returns the fields a triage step needs. It keeps each source's attribution, so the output can show where every field came from.

from crw import CrwClient

client = CrwClient()  # reads CRW_API_KEY


def triage_cve(cve_id: str) -> dict:
    nvd = client.get_data("nvd", "cve", {"cveId": cve_id})
    epss = client.get_data("epss", "score", {"cve": cve_id})
    record = client.get_data("cve", "record", {"id": cve_id})
    osv = client.get_data("osv", "vulnerability", {"id": cve_id})
    cve = nvd["data"]["vulnerabilities"][0]["cve"]
    return {
        "severity": cve["metrics"]["cvssMetricV31"][0]["cvssData"]["baseSeverity"],
        "kev_added": cve.get("cisaExploitAdd"),
        "epss": epss["data"]["data"][0]["epss"],
        "published": record["data"]["cveMetadata"]["datePublished"],
        "osv_aliases": osv["data"]["aliases"],
        "attribution": [
            nvd["provenance"]["attributionText"],
            epss["provenance"]["attributionText"],
            osv["provenance"]["attributionText"],
        ],
    }


print(triage_cve("CVE-2021-44228"))

The run on 2026-11-05 printed:

{'severity': 'CRITICAL', 'kev_added': '2021-12-10', 'epss': '0.999990000', 'published': '2021-12-10T00:00:00.000Z', 'osv_aliases': ['GHSA-jfh8-c2jp-5v3q'], 'attribution': ['Source: NIST National Vulnerability Database', 'Source: EPSS, FIRST.org', 'Source: OSV.dev']}

Each lookup costs 1 credit, so a triage of one CVE costs 4 credits. A failed upstream call is not billed.

Stay inside NVD's window in a batch

NVD allows 5 requests in a rolling 30 second window without a key. Spread the calls evenly and the spacing is one call every 6 seconds, because 30 divided by 5 is 6. The loop below makes one NVD lookup per CVE and pauses after each one. It reads the severity only, to keep the example short.

import time

from crw import CrwClient

client = CrwClient()  # reads CRW_API_KEY
NVD_SPACING_SECONDS = 6  # 5 NVD requests per rolling 30 seconds: one call every 6 seconds


def nvd_severity(cve_id: str) -> str:
    nvd = client.get_data("nvd", "cve", {"cveId": cve_id})
    cve = nvd["data"]["vulnerabilities"][0]["cve"]
    return cve["metrics"]["cvssMetricV31"][0]["cvssData"]["baseSeverity"]


def severities(cve_ids: list[str]) -> dict[str, str]:
    result = {}
    for cve_id in cve_ids:
        result[cve_id] = nvd_severity(cve_id)
        time.sleep(NVD_SPACING_SECONDS)
    return result


print(severities(["CVE-2021-44228", "CVE-2014-0160"]))

The run on 2026-11-05 printed {'CVE-2021-44228': 'CRITICAL', 'CVE-2014-0160': 'HIGH'}. The pause is the simplest way to stay inside the window. If your agent runs several sources in parallel, count the NVD calls separately, because the window belongs to NVD and not to the other sources.

What to keep in a triage record

A triage record should hold the facts and the place each fact came from. For each CVE, store the severity, the EPSS score and percentile with its date, the KEV dates when they exist, the OSV aliases, and the attributionText of each source. Store the time of the lookup as well, because the scores and the catalogue change over time.

Keep the decision apart from the record: the ranking is a policy your team owns, and the record should only show its inputs.

Attribution you must show

NVD asks products that use its data to say so. Its start-here guide words the notice as "This product uses data from the NVD API but is not endorsed or certified by the NVD." (nvd.nist.gov), and its terms of use carry a similar line (terms of use). Show that notice wherever your agent displays NVD fields. Keep the attributionText from each result next to the field it supports, as the function above does.

Honest limits

  • The triage function returns facts from four sources. It does not score exploitability or decide priority.
  • NVD's rate window applies to every NVD call your agent makes.
  • The KEV catalogue is too large for MCP. Use the NVD KEV fields for single lookups.
  • A CVE with no package advisory has no OSV record to triage.

Sources

Related: Public data API for AI agents · Security sources in the Pergamon catalogue · NVD on Pergamon · OSV on Pergamon

FAQ

Frequently asked questions

Does Pergamon raise the NVD rate limit?
This post makes no such claim. NVD publishes its own window: 5 requests in a rolling 30 second window without a key, and 50 with one. Plan the agent around that window.
Can I check whether a CVE is in the CISA KEV list without downloading the whole list?
Yes, for a listed CVE. The NVD record carries cisaExploitAdd, cisaActionDue and cisaVulnerabilityName. The full catalogue is about 1.4 MB, which is too large for an MCP result.
Why would an OSV lookup return a GHSA id for a CVE query?
OSV advisories list their CVE ids as aliases, and a lookup by CVE id returns the advisory that carries that alias. Read the aliases field to connect the two identifiers.

Get Started

Try fastCRW free

Run a live request in the playground — no signup required. Or grab a free API key with 500 credits, earn up to 500 more, no credit card.

Continue exploring

More tutorial posts

View category archive