We just shipped a guide on Whop's express checkout button. It is one component that detects the visitor's browser and renders the fastest payment method available: Apple Pay on Safari, Google Pay on Chrome and Android, and the Whop Pay popup everywhere else. You never build a checkout page, and card data never touches your code.
There are two integration paths: a React component for Next.js apps, and a plain web component for everything else (Webflow, Framer, WordPress, static HTML). The guide walks both, plus the server side verification that makes the whole thing safe. Here is the shape of it.
Setup
npm install @whop/sdk @whop/checkout iron-session zod
Four env vars: WHOP_COMPANY_API_KEY, WHOP_SANDBOX, SESSION_SECRET (32+ chars, openssl rand -base64 32), and APP_URL. The guide validates all of them with a Zod schema at first call so a missing value fails loudly instead of surfacing as a mystery 401 later.
Products and plans come from the Whop CLI:
whop products create --title "Pro pass" --description "..." --visibility visible
whop plans create --product_id prod_XXXXXXXXX --plan_type one_time --initial_price 19.99 --visibility visible
The plan_... ID is what the button consumes.
The React path
import { WhopExpressCheckoutButton } from "@whop/checkout/react";
The component wraps the button with a small verification loop. When the buyer pays, onComplete hands you a receipt ID, and the component polls a server route (up to 10 attempts, 2 second intervals) until the payment settles.
The server route is the part worth internalizing. It never trusts anything the browser sends. It takes the receipt ID, retrieves the payment from Whop directly, confirms the payment exists, confirms it bought the right product, and checks the status: pending or open returns a 202 (the client keeps polling), paid writes the confirmation into an encrypted iron-session cookie, anything else fails with a 403. The receipt ID that crosses from browser to server is worthless until Whop confirms what it paid for.
The web component path
For sites that are not React apps, the same button ships as a web component:
<script async defer src="https://js.whop.com/static/checkout/loader.js"></script>
<whop-express-checkout-button
id="buy-button"
plan-id="plan_XXXXXXXXX"
return-url="https://yoursite.com/checkout/complete"
environment="sandbox"
skip-redirect="true"
theme="light"
theme-accent-color="orange"
></whop-express-checkout-button>
The component emits three JavaScript events: express-method-resolved, complete, and payment-error. skip-redirect="true" mirrors the React onComplete behavior, so a Webflow or WordPress page can run the same verify-then-unlock logic with a small script.
The return URL flow
Some payment paths redirect the browser (bank approval steps, or a flow without onComplete). The button takes a returnUrl, and the return page receives three query parameters: status, payment_id, and state_id. The guide's return page verifies the payment server side (same route as the polling path) before showing any confirmation. And for buyers who close the browser mid redirect, registering the payment.succeeded webhook makes sure they still get what they paid for.
Gotchas the guide covers
- Wallet buttons never render on sandbox. On sandbox you always get the Whop Pay button. Apple Pay and Google Pay appear only in production, so the real wallet test happens after the switch.
-
baseURL, capital URL. TypeScript silently acceptsbaseUrlas an unknown property, the SDK falls back to production, and every call 401s as if the key were bad. - Sandbox IDs do not carry to production. Recreate the product and plan with production keys and swap the IDs.
-
API key scopes. The key needs
payment:basic:read,access_pass:basic:read,plan:basic:read,access_pass:create, andplan:create. -
CSP. If your app sends Content-Security-Policy headers:
https://*.whop.comin frame and connect sources,https://js.whop.comin script sources.
Going live
The production checklist: recreate products and plans on whop.com, create a production API key with the same scopes, remove WHOP_SANDBOX, fresh SESSION_SECRET, update APP_URL, register the payment.succeeded webhook, then test on Safari with Apple Pay and Chrome with Google Pay, since sandbox never showed you the wallets.
Links
- Full guide: step by step on the Whop blog
- Demo: nextjs-whop-express-checkout-demo.vercel.app
- Code: github.com/whopio/whop-tutorials/express-checkout
- Whop developer docs: dev.whop.com
If your product has a "buy" moment anywhere in it, this is the shortest path from that moment to a settled payment: one button, browser aware, verified server side.
Top comments (1)
the browser-detection framing is the part i would sanity check before shipping this. apple pay availability is not really a property of safari, it is a property of the user having a card provisioned in wallet, which is why canMakePayments and canMakePaymentsWithActiveCard return different answers and plenty of safari users fail the second one. if the component keys off the user agent rather than the availability callback, those users get a wallet button that does nothing, and that converts worse than the popup they would otherwise have been shown.
related, and it tends to bite at launch rather than in dev: apple pay domain registration is per domain, so staging and production each need their own. your note about wallet buttons not rendering in sandbox reads like the same problem wearing a different hat.