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.
| You are | Use |
|---|---|
| Repointing an existing NVD integration | startIndex - unchanged, no code edit, works at any depth |
| Writing a new integration | cursor - 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) |
| Endpoint | Maximum resultsPerPage |
|---|---|
/rest/json/cves/2.0 | 2,000 |
/rest/json/cvehistory/2.0 | 5,000 |
/rest/json/cpes/2.0 | 10,000 |
/rest/json/cpematch/2.0 | 500 |
/rest/json/source/2.0 | 1,000 |
Omitting resultsPerPage gives you the maximum, matching NVD's behaviour.
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'
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.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.
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.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.
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.