NVD API Compatibility

Pagination

startIndex, opt-in cursor paging, and when to use bulk downloads instead

Four of the five endpoints are paginated with NVD's startIndex and resultsPerPage. VulnCheck adds an opt-in cursor as an alternative, and bulk downloads remain the right tool for pulling a whole corpus.

Choosing an approach

You areUse
Repointing an existing NVD integrationstartIndex - unchanged, no code edit, works at any depth
Writing a new integrationcursor - flat cost, no depth ceiling
Pulling the whole corpus/v3/backup/{index} - one download instead of hundreds of requests (for more on backups see API backup endpoint)

Page size limits

EndpointMaximum resultsPerPage
/rest/json/cves/2.02,000
/rest/json/cvehistory/2.05,000
/rest/json/cpes/2.010,000
/rest/json/cpematch/2.0500
/rest/json/source/2.01,000

Omitting resultsPerPage gives you the maximum, matching NVD's behaviour.

Offset paging

Works exactly as it does against NVD:

curl "https://api.vulncheck.com/rest/json/cves/2.0?resultsPerPage=2000&startIndex=4000" \
    --header 'apiKey: insert_token_here'
An offset gets more expensive the deeper you page. The server still has to walk past everything you skipped. On a 2.3 million record corpus, startIndex=1000000 takes around six seconds. That is a characteristic of offset paging rather than a fault, and it is why the cursor below exists.

Cursor paging

We did not want to limit our users to the much slower 'offset' based pagination, so we are supplying a cursor parameter which is opt-in. An empty value starts the walk:

curl "https://api.vulncheck.com/rest/json/cvehistory/2.0?resultsPerPage=5000&cursor=" \
    --header 'apiKey: insert_token_here'

The response gains a single field, nextCursor:

{
  "resultsPerPage": 5000,
  "startIndex": 0,
  "totalResults": 2305402,
  "format": "NVD_CVEHistory",
  "version": "2.0",
  "timestamp": "2026-09-21T10:14:22.017",
  "cveChanges": [],
  "nextCursor": "WzE2MzkxNDIxMDc4MjcsIjQwQzlDOTA1LTBERkQtNDk1Mi1BODk2LTNGQUNBRDE5QkZCQiJd"
}

Replay it to walk forward, and stop when nextCursor is absent. The last page omits it, so you terminate on the cursor rather than counting against totalResults:

cursor=""
while :; do
  response=$(curl -s --header "apiKey: $VULNCHECK_API_TOKEN" \
    "https://api.vulncheck.com/rest/json/cvehistory/2.0?resultsPerPage=5000&cursor=$cursor")
  echo "$response" | jq -c '.cveChanges[]'
  cursor=$(echo "$response" | jq -r '.nextCursor // empty')
  [ -z "$cursor" ] && break
done

Measured on a 2.3 million record index, a page that takes about six seconds by offset takes about 150 milliseconds by cursor, and cursor cost does not grow with depth.

Four behaviours worth knowing

nextCursor appears only if you asked
A request without a cursor parameter returns exactly NVD's seven envelope keys, so responses still validate against NVD's published schemas. Diffing two responses and seeing the envelope change shape is expected, not a bug.
A cursor is bound to its query
It encodes the sort position of one record, so it is meaningless against a different filter set. Keep every other parameter identical for the whole walk.
A cursor supersedes startIndex
Send both and the cursor wins.
A malformed cursor is a 400
Not a 500. Restart the walk with cursor=.
{ "message": "invalid cursor: not base64url", "status": 400 }

Cursors are opaque and the encoding may change; do not parse them.

Bulk downloads

Deep pagination is a poor bulk-download mechanism however it is tuned, which is why NVD publishes bulk feeds too. To pull a whole corpus use /v3/backup/{index} where {index} is set to your desired dataset depending on which endpoint you are using. This is then one download rather than hundreds of requests.