Introduction

Execution-only, custody-free, multi-rail payouts to humans.

The 8x Payout Gateway moves money to people across global rails. You integrate it the way you'd integrate Stripe: create a payee → send them to a hosted onboarding link → get a payee.updated webhook when they're payable → call POST /payouts.

What the gateway is (and is not)

  • Execution-only & custody-free — it owns payout identity, multi-rail routing, hosted onboarding, execution, and the transaction log. For direct POST /payouts the gateway holds no balance and tracks no owed ledger — you decide who and how much, exactly as with Stripe. It also offers an optional owed/IOU ledger (POST /disbursements + withdraw) for a pull/wallet model: YOU decide what is earned and push the owed rows; the gateway then holds them as owed until an explicit withdraw. It never independently judges or computes what a payee is owed — your externalRef stays the source-of-truth ledger id.
  • Funding is rail/platform funding, never a customer balance. Some rails require the company operating the gateway to fund a platform account or rail wallet (Stripe platform balance, Tipalti payer balance) before payouts settle. Prepaid clients may also fund a wallet: an append-only record of money received from you, whose available balance is computed — deposits minus in-flight/paid payouts — and gates release (held with insufficient_funds when short). There is no stored balance, no interest, no customer money-holding product. Underfunding yields a retryable failure or a held payout — never a silent re-route.
  • The caller decides WHO and HOW MUCH; the gateway decides WHERE. A payout request carries payeeId + amountCents — never a bank account or destination. Money can only land on the payee's KYC-locked method, resolved server-side. No single compromised surface can both inflate an amount and redirect it.

Authentication (every request)

Every request is authenticated (which client), signed (tamper- and replay-proof), and authorized (scope + ownership). Send three things:

  • Authorization: Bearer <API key> — identifies your client. The gateway stores only sha256(key).
  • x-8x-timestamp — Unix seconds when you signed the request.
  • x-8x-signature — HMAC-SHA256(signingSecret, "{timestamp}.{rawBody}"), hex-encoded. For a GET the body is empty, so you sign "{timestamp}.".

