Skip to content

payouts

Payouts is the money-out ledger: immutable rows per entity per period, grouped into batches by a payout rail.

await boomin.payouts.run({ periodStart: "2026-08-01", periodEnd: "2026-09-01" });
const accepted = await boomin.payouts.exportCsv({
periodStart: "2026-08-01",
periodEnd: "2026-09-01",
});
await boomin.operations.wait(accepted.operation, { timeout: 120000 });
const batch = await boomin.payouts.batches.retrieve(accepted.batch);
console.log(batch.downloadUrl);

Configuration lives on three nested clients, each with its own page:

ClientCovers
payouts.rulesHow a entity earns
payouts.railsHow money physically leaves
payouts.batchesOne frozen disbursement run

Start with Getting entities paid for the model.

MethodRouteScope
list(params, options)GET /payoutspayouts:read
run(params, options)POST /payouts/runpayouts:write
exportCsv(params, options)POST /payouts/export_csvpayouts:write
connectStatus(params, options)GET /payouts/connect_statuspayouts:read
const result = await boomin.payouts.run({
periodStart: "2026-08-01", // YYYY-MM-DD, required
periodEnd: "2026-09-01", // YYYY-MM-DD, required
});

Recomputes the period’s payout rows. It is an idempotent upsert: running it twice over the same period converges rather than duplicating.

periodStart must be strictly before periodEnd, or invalid_period (400).

Two outcomes, and only one is yours to fix

Section titled “Two outcomes, and only one is yours to fix”

Nothing configured — no active payout rule and no active content split on the brand — throws PayoutRulesRequiredError (payout_rules_required, 409). No input could have produced a payout, so this is a configuration error rather than an empty result.

Rules ran, nothing qualified — success:

{
"object": "payout_run",
"outcome": "no_eligible_activity",
"rulesEvaluated": 3,
"splitsEvaluated": 0,
"eventsEvaluated": 14,
"payoutsCreated": 0,
"underfunded": 0,
"awaitingAccount": 0,
"payouts": [],
"summary": { "totalAmountMinor": 0, "count": 0, "awaitingAccount": 0, "bridged": 0, "unresolvedRecipients": 0 }
}

(Raw HTTP responses spell these rules_evaluated, total_amount_minor, and so on; the SDK camelCases every response key.)

import { PayoutRulesRequiredError } from "@boomin/sdk";
try {
const result = await boomin.payouts.run({ periodStart, periodEnd });
if (result.outcome === "no_eligible_activity") {
console.log(`nothing qualified — ${result.rulesEvaluated} rules over ${result.eventsEvaluated} events`);
}
} catch (err) {
if (err instanceof PayoutRulesRequiredError) {
// create a rule before running again
} else throw err;
}

Branch on outcome ("payouts_created" | "no_eligible_activity"), never on a count and never on prose — there is no warnings[] by design. underfunded is structurally 0: payout compute records obligations and never draws down a budget. The count that actually blocks money leaving is awaitingAccount.

Build and export in one call on the csv_batch rail — the commonest operator intent, kept as one round trip. Answers 202.

const accepted = await boomin.payouts.exportCsv({
periodStart: "2026-08-01", // optional
periodEnd: "2026-09-01", // optional
});
{
"batch": "pob_...",
"status": "exporting",
"operation": "op_...",
"items": [ ],
"skipped": 0
}

batch and operation are id strings — the same 202 contract batches.export answers. There is one export contract, not two.

The build half runs synchronously inside the call, so the two failures you can actually fix come back immediately and typed rather than being discovered by polling an operation that fails:

ThrowsWhen
PayoutRailRequiredErrorNo active csv_batch rail. Nothing is auto-provisioned — why.
PayoutBatchEmptyErrorNo settle-able row for this rail and period.

skipped is a count of otherwise-eligible rows dropped for want of a recipient email. They stay eligible for a later batch.

await boomin.operations.wait(accepted.operation, { timeout: 120000 });
const batch = await boomin.payouts.batches.retrieve(accepted.batch);
console.log(batch.downloadUrl);

The presigned URL is minted on read of the batch, not returned by the mutation. One handed back here would already be expiring by the time an operator opened it, and could not be re-obtained without re-exporting.

Omit both period fields to sweep every eligible row regardless of period.

ParamValues
statuspending awaiting_account processing paid failed
periodStart / periodEndYYYY-MM-DD exact match
entityA ent_... id — one recipient’s ledger
limit1–100, default 20
startingAfterA po_... cursor
for await (const payout of boomin.payouts.list({ status: "awaiting_account" })) {
console.log(payout.id, payout.amountCents, payout.periodStart);
}

awaiting_account is the interesting bucket: money is owed, but the recipient has no usable payout destination. Because entity Connect onboarding is not yet available, it is also the normal status — and csv_batch batches those rows anyway.

Filtering by entity also excludes every user-recipient row, since a payout row has exactly one recipient (user XOR entity).

const status = await boomin.payouts.connectStatus();
{
"object": "payouts.connect_status",
"rails": [
{ "id": "prail_...", "object": "payout_rail", "rail": "csv_batch", "status": "active", "isDefault": true }
],
"stripe": {
"configured": true,
"entityAccounts": 42,
"entityAccountsPayoutsEnabled": 0
}
}

The disbursement-readiness read. Check it before a run rather than discovering the gap after building a batch.

Rail entries carry identity and state only, never config — this is a payouts:read surface, and a column mapping decides where money lands. Read config from payouts.rails.list(), which needs payout_rails:read.

Terminal window
npx @boomin/cli payout run --period-start 2026-08-01 --period-end 2026-09-01
npx @boomin/cli payout export --period-start 2026-08-01 --period-end 2026-09-01 --out payouts.csv
npx @boomin/cli payout list --status awaiting_account
npx @boomin/cli payout connect

payout export polls the operation to terminal, reads the batch, and downloads the CSV to --out. An operation that ends anything but succeeded exits non-zero rather than leaving an empty file behind.