Release a held payout
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.
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
Path Parameters
The gateway transaction id.
uuidResponse Body
application/json
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!,});// After you clear the blocker named in statusReason.const txn = await client.releasePayout(transactionId);{ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "source": "string", "payeeId": "79417842-2e1e-4110-a361-89b9f39b36ed", "idempotencyKey": "string", "amountCents": 0, "currency": "string", "status": "requested", "provider": "stripe", "providerRef": "string", "reason": "string", "ref": "string", "failureKind": "transient", "retryable": true, "createdAt": "2019-08-24T14:15:22Z", "updatedAt": "2019-08-24T14:15:22Z", "liftingFeeCents": 0, "claimUrl": "string"}Retry a transiently-failed payout POST
Re-run a payout that `failed` with a **transient** cause (e.g. the company wallet was momentarily underfunded) — check the `retryable` hint on `GET /payouts/{id}` first. The retry re-sends under the SAME rail idempotency code, so the rail dedupes: exactly-once is preserved and a retry can never double-pay. A payout that is not `failed`, or whose failure was **permanent** (bad destination, not onboarded, unsupported currency), returns `409`. See the Error & held-reason catalog for the concrete action per `statusReason`. **Required scope:** `payouts:write`.
Push owed (IOU) rows POST
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`.