Skip to navigation

Accept one-time payments

Use Checkout.ApplePayButton to accept one-time payments for physical goods or real-world services using Apple Pay. Payments are processed by Whop at 2.7% + $0.30 instead of Apple’s 15–30% in-app purchase fees.

Selling on the web too? Use checkout links or embedded checkout for 100+ payment methods across 195 countries.

Apple Pay is not for digital content. Only use Checkout.ApplePayButton if your app sells physical goods (merch, food, hardware) or real-world services (event tickets, deliveries). Apps selling digital content — premium features, subscriptions, in-app currency, content unlocks — must use StoreKit. See the Apple Pay eligibility section for details.

What you’ll build

  • A product detail screen with a native Apple Pay button
  • A purchase flow that handles success, failure, and cancellation
  • Plan configuration in the Whop dashboard for your physical goods

Prerequisites

Step 1: Configure the SDK

Initialize the SDK at app launch. For one-time physical goods you don’t need planMappings — that’s only used for StoreKit fallback on subscriptions.

import SwiftUI
import WhopCheckout
@main
struct DropsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(Checkout.shared)
.task {
try? await Checkout.shared.configure(
// companyId: find in Dashboard URL (starts with biz_)
companyId: "biz_xxxxxxxxxxxxxx",
// apiKey: create in Dashboard > Developer with iap:read permission
apiKey: "your_api_key_here"
)
}
}
}
}

See the installation guide for the full setup and security notes on the API key.

Step 2: Add the Apple Pay button

Drop Checkout.ApplePayButton into your product view. The component renders Apple’s official, HIG-compliant Apple Pay button — you can’t substitute a custom-styled button.

import SwiftUI
import WhopCheckout
struct ProductDetailView: View {
let product: MerchItem
@State private var purchaseComplete = false
var body: some View {
ScrollView {
// product image, title, price, size picker...
Checkout.ApplePayButton(
// planId: find in Dashboard > Products > [product] > Plans
planId: product.planId,
label: .buy,
onCompletion: { result in
switch result {
case .success(let purchase):
// purchase.receiptId, purchase.membership
purchaseComplete = true
case .failure(let error):
print("Purchase failed: \(error.localizedDescription)")
}
}
)
.frame(height: 48)
.padding(.horizontal)
}
}
}

When the user taps, they get the native Apple Pay sheet. Face ID or double-click confirms, and the payment flows through Whop.

onCompletion is not called when the user cancels — the sheet just dismisses. No cleanup needed.

Step 3: Choose a button label

The label parameter controls the text on the button. Pick the one that fits the action:

LabelButton textUse for
.buyBuy with Apple PayOne-time merch and physical goods
.checkoutCheck out with…Cart-style checkout flows
.orderOrder with Apple PayFood, deliveries, made-to-order items
.bookBook with Apple PayReservations, appointments, events
.continueContinue with…Multi-step flows where this isn’t final
.plain(Apple Pay logo)Fallback when no verb fits

The full list comes from Apple’s PayWithApplePayButtonLabel — see Apple’s documentation for every option.

Step 4: Handle the purchase result

onCompletion receives a Result<CheckoutPurchaseResult, Error>. On success you get back:

PropertyTypeDescription
receiptIdStringUnique receipt ID for this purchase
membershipCheckoutMembership?The membership Whop created for the purchase

Use this to show a confirmation screen, kick off shipping, or store the receipt for your own records.

onCompletion: { result in
switch result {
case .success(let purchase):
// Persist the receipt for your fulfillment flow
OrderStore.shared.record(
receiptId: purchase.receiptId,
membershipId: purchase.membership?.id,
product: product
)
showConfirmation = true
case .failure(let error):
showError(error.localizedDescription)
}
}

Step 5: Set up your plan

Each purchasable item needs a plan in your Whop dashboard:

1

Create a plan

Go to your Dashboard > Products > select your product > Plans > Create plan.

2

Set it to one-time

Choose One-time payment and set the price.

3

(Optional) Set inventory limits

For limited drops, set a stock cap. The checkout enforces it automatically — when stock runs out, purchases stop.

4

Copy the plan ID

The ID starts with plan_. Pass it to Checkout.ApplePayButton(planId:).

Handle fulfillment with webhooks

For physical goods, you’ll need a server-side webhook to trigger shipping after a successful purchase. Listen for the payment.succeeded event:

import { whopsdk } from "../../../../lib/whop-sdk";
export async function POST(request: Request) {
const body = await request.text();
const headers = Object.fromEntries(request.headers);
const event = whopsdk.webhooks.unwrap(body, { headers });
if (event.type === "payment.succeeded") {
// Trigger your shipping/fulfillment flow
await fulfillOrder(event.data);
}
return new Response("OK", { status: 200 });
}

The client-side onCompletion callback confirms the payment to the user, but your server should handle fulfillment via webhooks — don’t rely on the client alone. See the full webhooks guide for setup.

Complete example

A minimal merch detail view with size selection and Apple Pay:

import SwiftUI
import WhopCheckout
struct ProductDetailView: View {
let product: MerchItem
@State private var selectedSize: String?
@State private var showConfirmation = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
AsyncImage(url: product.imageURL) { image in
image.resizable().scaledToFit()
} placeholder: {
Color.gray.opacity(0.1)
}
.frame(maxWidth: .infinity)
Text(product.title)
.font(.title)
.bold()
Text(product.price, format: .currency(code: "USD"))
.font(.title2)
// Size picker
HStack {
ForEach(product.sizes, id: \.self) { size in
Button(size) {
selectedSize = size
}
.buttonStyle(.bordered)
.tint(selectedSize == size ? .accentColor : .secondary)
}
}
Checkout.ApplePayButton(
planId: product.planId,
label: .buy,
onCompletion: { result in
switch result {
case .success(let purchase):
print("Receipt: \(purchase.receiptId)")
showConfirmation = true
case .failure(let error):
print("Purchase failed: \(error.localizedDescription)")
}
}
)
.frame(height: 48)
.disabled(selectedSize == nil)
}
.padding()
}
.sheet(isPresented: $showConfirmation) {
OrderConfirmationView(product: product, size: selectedSize ?? "")
}
}
}

Next steps