CVE History
Query the change history of a CVE or retrieve its state at a specific point in time. Useful for audit trails and tracking how vulnerability severity evolved.
Get CVE History
Retrieve retained change snapshots for a CVE from the last 24 months. This is not a complete upstream history.
Snapshot sources describe the record at that observation, not every source currently available. Older snapshots affected by a source-recording bug may have an empty list. When the same CVE's archived record has the exact snapshot hash, the response recovers its sources and includes sourcesRecovery with basis: "matching_archived_record", hash and historyKey. The original audit entry, observation time and hash are not rewritten. Verified recoveries are stored separately by CVE and snapshot hash for two years, so later archive updates do not erase them. Recovery checks cover retained missing-source hashes in batches of at most 100 keys, with at most four concurrent bulk lookups.
If the archived version cannot be matched or read, sources stay empty. The service never copies today's index sources into an older snapshot. Empty sources mean provenance was not recorded or could not be recovered, not that the vulnerability has no authoritative source.
Endpoint
GET /api/v1/cve/:id/historyParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | CVE identifier (e.g., CVE-2024-45490) |
Response
{
"success": true,
"data": {
"cveId": "CVE-2024-45490",
"currentState": {
"id": "CVE-2024-45490",
"severity": "high",
"cvssScore": 8.1,
"state": "PUBLISHED",
"publishedAt": "2024-09-15T12:00:00Z",
"hash": "abc123...",
"sources": ["mitre", "nvd"]
},
"history": [
{
"timestamp": "2024-09-15T12:00:00Z",
"changeType": "created",
"hash": "def456...",
"severity": "medium",
"cvssScore": 5.3,
"state": "PUBLISHED",
"sources": ["mitre"]
},
{
"timestamp": "2024-09-20T08:30:00Z",
"changeType": "updated",
"hash": "abc123...",
"severity": "high",
"cvssScore": 8.1,
"state": "PUBLISHED",
"sources": ["mitre", "nvd"],
"changes": [
{ "field": "severity", "from": "medium", "to": "high" },
{ "field": "cvssScore", "from": 5.3, "to": 8.1 }
]
}
],
"totalChanges": 2
},
"timestamp": "2026-02-06T14:36:33.927Z"
}Response Fields
| Field | Type | Description |
|---|---|---|
cveId | string | The CVE identifier |
currentState | object | Current state, from the CVE index or (when the index lacks an entry) the record's own direct pointer; null only when no archived record exists |
history | array | Array of historical snapshots |
totalChanges | number | Total number of recorded changes |
partial | boolean | Present and true only when a monthly audit bucket could not be read; failedBuckets names them (newest first) and note says the list may be incomplete |
A response without partial read every month in the window. With partial, history holds only what could be read, so an empty list is not evidence that the CVE never changed.
History Snapshot Object
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO 8601 timestamp of the change |
changeType | string | Type of change: created or updated. A later update that happens to change severity is still updated -- there is no distinct value for it |
hash | string | SHA-256 hash of the CVE data at this point |
severity | string | Severity level at this point |
cvssScore | number | CVSS score at this point |
state | string | CVE state (PUBLISHED, REJECTED, etc.) |
sources | array | Data sources at this point |
changes | array | (Optional) Specific field changes from previous state |
Query CVE at Date
Retrieve the state of a CVE at a specific point in time.
Endpoint
GET /api/v1/cve/:id/at/:dateParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | CVE identifier (e.g., CVE-2024-45490) |
date | string | Yes | Target date in ISO 8601 format (e.g., 2025-01-01 or 2025-01-01T12:00:00Z) |
Response
{
"success": true,
"data": {
"found": true,
"cveId": "CVE-2024-45490",
"targetDate": "2025-01-01T00:00:00.000Z",
"snapshotDate": "2024-12-15T08:30:00.000Z",
"state": {
"timestamp": "2024-12-15T08:30:00.000Z",
"changeType": "updated",
"hash": "abc123...",
"severity": "high",
"cvssScore": 8.1,
"state": "PUBLISHED",
"sources": ["mitre", "nvd"]
},
"totalSnapshots": 5
},
"timestamp": "2026-02-06T14:40:00.000Z"
}Response Fields (at date)
| Field | Type | Description |
|---|---|---|
found | boolean | Whether a snapshot was found for the date |
cveId | string | The CVE identifier |
targetDate | string | The requested date |
snapshotDate | string | Date of the closest snapshot on or before target |
state | object | The CVE state at that point |
totalSnapshots | number | Total historical snapshots available |
earliestKnown | string | (If not found) Earliest known date for this CVE, when every snapshot postdates the target |
partial | boolean | (If not found) Present and true only when an audit bucket could not be read; failedBuckets names them and note says the absence is not evidence |
A plain found: false with neither earliestKnown nor partial is a genuine absence from the audit log. Buckets begin in 2020, so an earlier date is read from there.
Examples
# Get full history for a CVE
curl "https://api.vulnpatch.dev/api/v1/cve/CVE-2024-45490/history"
# Query CVE state on January 1st, 2025
curl "https://api.vulnpatch.dev/api/v1/cve/CVE-2024-45490/at/2025-01-01"
# Query with specific time
curl "https://api.vulnpatch.dev/api/v1/cve/CVE-2024-45490/at/2025-01-01T12:00:00Z"Code Examples
async function getCVEHistory(cveId) {
const response = await fetch(
`https://api.vulnpatch.dev/api/v1/cve/${cveId}/history`
);
const { data } = await response.json();
console.log(`CVE: ${data.cveId}`);
console.log(`Total changes: ${data.totalChanges}`);
for (const snapshot of data.history) {
console.log(`[${snapshot.timestamp}] ${snapshot.changeType}: severity=${snapshot.severity}`);
if (snapshot.changes) {
for (const change of snapshot.changes) {
console.log(` - ${change.field}: ${change.from} -> ${change.to}`);
}
}
}
return data;
}
async function getCVEAtDate(cveId, date) {
const response = await fetch(
`https://api.vulnpatch.dev/api/v1/cve/${cveId}/at/${date}`
);
const { data } = await response.json();
if (data.found) {
console.log(`CVE ${cveId} on ${date}: severity=${data.state.severity}`);
} else {
console.log(`CVE not found at ${date}, earliest: ${data.earliestKnown}`);
}
return data;
}
getCVEHistory('CVE-2024-45490');
getCVEAtDate('CVE-2024-45490', '2025-01-01');import requests
def get_cve_history(cve_id):
url = f"https://api.vulnpatch.dev/api/v1/cve/{cve_id}/history"
response = requests.get(url)
data = response.json()['data']
print(f"CVE: {data['cveId']}")
print(f"Total changes: {data['totalChanges']}")
for snapshot in data['history']:
print(f"[{snapshot['timestamp']}] {snapshot['changeType']}: severity={snapshot['severity']}")
if 'changes' in snapshot:
for change in snapshot['changes']:
print(f" - {change['field']}: {change['from']} -> {change['to']}")
return data
def get_cve_at_date(cve_id, date):
url = f"https://api.vulnpatch.dev/api/v1/cve/{cve_id}/at/{date}"
response = requests.get(url)
data = response.json()['data']
if data['found']:
print(f"CVE {cve_id} on {date}: severity={data['state']['severity']}")
else:
print(f"CVE not found at {date}, earliest: {data.get('earliestKnown')}")
return data
get_cve_history('CVE-2024-45490')
get_cve_at_date('CVE-2024-45490', '2025-01-01')Use Cases
- Audit trails: Track how a CVE's severity evolved over time
- Compliance reporting: Document CVE state at specific compliance checkpoints
- Incident response: Determine what was known about a CVE at incident time
- Research: Analyze patterns in CVE severity changes
Data Retention
- Historical snapshots are retained for 2 years
- Data is stored in monthly buckets sharded by CVE id (
cve_audit_YYYY_MM_sN, 8 shards); an unsuffixedcve_audit_YYYY_MMbucket, where one exists, holds older snapshots and is read alongside the shards - History is recorded automatically when CVEs are updated during sync
What an empty history means
The audit log records a snapshot when the indexed record changes. A CVE indexed once and never revised has at most one snapshot, and one never indexed at all has none: history is then [] and totalChanges is 0. That is a statement about this service's log, not about upstream activity. For the upstream payloads themselves, hashed per source and diffed field by field, use GET /api/v1/cve/:id/provenance.
currentState is read from the archived record directly (by its own pointer) when the CVE index itself has no entry for it, so a CVE whose index entry is missing is not the same as one with no archived record — history can be empty while currentState is still populated. It is null only when no archived record for the id exists at all.
Both routes are in the spec as /cve/{id}/history and /cve/{id}/at/{date}.
Related Endpoints
GET /api/v1/cve/:id- Get current CVE detailsGET /api/v1/cve/:id/provenance- What each upstream source said, and when it changedPOST /api/v1/cve/batch- Bulk CVE lookupGET /api/v1/analytics- CVE analytics aggregates