Docs / Cyber Threat Data API / Endpoints / Score a list of CVEs

Score a list of CVEs

Scores a LIST OF CVEs rather than paging a collection, and that is the one place this API works differently from everywhere else. Send up to 100 ids in `?cve=`, comma-separated or as a repeated parameter, and you get back the score in force for each one that FIRST publishes a score for. There is no `limit`, no `cursor` and no `meta.next`, because there is no walk: the corpus is over 377,000 CVEs re-scored every night, and taking all of it is a job for the one file FIRST publishes daily — whose URL `GET /v1/cyber/epss/snapshot` hands you. A 101st id is a `400` naming the count rather than a truncation, because the ids that got dropped would come back in `meta.not_found`, which means something else entirely. `meta.not_found` is the field to read and it is always there, `[]` included. A CVE with no score is an ordinary answer — FIRST scores published CVEs and the corpus moves — and this endpoint exists so that “there is genuinely no score for this” and “the lookup did not work” stop arriving as the same short array. `epss` is a probability in [0, 1] that the CVE will be exploited in the next 30 days. It is NOT a percentage and NOT a severity: 0.00042 is four in ten thousand, and it says nothing about how bad exploitation would be. `percentile` is the field to compare across days, because the raw score is re-scaled whenever FIRST retrains and every row carries the `model_version` that produced it. These scores are published by FIRST under CC BY 4.0. If you publish them onward, reproduce the attribution string the snapshot endpoint serves.

GET https://api.softon.dev/v1/cyber/epss Copy

Query parameters

ParameterTypeDescription
cve required string **Required.** Up to 100 CVE ids, comma-separated — `CVE-2021-44228,CVE-2014-0160` — or the parameter repeated, which means the same thing. Case is normalised. A 101st id is a `400` naming the count rather than a silent truncation, because the ids that were dropped would come back in `meta.not_found`, which means "FIRST publishes no score for this CVE" and would be false about them. There is no way to page the whole score set here and that is deliberate: it is over 377,000 rows re-scored daily, and `GET /v1/cyber/epss/snapshot` hands you the URL of the one file FIRST publishes it in.

Request

curl -G https://api.softon.dev/v1/cyber/epss \
  -H "Authorization: Bearer $SOFTON_KEY" \
  -d cve=CVE-2021-44228,CVE-2014-0160

Response

The envelope is identical on every softon.dev API: data, meta, error. Only the shape inside data changes per dataset — see the response envelope.

{
  "data": [
    {
      "id": "epss:2026-09-20:CVE-2021-44228",
      "source": "epss",
      "score_date": "2026-09-20",
      "model_version": "v2026.06.15",
      "cve_id": "CVE-2021-44228",
      "epss": 0.99999,
      "percentile": 1.0,
      "upstream_url": "https://epss.empiricalsecurity.com/epss_scores-2026-09-20.csv.gz",
      "checksum_sha256": "6e5d46d4fa7f7229fa41813e01ea9a56221fa082244323470a70911e984275b6",
      "ingested_at": "2026-09-21T02:00:41.207318Z"
    }
  ],
  "meta": { "requested": 2, "answered": 1, "not_found": ["CVE-2026-00000"], "request_id": "req_9Fv3" },
  "error": null
}

Fields of each item in data[]

FieldTypeDescription
id string Stable identifier, "<source>:<score date>:<CVE id>" — `epss:2026-09-20:CVE-2021-44228`. Opaque: treat the whole string as the id. **There is no `GET /v1/cyber/epss/{id}`, and that is deliberate** — a superseded score is pruned, so an id you stored last week names a row that is gone, and a by-id route would be a promise of permanence this dataset does not make. Ask for the CVE instead: `?cve=` always answers with the score in force.
source string Which scraper delivered this score. `epss` is FIRST's own published file.
score_date date The day FIRST produced this score — the as-of, and half the id. **Read it per row rather than assuming one value for the response.** A daily snapshot lands as nineteen batches, so a run that failed part-way leaves some rows on the previous day, and each row says which.
model_version string The trained model that produced the score, `v2026.06.15`. On every row because **scores are not comparable across a retrain** — FIRST re-scales, so a raw score from two different model versions is two different scales. `percentile` is the field to compare across days.
cve_id string The CVE identifier, `CVE-2021-44228`, upper-case as published.
epss number **The probability this CVE will be exploited in the wild in the next 30 days, in `[0, 1]`.** Not a percentage: `0.00042` is four in ten thousand, and multiplying by 100 somewhere and forgetting further down is how a backlog gets sorted by a number a hundred times too large. Served at the precision FIRST published — up to five decimals, unrounded, never padded, so `0.5` and `0.00047` both occur. It is **not a severity and not CVSS**, and it says nothing about impact. `/v1/cyber/kev` is the complementary fact: evidence that exploitation has already happened.
percentile number Where this score ranks against every other scored CVE, in `[0, 1]`. **This is the field to compare across days**, because it is a rank within its own model run where the raw score moves when the model is retrained. `0.0` and `1.0` both occur.
upstream_url string The dated file these numbers were read out of, after the redirect chain — not the `-current` pointer, which names no day. Download it and you have FIRST's own copy of this snapshot. **FIRST keeps every day's file at a URL of this shape**, which is why this platform serves only the current score per CVE rather than mirroring the archive.
checksum_sha256 string `sha256` of that file's **gzipped bytes as served**, so your `sha256sum` of the download and this value are the same string. A digest of a decompressed copy would be a statement about someone's gunzip instead.
ingested_at timestamp When this platform stored the score. **Not the as-of** — `score_date` is — and the two differ by the time between FIRST publishing and the daily run reading it.

Try it

v1 · stable
Send the request to see a response.