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 (may be null) |
history | array | Array of historical snapshots |
totalChanges | number | Total number of recorded changes |
History Snapshot Object
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO 8601 timestamp of the change |
changeType | string | Type of change: created, updated, severity_changed |
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 (
cve_audit_YYYY_MM) - 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 indexed before the log existed, or never indexed at all, has none: history is then [], totalChanges is 0 and currentState is null. 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.
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