Apply a balance-mutation to every budget matching a filter
Apply CREDIT, DEBIT, RESET, RESET_SPENT, or REPAY_DEBT to every budget ledger matching the provided filter in a single synchronous request. Mirrors /v1/admin/tenants/bulk-action and /v1/admin/webhooks/bulk-action in body shape, safety semantics (expected_count gate, idempotency_key, 500-row hard cap), and per-row outcome envelope. Resolves the operational pain described in cycles-server-admin issue #99: "Bulk Budget Reset at Tenant or Parent-Scope Level" — when a billing period rolls over, operators no longer iterate listBudgets + per-row fundBudget; they issue one bulk request scoped by tenant_id and (optionally) scope_prefix.
OPERATION-DISCRIMINATED REQUEST: - The action enum names which balance-mutation to apply
uniformly across the matched set. The action determines
which payload fields are required:
- CREDIT, DEBIT, RESET, REPAY_DEBT: require
amount. - RESET_SPENT: requires
amount;spentoptional (defaults to 0).
- Operation semantics MATCH BudgetFundingRequest exactly so
there is one place to read the per-operation balance math.
Anything fundBudget can do to one ledger, this endpoint can
do to a filtered set. - Server MUST validate the action/payload combination BEFORE
counting matches or mutating any row; an invalid combination
→ HTTP 400, no writes.
FILTER SEMANTICS: - Filter block mirrors the query params of listBudgets
(scope_prefix, unit, status, over_limit, has_debt,
utilization_min/max, search), AND combination, ILIKE match
on search. Operators preview the affected set by issuing
GET /v1/admin/budgets with the same filter before invoking
bulk-action and populating expected_count from the count.
tenant_idis REQUIRED (cross-tenant safety — see
BudgetBulkFilter). Cascading reset across a scope subtree
usesscope_prefix; no separatecascadeflag.
SAFETY: - expected_count gate: server counts before mutation;
mismatch → 409 COUNT_MISMATCH with no writes. See
bulkActionTenants for full rationale (operator preview
drifted; refuse rather than over-apply).
- idempotency_key: 15-min replay window; repeat submits
return the original response without re-applying. - HARD LIMIT: 500 matching rows. Larger filters → HTTP 400
LIMIT_EXCEEDED. Operators with > 500 budgets in a
rollover batch must partition by scope_prefix (the natural
partition for cascading resets). - Overall HTTP 200 even when some rows fail; response
envelope reports succeeded / failed / skipped counts.
Common per-row failure codes: BUDGET_EXCEEDED (DEBIT would
take remaining negative), INVALID_TRANSITION (unit
mismatch, ledger CLOSED).
EVENTS (document revision 0.1.25.32): - Server MUST emit one Event per successfully-mutated row,
typed by action:
- CREDIT →
budget.funded - DEBIT →
budget.debited - RESET →
budget.reset - RESET_SPENT →
budget.reset_spent - REPAY_DEBT →
budget.debt_repaid
Skipped rows (idempotent no-ops) and failed rows MUST NOT
produce an Event — emission is bound to actual balance
mutation, matching the single-op fundBudget path.
- Each emitted Event carries
correlation_id = budget_bulk_action:<action>:<request_id>
(action lowercased, e.g.budget_bulk_action:credit:req_abc).
Operators retrieve the full bulk fan-out via
GET /v1/admin/events?correlation_id=…; the shared
request_id also ties every emitted row to the invocation's
single AuditLogEntry.
AUDIT LOG: - One AuditLogEntry per bulk-action invocation (not per
row). The entry records actor_type=ADMIN_ON_BEHALF_OF,
tenant_id from the filter, operation=bulkActionBudgets,
and embeds the full per-row outcome (succeeded / failed /
skipped ledger_ids) in the entry payload so security
review can reconstruct exactly what was changed without
replaying the bulk request.
AUTHORIZATION: - AdminKeyAuth only. Tenants cannot bulk-mutate their own
budgets via this endpoint; per-budget mutation remains
available via fundBudget.
Authorizations
Administrative API key with full system access. Also accepted as an alternative to ApiKeyAuth on an explicit per-operation allowlist — the authoritative list is the union of operations whose security: block declares AdminKeyAuth (consult per-operation security blocks rather than this prose, which has historically drifted as the dual-auth surface expanded). When using AdminKeyAuth on list or fund endpoints, a tenant scoping parameter (typically tenant or tenant_id) is required for scoping (400 if missing) — the per-operation description specifies which. Lookup-style endpoints that uniquely identify a resource by non-tenant key (e.g. GET /v1/admin/budgets/lookup, where the (scope, unit) pair is unique) do NOT require a tenant parameter. Allowlisting is per-operation (exact method:path matching — no prefix matching, no wildcards) so new endpoints do not accidentally inherit admin-accessible status.
Request Body
Responses
Bulk action applied (may include per-row failures)