We just shipped a guide on Whop's global payouts API: paying sellers, creators, or vendors across 200+ countries from a Next.js app. Sellers verify identity and pick how they get paid inside an embedded element. Your server sends money with one call, and webhooks track every payout until the funds land. You never build payout forms and never store banking data.
The account model
Four pieces to keep straight. Your platform is an account (a biz_ ID). Each seller becomes a connected account: a child account created under your platform. A payout method is the seller's saved receiving preference (a bank account, an IBAN, a wallet address; what is available varies by destination). A payout is the actual transfer, tracked from request to arrival.
Setup
npm install @whop/sdk @whop/embedded-components-react-js @whop/embedded-components-vanilla-js zod
Env vars: WHOP_COMPANY_API_KEY, WHOP_PLATFORM_ACCOUNT_ID (biz_...), WHOP_WEBHOOK_SECRET, DEMO_SELLER_EMAIL, WHOP_SANDBOX, SESSION_PASSWORD (32+ chars), APP_URL. The guide validates all of them with Zod at startup.
The SDK client carries the usual gotcha: the option is baseURL with a capital URL (lowercase is ignored), and the value must end with /api/v1 (https://sandbox-api.whop.com/api/v1 on sandbox).
Step 1: create the seller
const seller = await whop().companies.create({
title,
country: parsed.data.country as never,
parent_company_id: env.WHOP_PLATFORM_ACCOUNT_ID,
email: demoEmail(env.DEMO_SELLER_EMAIL, sellerRef),
send_customer_emails: false,
metadata: { seller_ref: sellerRef },
});
Three details that bite: the email must be a real, active mailbox (Whop validates delivery), two sellers under the same platform cannot share a name, and send_customer_emails: false keeps Whop from mailing your sellers directly.
Step 2: a scoped session token
The embedded components authenticate with a scoped access token minted by your server, carrying only the payout permissions the seller needs. Pass the token to components as a function, not a string, so the components refresh it themselves before it expires.
Step 3: embedded verification and payout method setup
The screens the seller sees are drop in components: wrap the flow in WhopElementsProvider (with environment set to sandbox or production), then render AddPayoutMethodElement inside a PayoutsSession scoped to the seller's companyId. The seller completes identity verification and saves how they want to get paid without leaving your app, and without your database ever seeing an account number.
If your app sends CSP headers, allow https://apollo.elements.whop.com in script-src, frame-src, and connect-src.
Step 4: live fee quotes before sending
payouts.methods.list({
account_id: state.sellerId,
include_available: true,
amount: desiredAmount,
currency: "usd",
})
Every destination comes back with its actual cost. In the guide's example, a $100 transfer ranges from $0.20 to $13.26 depending on where it lands. Show sellers the real number before sending; the platform decides per payout whether to absorb the fee.
One gotcha: a seller who has not finished verifying returns an empty list with a 200, which reads like an unsupported country but is not.
Step 5: send the money
const withdrawal = await whop().withdrawals.create({
amount: parsed.data.amount,
company_id: state.sellerId,
currency: "usd",
payout_method_id: state.payoutMethodId,
platform_covers_fees: parsed.data.platformCoversFees,
idempotency_key: paymentRecordId,
});
Amounts are in dollars, not cents, so 250.00 sends $250. The idempotency_key is what makes retries safe: reuse your payment record's ID and a network hiccup can never double pay a seller. The response returns status: "requested" and a wdrl_ ID.
Step 6: track it with webhooks
The webhook handler verifies the signature with whop().webhooks.unwrap(raw, { headers }), filters for withdrawal. events, and dedupes deliveries on the webhook-id header. A payout progresses from requested to in_transit to completed; failures carry an error_code and error_message so you can tell the seller what happened.
Sandbox to production
Everything runs on the Whop sandbox first. The switch: create a production Company API key and webhooks on whop.com, remove WHOP_SANDBOX, update APP_URL, confirm capabilities.standard_payout is enabled on the live account, and resolve anything in required_actions. The Whop CLI checks account status in one command:
whop accounts get --account_id biz_your_account --format json
Links
- Full guide: step by step on the Whop blog
- Demo: nextjs-whop-payouts-demo.vercel.app
- Code: github.com/whopio/whop-tutorials/tree/main/global-payouts
- Whop developer docs: docs.whop.com
If your product owes money to people in other countries, this is the version of that problem where the seller onboards themselves and your server sends one idempotent call.
Top comments (1)
The
platform_covers_feesboolean is doing a lot more work than its position in the code suggests. We run affiliate software for SaaS on Stripe, so we owe commissions to people in a lot of countries, and the awkward part is that the fee gets quoted at payout time but the amount was promised at earning time. If the platform absorbs it, cost per payout is set by where each person happens to bank, which you do not control and cannot forecast, and the $0.20 to $13.26 spread you show on a $100 transfer is the entire issue. If the seller absorbs it, two people who did identical work take home materially different amounts because of their destination corridor, and the one on the expensive end will eventually ask why. That decision belongs in the promise you make at earning time rather than in a per-payout flag, and the two numbers have to agree or somebody gets surprised. Curious whether you have seen a clean way to close that gap.