Quickstart

Go from zero to your first test-mode payout with the @8x-payout/sdk.

Go from zero to your first test-mode payout. A client in test mode routes every payout and onboarding through the mock provider — no real KYC, no real money — so you can run this entire flow safely before going live.

You need a test-mode client

You need a test-mode client's API key + signing secret (8x provisions these). Put them in your environment as PAYOUT_API_KEY and PAYOUT_SIGNING_SECRET. Never hard-code or commit them.

Install the SDK

The @8x-payout/sdk handles request signing (HMAC), so you never touch the x-8x-timestamp/x-8x-signature headers by hand.

pnpm add @8x-payout/sdk
# or: npm i @8x-payout/sdk

Initialize the client

import { PayoutClient } from '@8x-payout/sdk';

const client = new PayoutClient({
  // Your provisioned gateway base URL (local dev shown here).
  baseUrl: process.env.PAYOUT_BASE_URL ?? 'http://localhost:3007',
  apiKey: process.env.PAYOUT_API_KEY!,        // 8xp_<slug>_…
  signingSecret: process.env.PAYOUT_SIGNING_SECRET!, // 8xs_<slug>_…
});

The flow

Create a payee

Keyed on (yourClient, externalId) — re-sending the same externalId updates the existing payee, never duplicates it. Send email, name, and country when you have them; otherwise the hosted onboarding intake collects and verifies the missing fields before rail handoff.

// 1. Create (or idempotently re-fetch) the payee.
const payee = await client.upsertPayee({
  externalId: 'creator_8842',      // your own stable id for this human
  email: 'creator@example.com',    // optional; hosted intake verifies/collects it if omitted
  name: 'Creator Example',         // optional; hosted intake collects it if omitted
  country: 'TR',                   // optional ISO-2; hosted intake collects it if omitted
  // preferredProvider: 'sideshift', // optional rail pin (stripe | tipalti | sideshift); omit for Grade, the default rail
});
console.log(payee.id); // → the gateway payee id you'll pay

Live Grade payees: create → pay, no redirect

On a live client whose payees route to Grade (the default rail), collect email + name + country on your platform and pass them here — the response comes back payable: true right away, so you skip the onboarding-link step below entirely and go straight to the payout. No redirect to the payouts gateway and no OTP; the creator does identity/KYC + picks a payout method on Grade's own claim link at withdraw, which the gateway emails them. The onboarding-link step (next) is only for hosted-onboarding rails like Stripe, or when you didn't supply the fields above.

The returned url is gateway-hosted and rail-agnostic. The payee first verifies email, confirms name/country if needed, then the gateway routes them to the selected rail. In test mode the final rail is mock; completing it makes the payee payable and fires a payee.updated webhook.

// 2. Get a hosted onboarding link and send the creator to it.
const { url, provider } = await client.getOnboardingLink(
  payee.id,
  'https://yourapp.example.com/payouts/onboarded', // returnUrl on YOUR domain
);
console.log(provider); // → 'mock' for a test-mode client
console.log(url);      // open this in a browser

Wait until payable

In production you react to the payee.updated webhook. For the quickstart we poll getReadiness — the always-correct pull backstop.

Readiness also returns payability, routing, and sideshift* fields (the resolved rail, blocker detail, and any SideShift resume signal) — see the API reference for the full shape.

// 3. Poll readiness until the payee can be paid.
//    (In production you'd react to the `payee.updated` webhook instead of polling.)
let ready = await client.getReadiness(payee.id);
while (!ready.payable) {
  await new Promise((r) => setTimeout(r, 1000));
  ready = await client.getReadiness(payee.id);
}
console.log(ready); // → { payable: true, provider: 'mock', onboardingStatus: 'verified', blockers: [], ... }

Send a $1 test payout

Note what the request does not contain: any bank account or destination. The gateway resolves where the money goes from the payee's KYC-locked identity.

// 4. Send a $1.00 test payout. Idempotent on (yourClient, idempotencyKey).
const txn = await client.payout({
  payeeId: payee.id,
  amountCents: 100,        // integer cents — $1.00
  currency: 'USD',         // ISO 4217
  idempotencyKey: 'quickstart-001', // re-sending returns this same txn, never a 2nd payout
  ref: 'invoice_123',      // your row id, echoed on the payout.status webhook
  reason: 'Quickstart test payout',
});
console.log(txn.status); // → 'paid' (the mock rail settles synchronously)

Read the payout back

// 5. Read it back any time (the pull backstop).
const latest = await client.getPayout(txn.id);
console.log(latest.status); // → 'paid'

Verify the webhook

The gateway pushes a payout.status event when the payout settles and a payee.updated event when onboarding completes. Always verify the signature before trusting the body. See the Webhooks guide for the full scheme, idempotency, and retry behavior.

// In your webhook route (e.g. Next.js / Express). Verify BEFORE trusting the body.
import { PayoutClient } from '@8x-payout/sdk';

const client = new PayoutClient({ baseUrl, apiKey, signingSecret });

export async function POST(req: Request) {
  const rawBody = await req.text(); // the EXACT raw body — do not re-stringify
  const event = await client.verifyWebhook(
    rawBody,
    req.headers.get('x-8x-timestamp')!,
    req.headers.get('x-8x-signature')!,
  ); // throws PayoutError(401) on a bad signature

  if (event.type === 'payout.status') {
    // settle your own ledger row by event.ref
    console.log(event.ref, event.status);
  }
  return new Response('ok'); // any 2xx acknowledges; non-2xx triggers retry
}

No SDK? Sign requests by hand

Every request needs three headers: the bearer key, a Unix-seconds timestamp, and HMAC-SHA256(signingSecret, "{timestamp}.{rawBody}") hex-encoded. For a GET the body is empty, so you sign "{timestamp}.".

API_KEY="8xp_..."          # from your provisioned credentials
SIGNING_SECRET="8xs_..."   # ditto
BASE="https://8x-payout.com"

BODY='{"payeeId":"<uuid>","amountCents":2500,"currency":"USD","idempotencyKey":"inv-2026-07-001"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" -hex | sed 's/^.* //')

curl -sS -X POST "$BASE/api/v1/payouts" \
  -H "Authorization: Bearer $API_KEY" \
  -H "x-8x-timestamp: $TS" \
  -H "x-8x-signature: $SIG" \
  -H "content-type: application/json" \
  --data-raw "$BODY"

The signature covers the exact raw bytes you send, so do not re-serialize the body after signing. The timestamp must be within 5 minutes of the gateway clock, and a POST replayed with the same signature is rejected, so mint a fresh timestamp per request.

Next steps

Browse the full API reference, read the Webhooks guide, then ask 8x to flip your client to live. The contract is identical in live mode — only the rail changes from mock to a real provider (Grade today).