docs / sbom

SBOM ingestion

POST /v1/sbom accepts a CycloneDX JSON document as the request body. Each flattened component becomes one result row. Mapped rows call the same check path as GET /v1/check. Unmapped rows still return and still bill.

Container and OS SBOMs

A Trivy or Syft image BOM is mostly pkg:deb, pkg:apk, and pkg:oci. Every row is billed. Almost none are mapped. Use lockfile CycloneDX (npm or PyPI) or the GitHub Action lockfile scan. A golang-only BOM is a valid coverage probe and returns 200 with mapped_count 0.

what v1 accepts

CycloneDX JSON 1.3 through 1.6

Send the BOM object itself (bomFormat at the top level). No wrapper. JSON only. XML returns 400. SPDX is out of v1. GitHub Dependency Graph / repository SBOMs are SPDX. Convert to CycloneDX or use lockfile mode in the GitHub Action.

  • Cap: 2,000 flattened components (includes metadata.component and nested components). Over cap is 400, not billed.
  • Body cap: 4 MiB. Over size is 413, not billed.
  • Content-Type may be application/json or application/vnd.cyclonedx+json. Missing or charset suffixes are fine.
  • scope: excluded and optional rows are returned. BOM vulnerabilities[] is ignored.
  • Nesting deeper than 32 levels is 400, not billed. Nodes are not dropped silently.
  • No query parameters. include=cves is not available on this route.

A large unique scan can take tens of seconds. Cloudflare proxies api.attestd.io with a roughly 100s timeout.

mapping

What maps onto a check

purl typev1 behavior
pkg:npmMapped. Supply-chain check with ecosystem=npm. Unwatched names return supported: false.
pkg:pypiMapped. Supply-chain check with ecosystem=pypi.
pkg:genericMapped only when the name is an Attestd CVE product slug. Aliases such as postgres work. OS names (linux, windows, ubuntu, debian, macos, kernel) are never mapped.
golang, maven, deb, apk, oci, cargo, nuget, othersoutside_coverage, still billed. Telemetry records the type.

Placeholder versions (unspecified, unknown, *, latest) and ranges are invalid_identity. A component with no purl is missing_purl. We do not guess ecosystem from name alone.

billing

One row, one call

Every returned row bills one call, including outside_coverage. Internal per-item errors are not billed. Quota is checked before any check work. If the BOM needs more calls than you have left, the response is 429 and nothing is billed.

Free tier is 1,000 calls per month and 60/min. A 2,000-component BOM does not fit Free. Solo includes 10,000 calls, enough for five max-size uploads. Duplicate identities in one BOM are two rows and two bills. The check itself runs once for that identity in the request.

example

Upload a BOM

Keys scoped to v1.check may call this route. Scope id is v1.sbom.

bash
curl -X POST "https://api.attestd.io/v1/sbom" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/vnd.cyclonedx+json" \
  --data @bom.json

Minimal bom.json:

json
{
  "bomFormat": "CycloneDX",
  "specVersion": "1.5",
  "metadata": {
    "component": {
      "type": "application",
      "name": "example-app",
      "version": "1.0.0"
    }
  },
  "components": [
    {
      "type": "library",
      "name": "left-pad",
      "version": "1.3.0",
      "purl": "pkg:npm/[email protected]"
    },
    {
      "type": "library",
      "name": "requests",
      "version": "2.32.3",
      "purl": "pkg:pypi/[email protected]"
    }
  ]
}

Response leads with summary, then results in document order. Truncated:

json
{
  "summary": {
    "component_count": 3,
    "mapped_count": 2,
    "outside_coverage_count": 1,
    "risk_state_counts": {
      "critical": 0,
      "high": 0,
      "elevated": 0,
      "low": 0,
      "none": 2
    },
    "compromised_count": 0,
    "billed_calls": 3,
    "spec_version": "1.5"
  },
  "results": [
    {
      "bom_ref": null,
      "purl": null,
      "name": "example-app",
      "version": "1.0.0",
      "ecosystem": null,
      "outside_coverage": true,
      "outside_coverage_reason": "missing_purl",
      "result": null,
      "error": null
    }
  ]
}
errors
HTTPWhen
413Body larger than 4 MiB. Not billed.
400Not JSON, XML, bad bomFormat, specVersion outside 1.3 to 1.6, zero components, more than 2,000 components, or nesting deeper than 32 levels. Not billed.
401 / 403Auth or route scope.
429Monthly quota, key cap, or Free 60/min, before checks run. Not billed.
not in v1

SPDX, CycloneDX XML, async job ids, SDK methods, MCP tools, GitHub Action SBOM input, and include=cves on this route.

related