Skip to main content
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

Webhooks

Handle payment events and trigger fulfillment

Apple Pay eligibility

Confirm your app qualifies under App Store Review Guidelines

Build a paywall

Sell subscriptions instead of one-time purchases

API reference

Full SDK documentation