Push owed (IOU) rows
Push owed (IOU) rows programmatically — the API twin of the client-portal
CSV upload. Each item identifies the payee by EXACTLY ONE of payeeId (one
you own) or email (resolve-or-create, mirroring BatchPayoutItem), plus an
amountCents and YOUR stable externalRef (e.g. your ledger row id).
The response is 207 Multi-Status with one entry per row, in request
order: status: ok carries the created (or replayed) disbursement,
status: error carries a stable error code from the closed per-row enum on
DisbursementPushResultItem.error. A failing row never aborts the others.
Idempotent per row on (yourClient, externalRef) — pushing the same
externalRef again always returns the SAME owed row (idempotentReplay: true) rather than double-crediting, regardless of its current status, and a
replay never re-touches payee data. Two rows in the SAME call sharing an
externalRef is always a caller bug (an owed row's ledger id is unique by
definition) and is rejected as a whole-request 400 duplicate_external_ref
before any row is processed.
Set sendEmail: true to send the "you have money waiting" email — sent only
for rows that were newly created in this call, after the batch commits.
Unlike POST /payouts/batch, the email resolve-or-create path here is authorized
by disbursements:write alone — no separate payees:write is required.
Required scope: disbursements:write.
Authorization
bearerAuth x8xTimestamp x8xSignature Your client API key. Authorization: Bearer <API key>. The gateway stores
only sha256(key); keys are rotatable and revocable.
In: header
Unix time (seconds) at which the request was signed. Must be within 300s of gateway time.
In: header
Hex HMAC-SHA256(signingSecret, "{timestamp}.{rawBody}") over the exact raw
request body (empty string for GET). Defeats tampering and replay.
In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
import { PayoutClient } from '@8x-payout/sdk';const client = new PayoutClient({ baseUrl: process.env.PAYOUT_BASE_URL!, apiKey: process.env.PAYOUT_API_KEY!, signingSecret: process.env.PAYOUT_SIGNING_SECRET!,});const result = await client.pushDisbursements({ items: [ { email: 'creator@example.com', amountCents: 5000, currency: 'USD', externalRef: 'ledger_row_9f3c21', // your stable id; re-pushing is a no-op reason: 'October earnings', }, ], sendEmail: true,});for (const row of result.results) console.log(row.index, row.status);{ "results": [ { "index": 0, "status": "ok", "disbursement": { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "payeeId": "79417842-2e1e-4110-a361-89b9f39b36ed", "email": "user@example.com", "amountCents": 0, "currency": "string", "status": "owed", "externalRef": "string", "createdAt": "2019-08-24T14:15:22Z" }, "idempotentReplay": true, "error": "payee_target_missing" } ]}Release a held payout POST
Re-drive a payout that is `held` — a **recoverable** anomaly hold, not a terminal failure. A payout holds for ordinary, fixable reasons: the payee isn't onboarded yet, a required profile field is missing, the resolved rail isn't in your allow-list, or a per-payee daily cap window hasn't elapsed. Read `statusReason` on `GET /payouts/{id}` (a leading snake_case code — `missing_email`, `not_payable`, `rail_not_allowed`, `cap_exceeded_daily`, …), clear the blocker, then call this. It re-runs routing + operational caps under the SAME transaction id: when the blocker has cleared it advances to `authorized`/`submitted`/`paid`; otherwise it stays `held` with an updated `statusReason`. The re-run reuses the persisted rail idempotency code, so a release can NEVER double-pay. A payout that is not `held` returns `409`. See the Error & held-reason catalog for the concrete action per `statusReason`. **Required scope:** `payouts:write`.
Link released disbursements to their replacement payout POST
Attach several already-released owed rows to ONE already-submitted replacement payout. A **ledger repair only**: it never creates, retries, or calls a payout provider — it repairs the pre-consolidation fan-out shape, where a withdrawal released N disbursements as N one-row payouts and a single replacement payout was later submitted in their place. **Idempotent** — re-sending the same `(replacementTransactionId, disbursementIds)` returns `idempotentReplay: true` and links nothing twice. The replacement's amount must equal the sum of the linked rows, and every row must belong to the replacement's payee, or the call is rejected whole. **Required scope:** `disbursements:write`.