Requires a credential holding scans:create and scans:read.

Queue it

202 is work accepted, not work done. 200 means an existing scan was returned instead, which is how a client tells a deduplicated retry from a new scan — the two look the same in the body, so branch on the status here. The image reference is untrusted input and is validated at the boundary. Private and link-local registries, embedded credentials, URL schemes and traversal segments are all refused with a message naming the rule. TRUSTIVAN pulls remotely from the registry. A registry that requires a login needs a credential stored against its host — see Private registries — and the scan carries a reference to it, never the secret itself.

Make the retry safe

Idempotency-Key is a header, or idempotency_key in the body. Use something your pipeline already has and will reproduce on a retry — a build ID, a job ID — not a random value. The key is reserved before any work begins, so two concurrent retries cannot both queue a scan. A completed request replays its stored response verbatim with Idempotent-Replay: true. Reusing the same key with a different body is 409, never a replay: returning the first response would tell you that work succeeded which was never attempted. Records are kept for 30 days.

Poll to a terminal state

Terminal statuses are succeeded and failed. Anything else means keep polling. Two seconds between attempts is reasonable; give the loop a ceiling, because pulling and unpacking a large image takes minutes and an unbounded poll loop is a build that hangs rather than one that reports a problem. A retryable failure returns the scan to queued rather than reporting failed, so a failed status is genuinely terminal.

Check database_state before you trust the result

It reports how fresh the advisory data behind the result was. A scan run against stale data reports “no vulnerabilities” with exactly the same confidence as a correct one, which is why the age is surfaced rather than hidden — and why a sufficiently old database fails the scan outright instead of producing a clean-looking report.

Gotchas

  • Concurrency is capped per tenant (default 4). Work over the cap stays queued; nothing is dropped, but a burst of scans will not all start at once.
  • findings_total of zero on a real image is a signal, not a result. Check database_state and the scan’s recorded scanners before believing it.
  • A 404 on the scan means “no such scan, or not yours”. The two are byte-identical on purpose.
  • Secret and misconfiguration scanners are off by default. Rather than passing them per request, set them once for a target kind with PUT /api/v1/settings/scanners/{kind}; their findings appear in the dashboard like any other. Licences need no scanner at all — read them from /api/v1/licenses.

See also