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
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/sdkInitialize 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 payLive Grade payees: create → pay, no redirect
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.Get an onboarding link
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 browserWait 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).