Skip to content
engineer

API v1

Was this page helpful?

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:

ScopeGrants
sboms:writePOST /api/v1/products/{id}/sboms
alerts:readGET /api/v1/alerts
cases:readGET /api/v1/cases, GET /api/v1/advisories
products:readGET /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
}
  • result is pass, blocked or pending. pending means 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: false means the version is no longer supported. Matching skips such versions, so the result may rest on stale matches.
  • latestUploadFailed: true means 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 malformed version is 400.

blockers[]

Empty unless result is blocked. One entry per advisory and component that blocks.

FieldTypeMeaning
advisoryIdstringThe advisory, for example a CVE id
purlstringThe component's package URL
signalkev | euvd | tierAWhy 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
alertDecisionstring | nullThe 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.

FieldTypeMeaning
resultpass | blocked | acknowledgedWhat was recorded. acknowledged means someone recorded releasing over a block
decidedAtstringWhen, ISO 8601 UTC
decidedByuser | apiWhether 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_progress with Retry-After: 1.
  • A 5xx is not remembered, so retrying it runs the upload for real. A 4xx is 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:

ToolScopeSame answer as
list_productsproducts:readGET /api/v1/products
get_vexproducts:readGET /api/v1/products/{id}/vex
get_release_gateproducts:readGET /api/v1/products/{id}/gate
get_support_statusproducts:readThe product's support period, per version
list_alertsalerts:readGET /api/v1/alerts
list_casescases:readGET /api/v1/cases
get_case_deadlinescases:readOne case's stored deadlines, with citations
list_security_advisoriescases:readGET /api/v1/advisories

See Connect an AI assistant.

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.

StatusCodes
400invalid_request (a filter, cursor or query parameter), version_required, invalid_version, empty_body, invalid_idempotency_key
401unauthorized — no key, not a dcl_ bearer token, or unknown, revoked or expired
403unauthorized — missing scope (the message names it), closed workspace, or a write to a read-only workspace
404product_not_found — the same answer whether the product does not exist or belongs to another workspace
409idempotency_key_reused, idempotency_in_progress (with Retry-After)
413body_too_large
429rate_limited, with Retry-After
500internal_error — something failed on our side; the message says nothing about what
503storage_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.