The timestamp must be within a 5-minute window (300s) of the gateway clock, or the request is rejected as stale. Mutations (POST) are additionally replay-protected: a duplicate signature inside the window is rejected. Idempotent GETs are not replay-checked (so a polling client is never spuriously 401'd).

The official @8x-payout/sdk PayoutClient performs the signing for you — see the Quickstart.

Authorization (scopes)

Each client carries a set of scopes; every route asserts the scope it needs and fails closed (403) if it's missing. A read-only integration can be issued a key whose client holds only the *:read scopes — it cannot move money.

ScopeGrants
payouts:writePOST /payouts, POST /payouts/batch †, POST /payouts/{id}/retry, POST /payouts/{id}/release
payouts:readGET /payouts, GET /payouts/{id}
payees:writePOST /payees, POST /payees/{id}/methods
payees:readGET /payees, GET /payees/{id}, GET /payees/{id}/readiness, GET /payees/{id}/balance
onboarding:writePOST /payees/{id}/onboarding-link
portal:writePOST /payees/{id}/portal-link
disbursements:writePOST /disbursements, POST /disbursements/recovery-link, POST /payees/{id}/withdraw

† Batch items that resolve-or-create a payee by email additionally require payees:write.

Tenant isolation

A client can only see and act on its own payees and transactions. A payee or transaction owned by another tenant is reported as 404 (existence never leaks across tenants).

Test mode (no real money)

Test mode is a property of your client, not a separate host — the base URL is the same. A client in test mode routes every payout and onboarding through the mock provider (settles synchronously, no real KYC, no real money), so you can run this entire contract end to end before going live. Flip to live when ready. See the Quickstart for a zero-to-first-payout walkthrough.

Money & values

  • Money is always integer cents (amountCents). The gateway converts to a rail's units at the execution seam, never on the wire.
  • currency is ISO 4217 uppercase (e.g. USD).
  • Timestamps are ISO 8601 strings (UTC).

Error & held-reason catalog

For a held or failed payout, statusReason (on GET /payouts/{id}) leads with a stable snake_case code from the closed set below. This same catalog is the authoritative source for the statusReason token and the stable error code; the withdraw, batch, and disbursement per-row error enums reference it.

codetriggerkindself-releases?remediation
missing_emailpayee has no emailheldnoupsert payee with email, then release
missing_namenon-test payee has no legal nameheldnosupply payee name, then release
not_payableresolved method not payableheldnocomplete onboarding/KYC
rail_not_allowedrail outside client allow-listheldnocontact 8x to widen allow-list
cap_exceeded_dailydaily cap hitheldon window resetwait for the daily window, then release
cap_exceeded_singlesingle-payout cap hitheldnosplit the payout or request a higher cap
insufficient_fundsrail/wallet underfundedheldon fundingfund the rail/wallet, then release
below_min_payoutamount under economical floorheldyes — accumulated-floor sweepwait for accumulation, or force-release (bypasses floor)
no_railno supported rail for countryheldnofix country / choose a configured rail
route_blockedrouting explicitly blockedheldnochoose an allowed rail
missing_countryno country on payeeheldnoset payee country
provider_not_supportedprovider can't serve countryheldnochoose a supported rail
provider_not_configuredprovider not wired for deploymentheldnocontact 8x
country_not_supported_by_providercountry/provider mismatchheldnochoose a supported rail
unsupported_countrylegacy: routing no longer produces it since Grade became the default rail; may remain on older held payoutsheldnorelease
provider_<name>_not_implementedrouted rail not yet wiredfailed (permanent, 400)nocontact 8x
authorized_payout_missing_providerauthorized payout lost its providerfailednocontact 8x

below_min_payout is a benign, self-releasing hold (an accumulated-floor sweep releases it once enough accrues).

Payout status lifecycle

held is caller-remediable — not a dead end. Read statusReason, clear the blocker (see the catalog above), then POST /payouts/{id}/release.

statusmeaningterminal?what you doendpoint
requestedaccepted, not yet routednowait—
authorizedrouted, payee payablenowait—
submittedsent to the provider, awaiting resultnowait (do NOT retry)—
heldblocked by a statusReasonnoread statusReason, clear the blocker, releasePOST /payouts/{id}/release
failedattempt failedno (transient) / yes (permanent)retry if failureKind=transientPOST /payouts/{id}/retry
paidfunds released to railterminalkey off settledAt for bank arrival—
returnedbounced after settlementterminalreverse in your ledger; read restoredCents—

Legal transitions: requested→authorized→submitted→paid; held→(release) and failed→(retry) re-enter the flow; paid→returned.

Status versions

Your client's status version picks the words a payout's status uses on the payout.status webhook and on every payout response. The webhook and REST are versioned together. Version 1 is the default and uses the lifecycle above. Version 2 is opt-in per client and set by 8x ops. It uses owed, sent, awaiting_withdraw, paid, failed, returned, held (see the PayoutStatusV2 schema). requested and authorized become sent. submitted becomes sent on custodial rails (stripe, tipalti, and mock in test mode) and awaiting_withdraw on custody-free rails (grade, sideshift); a Grade payout reads sent once the creator has claimed it, a SideShift payout stays awaiting_withdraw until it settles as paid. The other words stay the same. owed never appears on a payout today because it lives on disbursements. The status filter on GET /payouts takes version 1 words only.

Rate limit

600 requests per minute per client, fixed 60-second window (configurable per deployment). No Retry-After header is returned — back off to the next minute boundary.

Versioning

/api/v1 is stable. Within a version, changes are additive-only; anything breaking ships under a new version path. Deprecations are announced before removal.

Servers

  • https://8x-payout.com — Production gateway. Your exact base URL is provided when your client is provisioned.
  • http://localhost:3007 — Local development (the gateway's default dev port).

Authentication

Every request carries all of the following (bearerAuth & x8xTimestamp & x8xSignature).

bearerAuth http · bearer

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

x8xTimestamp apiKey · header: x-8x-timestamp

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

x8xSignature apiKey · header: x-8x-signature

Hex HMAC-SHA256(signingSecret, "{timestamp}.{rawBody}") over the exact raw request body (empty string for GET). Defeats tampering and replay.