Vandorisk API
Two endpoints, meant for one job: screen an SBOM from your build pipeline, write the result into the product’s technical file as dated evidence, and let the build fail when a high-severity finding has nobody’s name against it.
The API runs the samescreening pipeline as the upload button in the assessment wizard, writes the same evidence line to Annex I Part I(2), and stores the same scan record. A pipeline scan and a hand upload mean the same thing in the file — that is the point of having the API at all.
Base URL: https://www.vandorisk.com
Authentication
Create a key on the API keys page. It looks like vrk_live_… and is shown once, because only its SHA-256 hash is stored. Send it on every request:
Authorization: Bearer vrk_live_xxxxxxxxxxxxxxxxxxxxxxxx
A key belongs to one workspace. Every request is scoped to that workspace: an assessment id belonging to anyone else returns 404, which does not confirm that the id exists.
No cookies, and no Origin check.The browser routes are cookie-based and therefore refuse cross-origin mutations, because browsers attach cookies to cross-site requests automatically. A bearer token is never attached automatically by any browser — something has to hold the secret and put it on the request — so there is no confused deputy and no CSRF surface to close here. Enforcing an Origin rule would only break the legitimate caller: curl and CI runners send no Origin header at all.
Keep the key in your CI secret store, never in the repository. Revoking a key on the keys page stops it authenticating immediately.
Scopes
A key carries only what you tick. A scan key that cannot read assessments is a smaller blast radius if the runner leaks it.
| Scope | Grants |
|---|---|
sbom:scan | POST /api/v1/scan |
assessments:read | GET /api/v1/assessments/{id} |
A request with a valid key but the wrong scope returns 403 and names the scope it needed.
Rate limits and body limits
- 60 requests per minute per key. Over that you get
429with aRetry-Afterheader in seconds. The counter is per key, so one noisy pipeline cannot starve another. - 2 MB maximum request body (
413above it) and 3000 components per SBOM (400above it). - A scan holds the connection while OSV is queried; the whole screen is budgeted at 45 seconds, after which unfinished components are reported as not screened rather than silently as clean.
POST /api/v1/scan
Scope sbom:scan. Body: { assessmentId, sbom } where sbomis a parsed CycloneDX 1.4–1.6 or SPDX 2.2/2.3 JSON document (the object itself, not a string).
Each call: screens the components, stores an SbomScan record, and appends one dated evidence line to requirement I.2 of the assessment.
curl -sS -X POST https://www.vandorisk.com/api/v1/scan \
-H "Authorization: Bearer $VANDORISK_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"assessmentId\":\"$ASSESSMENT_ID\",\"sbom\":$(cat sbom.cdx.json)}"Response
{
"scan": {
"id": "clx…",
"createdAt": "2026-08-21T09:14:02.114Z",
"assessmentId": "clx…",
"format": "cyclonedx",
"componentCount": 412,
"screenedCount": 361,
"notScreenableCount": 51,
"unscannedCount": 0,
"findings": [
{ "id": "CVE-2021-23337", "severity": "HIGH", "score": 7.2, "component": "lodash" }
],
"findingsTruncated": false,
"evidenceLine": "SBOM scanned 2026-08-21: 412 components, 361 screened, 1 with known vulnerabilities (OSV), highest: CVE-2021-23337 (HIGH 7.2, lodash), 51 not screenable by OSV (busybox, …)"
},
"gate": {
"total": 1,
"high": 1,
"worst": { "id": "CVE-2021-23337", "severity": "HIGH", "score": 7.2, "component": "lodash" },
"untriagedHigh": 1
}
}notScreenableCountis not a failure and not a pass: those components carry no purl of a type OSV indexes, so OSV was never asked about them. An empty answer for them would be a false “clean”, so they are counted separately and named in the evidence line. Check EUVD or NVD for them by hand.
The gate object
Everything a pipeline needs to decide an exit code.
{
"total": 12, // advisory rows across all components
"high": 3, // of those, HIGH or CRITICAL
"worst": { // the single worst finding, CVE id when one exists
"id": "CVE-2021-23337",
"severity": "HIGH",
"score": 7.2,
"component": "lodash"
},
"untriagedHigh": 1 // HIGH/CRITICAL findings with no not_affected/fixed disposition
}Fail the build on untriagedHigh, not on high. Annex I Part I(2) is not “ship no CVEs” — it is a judgement per finding: this one cannot be reached in our product, that one is already fixed, this one is still open. In Vandorisk that judgement is a disposition recorded on the finding (VEX-style: not_affected, affected, fixed, under_investigation). not_affected will not save without a written justification, because that is the claim an assessor challenges first.
A finding dispositioned not_affected or fixed stops counting toward untriagedHigh. So the gate goes green when the team has actually answered the findings — not when the scanner happens to be quiet.
Dispositions are recorded in the assessment wizard’s SBOM panel. There is no API for writing them yet (see below).
GET /api/v1/assessments/{id}
Scope assessments:read. Returns the assessment summary and its readiness, scored by the same engine the wizard and the exported technical file use.
curl -sS https://www.vandorisk.com/api/v1/assessments/$ASSESSMENT_ID \ -H "Authorization: Bearer $VANDORISK_API_KEY"
{
"assessment": {
"id": "clx…",
"productName": "Sensor",
"versionLabel": "1.0",
"packVersion": "0.2.0",
"currentPackVersion": "0.2.0",
"category": "default",
"route": "self-assessment"
},
"readiness": {
"pct": 74,
"evidencedPct": 61,
"total": 31, "applicable": 29, "assessed": 24,
"met": 19, "metEvidenced": 12, "metAsserted": 7,
"partial": 3, "notMet": 2, "na": 2,
"blockers": ["Risk assessment incomplete (3/5 fields) — mandatory under Art. 13(2)-(3)."],
"gapCount": 7,
"orderedGaps": [
{ "requirementId": "P.4", "title": "…", "legalRef": "Art. 14", "state": "not met" }
]
},
"sbom": {
"lastScanAt": "2026-08-21T09:14:02.114Z",
"componentCount": 412, "screenedCount": 361, "notScreenableCount": 51,
"findingCount": 1, "highCount": 1,
"untriagedHigh": 1, "untriagedTotal": 1
}
}pct counts a “met” answer in full. evidencedPct counts a “met” with no evidence text at half weight — it is the more honest number to put on a dashboard, because a claim with nothing behind it is not a demonstrated requirement. blockersare hard stops under Art. 13(2)–(4) that cannot be scored away; treat a non-empty array as a failure.
Machine-readable description
The same two endpoints as an OpenAPI 3.1 document, served without a key because it documents a public API surface and contains no secrets:
curl -sS https://www.vandorisk.com/api/v1/openapi
/api/v1/openapi— request and response schemas (including the gate object), the bearer security scheme, the scope each operation needs as x-required-scope, and the 401/403/404/429 responses. Enough to generate a client, and enough to import into Postman or Insomnia.
It is hand-written and kept honest by a test that fails the build if it ever describes a path with no route behind it. It is not a replacement for this page: the reasoning about untriagedHigh and notScreenableCount lives here.
The rule pack the assessment engine evaluates is public in the same spirit: /api/pack serves the versioned requirement pack and its changelog as JSON (human-readable, with the citation index, at /pack).
CI example: GitHub Actions
Generates an SBOM, screens it, prints the evidence line into the job log and fails the build when a HIGH or CRITICAL finding has not been triaged. Needs jq, which is preinstalled on GitHub-hosted runners. Store the key as the repository secret VANDORISK_API_KEY and the assessment id as the variable VANDORISK_ASSESSMENT_ID.
# .github/workflows/cra-sbom.yml
name: CRA SBOM screen
on: [push]
jobs:
sbom-screen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Produce a CycloneDX SBOM however your stack does it. Examples:
# npm: npx @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json
# syft: syft dir:. -o cyclonedx-json=sbom.cdx.json
- name: Generate SBOM
run: npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json
- name: Screen it with Vandorisk
env:
VANDORISK_API_KEY: ${{ secrets.VANDORISK_API_KEY }}
ASSESSMENT_ID: ${{ vars.VANDORISK_ASSESSMENT_ID }}
run: |
jq -n --arg id "$ASSESSMENT_ID" --slurpfile sbom sbom.cdx.json \
'{assessmentId: $id, sbom: $sbom[0]}' > payload.json
http_code=$(curl -sS -o response.json -w '%{http_code}' \
-X POST https://www.vandorisk.com/api/v1/scan \
-H "Authorization: Bearer $VANDORISK_API_KEY" \
-H "Content-Type: application/json" \
--data @payload.json)
if [ "$http_code" != "200" ]; then
echo "::error::Vandorisk scan failed (HTTP $http_code)"
cat response.json
exit 1
fi
jq -r '.scan.evidenceLine' response.json
untriaged_high=$(jq -r '.gate.untriagedHigh' response.json)
not_screenable=$(jq -r '.scan.notScreenableCount' response.json)
if [ "$not_screenable" -gt 0 ]; then
echo "::warning::$not_screenable components could not be screened by OSV — check EUVD/NVD manually"
fi
if [ "$untriaged_high" -gt 0 ]; then
echo "::error::$untriaged_high untriaged HIGH/CRITICAL findings. Triage them in Vandorisk (not affected / fixed, with a reason) or fix the dependency."
exit 1
fiThe same shape works on any runner: POST, read .gate.untriagedHigh, exit non-zero when it is above zero.
Errors
Every error is JSON with an error string. Status codes:
400— malformed body, unparseable SBOM, or more than 3000 components.401— missing, malformed, unknown or revoked key. Revoked and wrong keys give the same message on purpose.403— valid key, missing scope.404— the assessment does not exist, or belongs to another workspace.413— body over 2 MB.422— the SBOM parsed but contained no components. Refused deliberately: a “0 components, clean” evidence line would read as diligence that never happened.429— over the per-key rate limit; seeRetry-After.
What is not here yet
Rather than let you find these out in an outage:
- No webhooks. Nothing calls you; poll
GET /api/v1/assessments/{id}if you need state. - No SDK for any language. There is an OpenAPI 3.1 document to generate one from, but nothing published or supported by us.
- No write API for dispositions, answers or documents. Triage and the questionnaire happen in the app. The API can screen and read; it cannot fill in your technical file.
- No listing endpoints and no pagination— you address one assessment by id.
- Scan history is not exposed over the APIyet; it is visible in the assessment’s SBOM panel.
- The rate limiter is per server instance and resets on deploy. Treat the 60/minute figure as the guarantee, not the ceiling.
- No API versioning policy beyond the
/v1prefix. Additive fields can appear in responses without notice; parse leniently and ignore what you do not recognise.
Vandorisk is guided self-assessment software. Screening an SBOM is evidence toward Annex I Part I(2); it is not a conformity assessment, and the manufacturer remains responsible for the Declaration of Conformity.