Create many payouts in one call
Pay many people in a single signed request — the email + amount contract.
Each item identifies a payee by payeeId (one you own) or by
email (the gateway resolves-or-creates a payee keyed by that email),
plus an amountCents. As always the body carries no destination — each
rail is resolved server-side from the payee's KYC-locked method.
The response is 207 Multi-Status with one entry per row, in request
order: status: ok carries the created/replayed transaction (inspect its
own status — an un-onboarded email row comes back held), status: error
carries a stable error code from the closed per-row enum on
BatchPayoutResultItem.error. A failing row never aborts the others.
Exactly-once. Each row's idempotency key is its own idempotencyKey when
set, else it is derived from the batch key and the row's payee identity
(not its array position). So re-sending the same batch key with rows added,
removed, or reordered replays the rows already paid and pays only the genuinely
new ones — it never double-pays. Two rows targeting the same payee without
explicit per-row keys are ambiguous and rejected (400 duplicate_target); give
them distinct idempotencyKeys to pay the same payee twice in one batch.
Required scope: payouts:write. Items that use email (which mint a
payee identity) additionally require payees: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.payoutBatch({ idempotencyKey: 'payroll-2026-10', items: [ { email: 'a@example.com', amountCents: 5000, currency: 'USD', ref: 'row_1' }, { payeeId, amountCents: 1250, currency: 'USD', ref: 'row_2' }, ],});for (const row of result.results) console.log(row.index, row.status);{ "results": [ { "index": 0, "status": "ok", "transaction": { "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" }, "idempotentReplay": true, "error": "invalid_target" } ]}Create a payout POST
Create — or idempotently re-fetch — a payout. **Exactly-once** on `(yourClient, idempotencyKey)`: re-sending the same `idempotencyKey` returns the **existing** transaction (HTTP `200`), never a second payout. A brand-new payout returns HTTP `201`. The body carries **no destination** — the gateway resolves the rail from the payee's KYC-locked identity. If the payee isn't payable, the rail isn't built, a per-client rail allow-list excludes the resolved rail, or an operational ceiling is exceeded, the transaction is created in a non-terminal state (`held` / `failed`) and returned — inspect `status`. On a provider timeout/5xx the transaction stays `submitted` (never silently `failed`) for the reconcile cron to resolve. **Required scope:** `payouts:write`.
Get one payout GET
Fetch a single transaction by id, scoped to your client, with its full event trail and a `retryable` hint. A transaction owned by another client is reported as `404`. **Required scope:** `payouts:read`.