POST /findings/bulk applies one action to many findings. Requires findings:write, or findings:suppress when the action is suppress.

From the dashboard

On Findings, tick the findings (or every finding on the page) and choose Mark as triaged, Suppress or Unsuppress. A selection can span pages and resets when the filters change. Each action is sent only for the selected findings it applies to, and the confirmation says how many are left out and why. Findings the server refuses are listed with its reason and stay selected. A viewer is offered no selection, and Suppress is disabled for a member, because it needs findings:suppress.

The request

Suppressing takes the same fields as the single-finding endpoint:
At most 500 finding IDs per request; more is a 400. The ceiling is on work rather than convenience — without one, a single request makes the server hold a transaction over an unbounded number of rows, which is a denial of service the caller does not have to intend.

Actions are verbs

suppress, unsuppress, triage. That is the whole set. The single-finding endpoint takes a target status because a human is looking at one row and knows its state. A bulk caller does not: their 500 findings are in a mix of states, and “set them all to open” means something different to each. A verb says what you intend; the state machine decides per row whether that intent is reachable from where the row actually is. There is no bulk resolve because no caller may assert resolution at all — it means a scan that would have found the issue did not, which is evidence rather than opinion. There is no bulk reopen because reopening in bulk has no legitimate use and would be an excellent way to undo a colleague’s triage across an entire asset.

The response

Always 200 when the request itself was valid, including when items were rejected. Branch on the counts, not on the status line: 207 Multi-Status is a WebDAV code most HTTP clients treat as an error. not_found is deliberately the same answer for “does not exist” and “belongs to somebody else”. Distinguishing them would be a fast way to enumerate which UUIDs are real.

What is atomic and what is not

Every item is validated before anything is written. That is what makes the result deterministic: the same request produces the same per-item outcomes whether or not it is retried, because rejections were computed from state rather than discovered halfway through a loop that had already changed some of it. Items rejected during validation are not attempted, so one unusable ID in a batch of 500 does not fail the other 499. What is atomic is the execution: either every valid item moved, or none did. Partial success at the level of the request is a feature; partial application of a transaction would be a batch left half applied with no record of where it stopped. Request-level problems are still a single 400. A missing suppression reason is the same answer for every row, so it is one request error — not 500 identical per-item rejections inside a 200 claiming partial success for a request in which nothing could ever have succeeded.

Retries

Supply Idempotency-Key (header or idempotency_key in the body). The key is reserved before any work, so two concurrent retries cannot both execute; a completed request replays its stored response verbatim with Idempotent-Replay: true, which matters here because a fresh evaluation on retry would report already_applied for everything and hide what the original call actually did. Same key with a different body is 409.

Gotchas

  • Enumerate with order=recent before acting. Collecting IDs in severity order can miss rows that moved. See Enumerate findings safely.
  • Each finding still gets its own timeline event, correlated by batch_id. A timeline that said “see batch 7” would not be evidence.
  • A bulk suppression of 500 findings publishes 500 webhook deliveries per subscribed endpoint. Five hundred suppressions genuinely are five hundred state changes, and collapsing them would leave a receiver unable to tell which findings moved.
  • A verb applies whatever transition the row’s state allows. unsuppress on a triaged finding returns it to open, triage on a suppressed finding removes the suppression, and suppress on a suppressed finding replaces its reason and expiry. Send each action only for the findings it is meant for.
  • 403 names the missing scope in required_scope, so a runner that lacks findings:suppress reports that rather than “forbidden”.

See also