vciy
API version 1

The API

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.

01

Get a key

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>
02

Limits

Calls are counted per clock hour, UTC. The allowance is your account's, shared by every key on it.

PlanCalls an hour
Community100 calls an hour
Practitioner1,000 calls an hour
Advisor1,000 calls an hour
CustodyAs 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.

03

Read a record

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"
  }
}
04

Check a version

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:

verdictWhat it means
affectedThe version is inside a range this record publishes for that product. matched_range names it.
not-affectedThe 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.
unknownThe 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"
}
05

Record a decision

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.

FieldWhat it holds
cveRequired. The CVE identifier.
verdictRequired. One of accept, defer, patch_by, not_affected, mitigated, investigation or investigation_closed.
rationaleRequired. Why you made the call, up to 2,000 characters.
product, versionOptional. What the call is about, up to 200 and 64 characters. Written into the record's note.
review_byOptional. 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.

06

Read a 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.

07

Refusals

A refusal is JSON with a code to switch on and a sentence to read: {"error": "rate_limited", "message": "..."}.

StatuserrorWhen
400bad_idThe path does not hold a CVE identifier.
400no_versionA check was sent without version=.
400bad_bodyA record call whose body is not a JSON object with only the fields above.
400bad_verdictverdict is not one of the five. The message lists them.
400no_rationalerationale is empty.
400rationale_too_longrationale is over 2,000 characters.
400bad_review_byreview_by is not a date in the form 2026-12-01.
400bad_idempotency_keyIdempotency-Key is over 128 characters.
401no_keyNo Authorization: Bearer header.
401bad_keyThe key is not valid, or was revoked.
402record_capYour plan's records are all used. Nothing was saved.
404not_foundYour organisation holds no record with that id.
404not_heldThe current build holds no record for that CVE. A record published since then appears after the next build.
422idempotency_mismatchThe Idempotency-Key was already used for a different record.
422evidence_not_freezableThe evidence held for that CVE cannot be frozen into a record as it stands. Retrying will not change that. Nothing was saved.
429rate_limitedThis hour's allowance is used. Wait the Retry-After seconds.
503warming_upThe record index is loading after a restart or a new build. Retry after Retry-After.
503unavailableThe key or the record could not be read just now. Retry after Retry-After.
08

Where the answers come from

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.