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 needsfindings:suppress.
The request
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
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 single400. 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
SupplyIdempotency-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=recentbefore 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.
unsuppresson a triaged finding returns it toopen,triageon a suppressed finding removes the suppression, andsuppresson a suppressed finding replaces its reason and expiry. Send each action only for the findings it is meant for. 403names the missing scope inrequired_scope, so a runner that lacksfindings:suppressreports that rather than “forbidden”.
See also
- Suppress a finding — the constraints on reason and expiry, which apply identically here.
- Idempotency — reservation and replay.
- Endpoints — the full request schema.

