Developer documentation
API and CLI reference
Push SBOMs, releases and evidence into your product-security record from CI: four REST endpoints, API keys with scopes, and a small command-line client that wraps them.
Authentication
API keys belong to a workspace. Whatever a key does happens in that workspace and is attributed to the key in the audit log and the evidence ledger.
Send the key in the Authorization header of every request: Authorization: Bearer pmk_…. Owners and admins create keys under Settings → API keys in the workspace; keys are included from the Starter plan. The secret is shown once and stored only as a salted hash, so copy it into your CI secret store straight away. Revoking a key stops it immediately.
Each key carries one or more scopes. A key for a CI pipeline that pushes SBOMs and creates the release on the fly needs sbom:write, sbom:read and release:write.
| Scope | Allows | Endpoints |
|---|---|---|
sbom:write | Upload SBOMs and evidence files | POST /api/sboms, POST /api/evidence |
sbom:read | Read SBOM processing status and list releases | GET /api/sboms/{sbomId}, GET /api/releases |
release:write | Create releases | POST /api/releases |
findings:read | Read findings | Reserved for the findings endpoint, not yet published |
The same routes accept a signed-in browser session, which is how the workspace’s own upload form uses them. Scripts and pipelines should always use a key.
Conventions, errors and limits
Requests and responses are JSON unless an endpoint says otherwise; ids are UUIDs; an error is one JSON object with a message.
The base URL is https://app.permenta.com. A product’s id is in the address bar of its page (/app/products/<id>); release ids come from GET /api/releases and SBOM ids from the upload response. Every error response has this shape, with the HTTP status code carrying the category:
{ "error": "API key lacks the release:write scope" }| Status | Meaning |
|---|---|
| 400 | The request is malformed: a missing header or field, an invalid id or value. The message names it. |
| 401 | No API key, a malformed one, or a key that was revoked. |
| 402 | A plan limit is reached, for example the monthly SBOM uploads of the Free plan. |
| 403 | The key lacks the scope the endpoint needs, or the signed-in user lacks the permission. |
| 404 | The product, release or SBOM does not exist or belongs to another workspace. |
| 409 | A release with that version already exists for the product. |
| 413 | The body exceeds the size limit of the endpoint. |
Limits
- An SBOM may be at most 25 MiB; an evidence file at most 50 MiB.
- SBOM uploads per calendar month depend on the plan: Free 1 upload a month, Starter unlimited, Growth unlimited and Scale unlimited. Uploading the same file again for the same product is not counted: the response is
200withduplicate: trueand the existing id. - A release version is unique per product (
409otherwise) and at most 80 characters; notes are at most 4000.
SBOMs
Upload a CycloneDX (JSON or XML) or SPDX (JSON) file, then poll its status while the worker parses, validates and matches it against vulnerability data.
POST /api/sboms
Scope sbom:write. Send the file as the raw request body with the metadata in headers, or as a multipart form. The format is detected from the content, not from the header or the file name.
| Name | Type | Description |
|---|---|---|
Content-Type | header | application/json or application/xml. |
x-permenta-product | header | The product id. Required. |
x-permenta-release | header | A release id to attach the SBOM to. Optional. |
x-permenta-filename | header | The file name to record; default sbom.json. |
| Name | Type | Description |
|---|---|---|
file | file | The SBOM. Required. |
productId | string | The product id. Required. |
releaseId | string | A release id. Optional. |
| Name | Type | Description |
|---|---|---|
sbomId | string | The id to poll and to show in the workspace. |
status | string | uploaded for a new file; the existing status for a duplicate. |
duplicate | boolean | True when this product already had a file with the same content. |
sha256 | string | Hex digest of the uploaded bytes. |
curl -sS -X POST "https://app.permenta.com/api/sboms" \
-H "Authorization: Bearer $PERMENTA_API_KEY" \
-H "x-permenta-product: <product id>" \
-H "x-permenta-release: <release id>" \
-H "x-permenta-filename: bom.json" \
-H "content-type: application/json" \
--data-binary @bom.jsonpermenta sbom push bom.json --product <product id> --release-version 2.4.1 --waitGET /api/sboms/{sbomId}
Scope sbom:read. Poll every couple of seconds until status is parsed or failed; parsing normally completes within a minute. Validation follows the BSI TR-03183-2 minimum content: warnings do not block monitoring but weaken the evidence, errors mean the SBOM should be regenerated.
| Name | Type | Description |
|---|---|---|
sbomId | string | The id. |
status | string | uploaded, parsing, parsed or failed. |
format | string | null | cyclonedx-json, cyclonedx-xml or spdx-json once parsed. |
specVersion | string | null | The specification version the file declares. |
componentCount | number | Components found. |
matchableCount | number | Components with a package URL that can be matched against vulnerability data. |
validation | array | Issues, each with code, message, severity (error or warning) and an optional path. |
error | string | null | Why parsing failed. |
parsedAt | string | null | When parsing finished (ISO 8601). |
matchedAt | string | null | When vulnerability matching last ran; null until it has. |
| Status | Meaning |
|---|---|
uploaded | Stored and queued; the worker usually picks it up within a minute. |
parsing | Being read and normalised. |
parsed | Components stored, validation done, vulnerability matching queued or done. |
failed | Could not be parsed; `error` says why. Fix the file and upload it again. |
curl -sS "https://app.permenta.com/api/sboms/<sbom id>" \
-H "Authorization: Bearer $PERMENTA_API_KEY"permenta sbom status <sbom id>Releases
A release is a version of a product. SBOMs and findings attach to it, and the support and security-update record refers to it.
POST /api/releases
Scope release:write. The body is a JSON object; the response is 201 Created with releaseId and version.
| Name | Type | Description |
|---|---|---|
productId | string | The product id. Required. |
version | string | The version, unique per product, at most 80 characters. Required. |
releasedAt | string | The release date as YYYY-MM-DD. Optional. |
isSecurityRelease | boolean | Whether it is a security update. Default false. |
isSupported | boolean | Whether it is still supported. Default true. |
notes | string | Free text, at most 4000 characters. Optional. |
curl -sS -X POST "https://app.permenta.com/api/releases" \
-H "Authorization: Bearer $PERMENTA_API_KEY" \
-H "content-type: application/json" \
-d '{ "productId": "<product id>", "version": "2.4.1", "releasedAt": "2026-09-24", "isSecurityRelease": true }'permenta release create --product <product id> --version 2.4.1 --date 2026-09-24 --securityGET /api/releases?productId={productId}
Scope sbom:read. Lists the product’s releases, newest first, as { "releases": [ … ] }. The CLI uses it to turn --release-version into a release id.
| Name | Type | Description |
|---|---|---|
releaseId | string | The id to pass as x-permenta-release. |
version | string | The version. |
releasedAt | string | null | YYYY-MM-DD, or null when no date was recorded. |
isSecurityRelease | boolean | Whether it is a security update. |
isSupported | boolean | Whether it is still supported. |
curl -sS "https://app.permenta.com/api/releases?productId=<product id>" \
-H "Authorization: Bearer $PERMENTA_API_KEY"Evidence
Test reports, risk assessments, policies, screenshots and exports go into the product’s evidence ledger, hashed and linked to the obligations they support.
POST /api/evidence
Scope sbom:write. A multipart form; the response is 201 Created with evidenceId and sha256. Obligation keys are the identifiers shown on the product’s obligations page, for example annex1-ii-3-regular-security-testing or annex1-ii-1-sbom.
| Name | Type | Description |
|---|---|---|
file | file | The file, at most 50 MiB. Required. |
productId | string | The product id. Required. |
title | string | How the entry is listed, at most 200 characters. Default: the file name. |
kind | string | document, test_report, risk_assessment, policy, screenshot, export, link or other. Default document. |
description | string | At most 2000 characters. Optional. |
obligationKeys | string | Comma-separated obligation keys the evidence supports. Optional. |
curl -sS -X POST "https://app.permenta.com/api/evidence" \
-H "Authorization: Bearer $PERMENTA_API_KEY" \
-F "file=@pentest-2026-09.pdf" \
-F "productId=<product id>" \
-F "title=Penetration test, Q3 2026" \
-F "kind=test_report" \
-F "obligationKeys=annex1-ii-3-regular-security-testing"permenta evidence push pentest-2026-09.pdf --product <product id> --title "Penetration test, Q3 2026" --kind test_report --obligations annex1-ii-3-regular-security-testingCommand-line client
@permenta/cli wraps the endpoints above for pipelines: no runtime dependencies, Node 24 or later, exit codes a CI job can act on.
Run it with npx @permenta/cli or pnpm dlx @permenta/cli, or install it globally with npm install --global @permenta/cli. Configure it with PERMENTA_API_KEY (or --api-key) and, for anything other than the hosted service, PERMENTA_URL. Every command takes --json for one machine-readable document on stdout and --help.
permenta sbom push <file> --product <id> [--release <id> | --release-version <version>] [--filename <name>] [--wait]
permenta sbom status <sbomId>
permenta release create --product <id> --version <version> [--date YYYY-MM-DD] [--security] [--unsupported] [--notes <text>]
permenta evidence push <file> --product <id> --title <text> [--kind <kind>] [--obligations <key,key>]
permenta whoami [--product <id>]| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Usage error: unknown command or option, missing file or key, invalid id. |
| 2 | The request could not be completed: network error, timeout, a 5xx response, or --wait ran out of time (10 minutes). |
| 3 | The server rejected the request (a 4xx status, see above), or the SBOM failed to parse. |
In a pipeline
Generate the SBOM with the tool of your ecosystem (cdxgen, syft, Trivy, the CycloneDX plugins), then push it with the release’s tag as the version. The key lives in the CI secret store; the product id is a plain variable.
- name: Push the SBOM to Permenta
env:
PERMENTA_API_KEY: ${{ secrets.PERMENTA_API_KEY }}
run: >-
npx --yes @permenta/cli sbom push bom.json
--product ${{ vars.PERMENTA_PRODUCT_ID }}
--release-version ${{ github.ref_name }}
--waitQuestions about the API, or an endpoint you need that is not here? Write to hello@permenta.com. Permenta provides tooling and record-keeping; it does not certify compliance. See also the guides and the security page.