API v1
The machine-readable description is the OpenAPI 3.1 document at
/api/v1/openapi.json. It needs no key, and it is generated
from the same schemas the route handlers read their limits and allowed values from,
and every response in the test suite is checked against it — so it is the reference
to generate a client from. This page explains the same API in prose.
Before you start
Creating an API key needs the Growth or Partner plan, and the owner or an admin role. A key that already exists keeps working after a downgrade, so a build does not break; only creating a new one is refused. The same applies to the MCP endpoint, the release gate and the Dependency-Track import, which all ride on the same integrations entitlement. See Generate and scope an API key.
Authentication
Every request carries an API key as a bearer token:
Authorization: Bearer dcl_xxxxxxxxxxxxxxxxxxxxxxxx
Keys are shown in full exactly once, at creation. After that, Declara stores only a SHA-256 hash — it cannot show you the secret again, and support cannot look it up for you. If you lose it, revoke the key and mint a new one.
Scopes
A key is created with one or more scopes, and a request fails with 403 if the
key's scopes don't cover the endpoint:
| Scope | Grants |
|---|---|
sboms:write | POST /api/v1/products/{id}/sboms |
alerts:read | GET /api/v1/alerts |
cases:read | GET /api/v1/cases, GET /api/v1/advisories |
products:read | GET /api/v1/products, GET /api/v1/products/{id}/vex, GET /api/v1/products/{id}/gate |
Endpoints
GET /api/v1/alerts
Lists alerts. Defaults to status=open.
Query parameters: status (open, monitoring, closed, all), tier (A,
B, C), cursor, limit.
Each alert includes its product, its advisory (id, summary, CVSS, EPSS, whether it's in KEV), and a fixed note: "An alert means a component you ship appears in an exploitation catalogue. It is a reason to assess, not a reportable event."
GET /api/v1/cases
Lists cases with their deadlines.
Query parameters: state (any of the case states — see
The reporting clock, explained), cursor,
limit.
Each case includes awareAt, its stored deadlines for early warning,
notification, and final report (null until the relevant clock has a starting
point), the submission timestamp for each stage that's been filed, and
usersNotifiedAt. Practice cases (drills) are never
returned.
GET /api/v1/advisories
Lists security advisories, newest first.
Query parameters: cursor, limit.
Each advisory includes its CSAF tracking id, the case it was drafted from, how many
revisions have been published, whether it is listed on your public disclosure page,
and csaf: the published CSAF 2.0 document exactly as it is served. A draft that has
never been published has csaf: null; the draft behind a published revision is
never returned.
GET /api/v1/products
Lists products and their versions, so CI can resolve a product name to an id.
Query parameters: cursor, limit.
Each product includes its versions, each version's support status, and whether it has an active SBOM.
GET /api/v1/products/{id}/vex
A VEX document for one product, built on request from your triage decisions and the VEX statements you imported. An imported statement wins over a triage decision for the same vulnerability.
Query parameters: format (openvex, the default, or cyclonedx), include_open
(true to add alerts still being assessed, as under_investigation).
A decision of not affected becomes not_affected, with component_not_present or
vulnerable_code_not_in_execute_path where the triage answers say so, and your
rationale as the impact statement otherwise. A reportable decision becomes affected.
Product versions are named by synthetic pkg:generic package URLs; matched components
keep their own.
GET /api/v1/products/{id}/gate
The release gate for one version: does it ship with a known exploitable vulnerability
(Cyber Resilience Act, Annex I Part I(2)(a))? Requires products:read.
Query parameter: ?version= (required), the release label, URL-encoded. The same answer
is served at GET /api/v1/products/{id}/versions/{version}/gate with the label as a path
segment, encoded (/ as %2F); prefer the query form, because some proxies refuse an
encoded slash in a path.
{
"result": "blocked",
"blockers": [{ "advisoryId": "CVE-2021-44228", "purl": "pkg:maven/...", "signal": "kev", "alertDecision": null }],
"citation": "Annex I Part I(2)(a)",
"note": "A known exploitable vulnerabilities signal, not a conformity verdict.",
"evaluatedAt": "2026-10-09T10:00:00.000Z",
"matchedAt": "2026-10-09T09:58:00.000Z",
"parsedAt": "2026-10-09T09:50:00.000Z",
"versionSupported": true,
"latestUploadFailed": false,
"lastDecision": null
}
resultispass,blockedorpending.pendingmeans matching has not yet run on the version's newest SBOM, or the version has no SBOM; poll again. It is never a pass.versionSupported: falsemeans the version is no longer supported. Matching skips such versions, so the result may rest on stale matches.latestUploadFailed: truemeans the newest SBOM did not parse, so the result is about the one before it.- A product or version in another workspace is
404 product_not_found, exactly as one that does not exist. A missing or malformedversionis400.
blockers[]
Empty unless result is blocked. One entry per advisory and component that blocks.
| Field | Type | Meaning |
|---|---|---|
advisoryId | string | The advisory, for example a CVE id |
purl | string | The component's package URL |
signal | kev | euvd | tierA | Why it blocks: in CISA's KEV catalogue, on ENISA's EUVD exploited list, or a Tier A match. When more than one applies, the first in that order is given |
alertDecision | string | null | The triage decision on the product's alert for the advisory, if any |
A match does not block when a VEX statement suppresses it, when your own security advisory for it lists this version as fixed, or when the alert was decided not applicable.
lastDecision
The newest decision recorded in the app for this version, or null.
| Field | Type | Meaning |
|---|---|---|
result | pass | blocked | acknowledged | What was recorded. acknowledged means someone recorded releasing over a block |
decidedAt | string | When, ISO 8601 UTC |
decidedBy | user | api | Whether a person or an API key recorded it |
Neither the reason nor a name is returned. Recording an acknowledgement is done in the app, not through the API.
See Gate a release in CI.
POST /api/v1/products/{id}/sboms
Pushes an SBOM for a specific version. Requires sboms:write.
Query parameter: ?version= (required) — the release this document belongs to. A
version that hasn't been seen before is created automatically. It may be 1 to 100
characters of letters, digits and . _ + ~ : / @ -, starting with a letter or digit
— the shape of a git tag or a package version. Anything else is 400 invalid_version.
Body: the raw SBOM document, up to 4 MiB (4,194,304 bytes — measured in bytes, so a
document of multibyte characters reaches the limit sooner than its character count
suggests). Larger documents must go through the browser upload, which writes straight
to storage rather than through this endpoint, and are refused with 413 body_too_large.
Response: 202 Accepted with an uploadId — parsing runs in the background.
If storage or the parsing queue is unavailable, the answer is 503 with
storage_unavailable or queue_unavailable and a Retry-After header. Nothing is
recorded in that case, so retrying is safe.
Retrying safely with Idempotency-Key
A push that timed out on your side may have succeeded on ours. Send an
Idempotency-Key header — a fresh UUID per upload, the same value on each retry of
that upload — and a retry gets the first answer back instead of uploading twice:
KEY=$(uuidgen)
curl --retry 3 -X POST \
-H "Authorization: Bearer $DECLARA_API_KEY" \
-H "Idempotency-Key: $KEY" \
--data-binary @sbom.json \
"https://your-declara-host/api/v1/products/$PRODUCT_ID/sboms?version=$VERSION"
- A key is remembered for 24 hours, per API key: two pipelines with their own keys can use the same value without colliding.
- A retry of the same request replays the stored status and body, with the header
Idempotent-Replayed: true. Nothing is uploaded or parsed again. - The same key with a different request — another body, version or product — is
409 idempotency_key_reused. Use a new key for a new upload. - A retry while the first request is still running is
409 idempotency_in_progresswithRetry-After: 1. - A
5xxis not remembered, so retrying it runs the upload for real. A4xxis remembered, because sending the same request again would not change it. - The value must be 1 to 255 printable ASCII characters; anything else is
400 invalid_idempotency_key. - Without the header, every request is a new upload.
MCP endpoint
The same data is available to AI assistants over the Model Context Protocol at
https://your-declara-host/api/mcp, authenticated with the same API keys and scopes. It
is read-only and its lists come from the same queries as the endpoints above, so the two
never disagree.
Connecting needs a key with products:read; without it every request is 403. Each
tool then checks its own scope, and a tool whose scope the key lacks refuses and names
it while the others keep working:
| Tool | Scope | Same answer as |
|---|---|---|
list_products | products:read | GET /api/v1/products |
get_vex | products:read | GET /api/v1/products/{id}/vex |
get_release_gate | products:read | GET /api/v1/products/{id}/gate |
get_support_status | products:read | The product's support period, per version |
list_alerts | alerts:read | GET /api/v1/alerts |
list_cases | cases:read | GET /api/v1/cases |
get_case_deadlines | cases:read | One case's stored deadlines, with citations |
list_security_advisories | cases:read | GET /api/v1/advisories |
Errors
Every error response has the same shape:
{ "error": { "code": "invalid_request", "message": "..." } }
Match on code, not on the message text — the wording is free to improve.
| Status | Codes |
|---|---|
400 | invalid_request (a filter, cursor or query parameter), version_required, invalid_version, empty_body, invalid_idempotency_key |
401 | unauthorized — no key, not a dcl_ bearer token, or unknown, revoked or expired |
403 | unauthorized — missing scope (the message names it), closed workspace, or a write to a read-only workspace |
404 | product_not_found — the same answer whether the product does not exist or belongs to another workspace |
409 | idempotency_key_reused, idempotency_in_progress (with Retry-After) |
413 | body_too_large |
429 | rate_limited, with Retry-After |
500 | internal_error — something failed on our side; the message says nothing about what |
503 | storage_unavailable, queue_unavailable, with Retry-After |
Error messages never repeat the value you sent.
Pagination
List endpoints use cursor pagination, newest first: pass limit (default 50, max
200) and cursor (from a previous response's nextCursor). nextCursor is null
on the last page.
limit is lenient: a value that is not a positive number falls back to 50, a
fraction is rounded down, and anything above 200 is treated as 200. cursor is
strict: anything that is not a cursor this API issued is 400 invalid_request.
Rate limits
Each API key may make 120 requests per minute. Past that, the API answers 429
with the code rate_limited and a Retry-After header giving the seconds to wait.
The limit is per key, so two CI pipelines with their own keys do not share it.
Usage metering
Every request is metered per API key, per day. This is recorded for visibility — it is not currently billed on. Don't read a usage number as a charge.