Read any CVE record, run the version check and record a decision from a script. The answers are the CVE page's own facts and verdicts, as JSON. The API is in every plan, Community included.
Sign in, free accounts included, and make a key on your API keys page. Name it for where it will live. The key is shown once: we keep only a hash of it, so copy it then. Revoke a key on the same page and calls with it are refused from then on. An account holds up to 20 keys at a time, and a key made by a teammate belongs to the team's plan.
Send the key on every call:
Authorization: Bearer vciy_<your key>
Calls are counted per clock hour, UTC. The allowance is your account's, shared by every key on it.
| Plan | Calls an hour |
|---|---|
| Community | 100 calls an hour |
| Practitioner | 1,000 calls an hour |
| Advisor | 1,000 calls an hour |
| Custody | As licensed |
Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds), so a script can pace itself. A call over the allowance is refused with HTTP 429 and a Retry-After header giving the seconds until the hour turns. A refused call does not count. Custody carries no limit headers, because it has no hourly number. The service limits state the same.
GET /api/v1/cve/{id}
curl -s https://app.vciy.com/api/v1/cve/CVE-2021-44228 \ -H "Authorization: Bearer $VCIY_KEY"
The answer holds what the CVE page shows: CISA's KEV listing, deadline and required action; the EPSS score, percentile and the date it was read; CVSS from the CNA and from NVD; the weakness class from both; the affected product and its published version ranges; the fixed version; public exploit code; references; and the dates. receipts maps each field to the receipt the page links beside it. A value the record does not hold is left out, never sent as zero. Abridged:
{
"cve": "CVE-2021-44228",
"url": "https://vciy.com/cve/CVE-2021-44228",
"kev": {
"listed": true,
"date_added": "2021-12-10",
"due_date": "2021-12-24",
"ransomware": "known"
},
"epss": {
"score": 0.99999,
"percentile": 1,
"as_of": "2026-06-15",
"model": "v2026.06.15"
},
"cvss": {
"nvd": {
"score": "10",
"vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
}
},
"cwe": {
"cna": "CWE-502 Deserialization of Untrusted Data; CWE-400 Uncontrolled Resource Consumption; CWE-20 Improper Input Validation",
"nvd": "CWE-20; CWE-400; CWE-502; CWE-917"
},
"affected": {
"product": "Apache Software Foundation Apache Log4j2",
"ranges": ["apache log4j >= 2.0.1 < 2.3.1", "apache log4j >= 2.4.0 < 2.12.2", "apache log4j >= 2.13.0 < 2.15.0"]
},
"fixed": "2.3.1; 2.12.2; 2.15.0",
"exploit_code": {
"repositories": "418",
"first_seen": "2021-12-09"
},
"dates": {
"published": "2021-12-10T00:00:00.000Z",
"last_change": "2026-09-04",
"build": "2026-09-20"
},
"receipts": {
"kev_listed": "https://app.vciy.com/app/receipt?e=CVE-2021-44228&f=kev_listed&p=volatile"
}
}
GET /api/v1/cve/{id}/check?version=&product=
curl -s "https://app.vciy.com/api/v1/cve/CVE-2024-23113/check?version=7.2.4&product=Fortinet+FortiOS" \ -H "Authorization: Bearer $VCIY_KEY"
The same check the CVE page runs, with the same sentence. verdict is one of three words:
| verdict | What it means |
|---|---|
affected | The version is inside a range this record publishes for that product. matched_range names it. |
not-affected | The version is outside every range this record publishes for that product. It is a statement about the record, not a promise the version is safe. |
unknown | The record cannot place the version: no range names the product, the version cannot be compared, or the record publishes more ranges than are held. sentence says which. |
When a record names more than one product, the answer lists them in products. Pass the one you run as product=, spelled as listed. With none named, only affected products are reported, in per_product, and anything else is unknown: nothing is cleared for a product you did not name.
{
"cve": "CVE-2024-23113",
"version": "7.2.4",
"product": "Fortinet FortiOS",
"verdict": "affected",
"sentence": "7.2.4 is inside an affected range this record publishes for Fortinet FortiOS: >= 7.2.0, <= 7.2.6",
"matched_range": "fortinet fortios >= 7.2.0 <= 7.2.6",
"products": ["Fortinet FortiSwitchManager", "Fortinet FortiOS", "Fortinet FortiPAM", "Fortinet FortiProxy"],
"url": "https://app.vciy.com/cve/CVE-2024-23113/check?v=7.2.4&p=Fortinet+FortiOS"
}
POST /api/v1/records
curl -s https://app.vciy.com/api/v1/records \
-H "Authorization: Bearer $VCIY_KEY" \
-H "Idempotency-Key: deploy-4812" \
-H "Content-Type: application/json" \
-d '{
"cve": "CVE-2021-44228",
"verdict": "patch_by",
"rationale": "Not internet facing. Patching in the March window.",
"product": "Apache Log4j2",
"version": "2.14.1",
"review_by": "2026-12-01"
}'
This makes the same decision record the web form makes, through the same code, for your organisation: you make the call, we date it and freeze the evidence the index holds for that CVE at that moment. The record is permanent. Its proof link opens to your organisation and to nobody else until you share it.
| Field | What it holds |
|---|---|
cve | Required. The CVE identifier. |
verdict | Required. One of accept, defer, patch_by, not_affected, mitigated, investigation or investigation_closed. |
rationale | Required. Why you made the call, up to 2,000 characters. |
product, version | Optional. What the call is about, up to 200 and 64 characters. Written into the record's note. |
review_by | Optional. A date in the form 2026-12-01. The record's review date: a record past it is listed as overdue, and on a patch_by call we tell you if CISA's deadline moves before it. |
The answer is HTTP 201:
{
"id": "6f1c0a52-9d7e-4a63-b1f4-0c52d8e7a911",
"url": "https://app.vciy.com/api/v1/records/6f1c0a52-9d7e-4a63-b1f4-0c52d8e7a911",
"proof_url": "https://app.vciy.com/r/6f1c0a52-9d7e-4a63-b1f4-0c52d8e7a911",
"version": 1,
"created_at": "2026-10-01T09:30:00Z",
"decider_source": "key_creator"
}
decider_source says who the record names as having made the call. key_creator means the person who made the key. org_owner means the key was made before we kept its maker, so the account owner is named. The record is held by your organisation either way.
A record counts against your plan's record limit. At the limit the call is refused with HTTP 402 and nothing is saved. Every record you hold stays readable. A call counts against the hourly allowance above, like any other.
Retrying safely. Send an Idempotency-Key header, up to 128 characters. A retry with the same key and the same body returns the record the first call made, with Idempotent-Replay: true, and makes no second record. The same key with a different body is refused with HTTP 422. Keys are per account and are held for 24 hours. A restart of the service clears them, so a retry after one can make a second record.
GET /api/v1/records/{id}
curl -s https://app.vciy.com/api/v1/records/6f1c0a52-9d7e-4a63-b1f4-0c52d8e7a911 \ -H "Authorization: Bearer $VCIY_KEY"
Returns id, cve, verdict, the current version, status, updated_at, url and proof_url. You can read your own organisation's records only. An id that belongs to another organisation and an id that does not exist answer the same HTTP 404.
A refusal is JSON with a code to switch on and a sentence to read: {"error": "rate_limited", "message": "..."}.
| Status | error | When |
|---|---|---|
| 400 | bad_id | The path does not hold a CVE identifier. |
| 400 | no_version | A check was sent without version=. |
| 400 | bad_body | A record call whose body is not a JSON object with only the fields above. |
| 400 | bad_verdict | verdict is not one of the five. The message lists them. |
| 400 | no_rationale | rationale is empty. |
| 400 | rationale_too_long | rationale is over 2,000 characters. |
| 400 | bad_review_by | review_by is not a date in the form 2026-12-01. |
| 400 | bad_idempotency_key | Idempotency-Key is over 128 characters. |
| 401 | no_key | No Authorization: Bearer header. |
| 401 | bad_key | The key is not valid, or was revoked. |
| 402 | record_cap | Your plan's records are all used. Nothing was saved. |
| 404 | not_found | Your organisation holds no record with that id. |
| 404 | not_held | The current build holds no record for that CVE. A record published since then appears after the next build. |
| 422 | idempotency_mismatch | The Idempotency-Key was already used for a different record. |
| 422 | evidence_not_freezable | The evidence held for that CVE cannot be frozen into a record as it stands. Retrying will not change that. Nothing was saved. |
| 429 | rate_limited | This hour's allowance is used. Wait the Retry-After seconds. |
| 503 | warming_up | The record index is loading after a restart or a new build. Retry after Retry-After. |
| 503 | unavailable | The key or the record could not be read just now. Retry after Retry-After. |
Every CVE answer is read from the same build the CVE pages are rendered from, so the API and the page never disagree. dates.build is the day of that build. CISA's catalogue and EPSS are refreshed daily upstream and reach the API with the next build. The as-of control, which reconstructs a record on a past date, stays on the page.