Push owed (IOU) rows

POST
/api/v1/disbursements

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
AuthorizationBearer <token>

Your client API key. Authorization: Bearer <API key>. The gateway stores only sha256(key); keys are rotatable and revocable.

In: header

x-8x-timestamp<token>

Unix time (seconds) at which the request was signed. Must be within 300s of gateway time.

In: header

x-8x-signature<token>

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`.