Create a hosted onboarding link

POST
/api/v1/payees/{id}/onboarding-link

Get a gateway-hosted, rail-agnostic onboarding URL for a payee — like a Stripe account link. Redirect your (already-authenticated) creator to the returned url, or embed it as an iframe; they need no separate gateway login. The URL points at the gateway's own /onboard page carrying a short-lived signed token — never at Tipalti/Stripe directly — and the gateway renders the right rail behind it.

returnUrl is where the creator is sent when they finish. It must be an absolute https URL whose host is either your registered webhook domain (the host of your configured webhook URL) or the gateway's own origin; anything else is rejected at request time with 400 invalid_return_url (open-redirect protection). Localhost over http is allowed in non-production environments only. When the payee becomes payable, the gateway fires a payee.updated webhook.

The onboarding experience depends on the payee's resolved rail. Custodial rails (Stripe, Tipalti) run hosted KYC and payout-method setup up front — the payee becomes payable only after they finish, at which point payee.updated fires. Custody-free rails (Grade, SideShift) are payable-by-design at account creation; KYC and method selection happen inline at claim/withdraw time via the claim link the payout produces, so there is no method to pre-set here.

Required scope: onboarding: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

Path Parameters

id*string

The gateway payee id.

Formatuuid

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

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 { url, provider } = await client.getOnboardingLink(  payeeId,  'https://yourapp.example.com/payouts/onboarded', // returnUrl on YOUR domain);// Redirect the creator to `url` (or pass mode: 'iframe' to embed it).
{  "url": "http://example.com",  "mode": "redirect",  "provider": "stripe"}

Adopt an existing provider account as a payout method POST

Attach an **already-KYC'd** provider account — a Stripe Connect `acct_…` on the gateway's own platform, or a Tipalti payee id under the gateway's payer — as this payee's payout method, bypassing hosted onboarding entirely. Use this when you're migrating a creator who is already verified on the rail directly. This is the gateway's trust boundary: the caller's payability claim is **never** believed. The gateway reads the account **live** from the rail (Stripe: `charges_enabled && payouts_enabled`; Tipalti: payable status) and sets `payable`/`onboardingStatus` from what the provider actually reports. One provider account can only ever fund **one** payee. An id already linked to a **different** payee is rejected `409` (without revealing who holds it). Re-adopting the SAME `(payee, provider, providerAccountId)` is a safe, idempotent replay. An account the rail can't confirm (nonexistent, on a foreign platform, or a failed live call) is rejected `422`. On a payable adoption the payee flips payable/verified and the same `payee.updated` webhook a normal onboarding completion fires is enqueued. **Required scope:** `payees:write`.

Check payout readiness GET

The **pull backstop**: can we pay this person right now, via which rail, and if not, what's blocking? Always correct even if a `payee.updated` webhook was missed. `blockers` is a list of machine-readable reasons (e.g. `kyc_incomplete`, `not_payable`). **Required scope:** `payees:read`.