---
title: "Whop Checkout"
description: "Connect Whop as an alternative payment method in Spark CRM and implement the embedded checkout, upsells, subscriptions, and refunds."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.sparkcrm.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Whop Checkout

Whop is an alternative payment method that runs its own embedded checkout. The customer pays inside a Whop iframe hosted on your funnel page, and Spark CRM records the order, subscriptions, fulfillment, and reporting around it.

**Navigation**: Sidebar > Payments > Alternative Payments

---

## How Whop Differs From a Gateway

Whop is the **merchant of record**. The customer's card is captured inside Whop's checkout and never reaches Spark CRM, so there is no card number to submit and no gateway to route to.

| Behavior | Card gateway | Whop |
|----------|-------------|------|
| Card data | Sent to Spark CRM, forwarded to the gateway | Never leaves Whop's iframe |
| Gateway assignment | Campaign gateway or orchestrator | None — Whop orders resolve to no gateway |
| Orchestrator routing | Yes | No |
| Decline salvage | Yes | No |
| BIN blacklist | Yes | No |
| Approval timing | Synchronous — approved or declined in the API response | **Asynchronous** — accepted first, confirmed by webhook |
| Recurring billing | Spark CRM bills the card on schedule | **Whop owns the schedule** and bills it |
| Refunds / voids | Through the gateway | Through the Whop API |

> **Important**: Because payments settle asynchronously, your funnel must treat a `202` response as *accepted, not yet confirmed* — not as a decline. See [Handling Asynchronous Confirmation](#handling-asynchronous-confirmation).

---

## Prerequisites

Before you begin, you need:

- A **Whop account** with a company — your company ID starts with `biz_`
- A **Whop API key** with permission to create checkouts (`checkout_configuration`, `plan`, and `access_pass`)
- A **Whop webhook** pointed at Spark CRM, and its **signing secret**
- For testing: a separate **sandbox account** at [sandbox.whop.com](https://sandbox.whop.com) with its own API key, company ID, and webhook

> **Note**: Sandbox and production are entirely separate Whop accounts. Credentials, company IDs, plans, and webhooks do not carry over between them.

---

## Step 1: Create the Payment Method

1. Go to **Payments > Alternative Payments**
2. Click **New Payment Method** (or **Create Payment Method** if you have not added one yet)
3. Enter a descriptive **name** (e.g., "Whop Production")
4. Select **Whop** as the payment type
5. Fill in the credentials:

| Field | Description | Required |
|-------|-------------|----------|
| **API Key** | Company API key from your Whop dashboard, under Developer | Yes |
| **Company ID** | Your Whop company identifier, starting with `biz_`, found under Settings | Yes |
| **Environment** | `Sandbox` for testing or `Production` for live payments | Yes |
| **Webhook Secret** | Signing secret of the Whop webhook you create in Step 2 | No, but required for confirmation to work |

6. Click **Create Payment Method**

> **Tip**: The **Environment** setting automatically sets test mode — selecting `Sandbox` enables test mode, selecting `Production` disables it.

### Capabilities

Refunds, voids, and upsells are all supported by Whop and are enabled automatically when you select the payment type. There is nothing to request from Whop and nothing extra to turn on.

---

## Step 2: Create the Webhook in Whop

The webhook is not optional. Whop settles payments asynchronously, and the webhook is what tells Spark CRM that a charge succeeded, a renewal was billed, a refund settled, or a dispute was opened.

1. Copy the **Webhook Notification URL** displayed in the Spark CRM credential form (it looks like `https://api.sparkcrm.io/webhooks/whop`)
2. In your Whop dashboard, create a webhook pointing at that URL
3. Subscribe to **exactly these events**:

```
payment.succeeded                          refund.created
payment.failed                             refund.updated
payment.created                            dispute.created
membership.activated                       dispute.updated
membership.deactivated                     setup_intent.succeeded
membership.cancel_at_period_end_changed
```

> **Important**: `payment.created` is not optional. A payment that Whop denies with its own fraud checks — before it ever reaches a card network — never fires `payment.failed`; the only signal is `payment.created`. Without it subscribed, those orders never decline and stay stuck in a pending state indefinitely.

4. Copy the webhook's **signing secret** back into the **Webhook Secret** field on the payment method and save

> **Note**: Sandbox secrets start with `ws_` and production secrets start with `whsec_`. Both are accepted — paste whichever Whop gives you, exactly as shown.

Deliveries are verified by signature, deduplicated, and safe to redeliver. If a delivery fails, Whop's retry is processed normally.

---

## Step 3: Test the Connection

1. Find the payment method in the list
2. Click the **three-dot menu** (...) and select **Connection Status**
3. Click **Test Now**

| Result | Meaning |
|--------|---------|
| **Successfully connected to Whop** | The key authenticates and can create checkouts. Ready to sell. |
| **Connected, but this API key cannot create checkouts** | The key is valid but lacks permissions. Grant it `checkout_configuration`, `plan`, and `access_pass` in the Whop dashboard. |
| **Failed to authenticate with Whop** | Wrong API key, wrong company ID, or credentials from the other environment. |

---

## Step 4: Implement the Checkout

The Whop flow adds two things to the [standard DTC checkout flow](/integrations/dtc-checkout-flow): you mount Whop's embed instead of collecting card fields, and you call **Capture** when the customer finishes paying.

```
Create Order  (payment.method = "whop")
  ↓
  Response returns plan_id + session_id
  ↓
Mount the Whop embed on your page
  ↓
Customer pays inside the Whop iframe
  ↓
  Capture      (confirms the payment)
  ↓
  Process Upsell   ← repeat per accepted upsell
  ↓
  Complete Order
```

### 4a. Create the order

Call **Create Order** (`POST /checkout/orders`) exactly as you would for a card order, but send `whop` as the payment method. No card fields are sent.

```bash
curl -X POST "https://api.sparkcrm.io/checkout/orders" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
"campaign_id": "1",
"currency": "USD",
"customer": { "email": "customer@example.com", "first_name": "John", "last_name": "Doe" },
"products": [{ "offer_id": "1", "quantity": 1 }],
"payment": {
  "method": "whop",
  "return_url": "https://yourfunnel.com/checkout/complete"
}
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `payment.method` | Yes | Must be `whop` |
| `payment.return_url` | Yes | Where Whop returns the customer after an external payment redirect. The request is rejected with a validation error if omitted. |
| `payment.external_payment_id` | No | The **ID** of a specific Whop connection, as shown in the Alternative Payment Methods list. Send it whenever the team has more than one **Active** Whop connection — without it, the order resolves to an arbitrary Active connection. |

The response carries everything the embed needs:

```json
{
  "success": true,
  "data": {
"order_number": "ORD-260803-00042",
"payment_status": "pending_redirect",
"redirect_required": true,
"redirect_url": "https://whop.com/checkout/ch_XXXXXXXXX",
"checkout": {
  "provider": "whop",
  "session_id": "ch_XXXXXXXXX",
  "plan_id": "plan_XXXXXXXXX",
  "environment": "sandbox",
  "setup_future_usage": "off_session",
  "purchase_url": "https://whop.com/checkout/ch_XXXXXXXXX",
  "prefill": { "email": "customer@example.com", "name": "John Doe" }
}
  }
}
```

> **Note**: `payment_status` is `pending_redirect` and no money has moved yet. Nothing is fulfilled, no subscription exists, and no order webhooks fire until the payment is confirmed.

### 4b. Mount the Whop embed

Use `plan_id` and `session_id` from the response. Passing `session_id` is what ties the payment back to the Spark CRM order, so it must always be included.

**HTML / vanilla JS** — add the loader to your `<head>`:

```html
<script async defer src="https://js.whop.com/static/checkout/loader.js"></script>
```

Then mount the element:

```html
<script>
  window.onWhopComplete = (planId, receiptId) => {
// Send receiptId to your server, which calls Capture with it.
fetch("/your-server/capture", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ order_number: ORDER_NUMBER, payment_id: receiptId }),
});
  };
</script>

<div
  data-whop-checkout-plan-id="plan_XXXXXXXXX"
  data-whop-checkout-session="ch_XXXXXXXXX"
  data-whop-checkout-environment="sandbox"
  data-whop-checkout-setup-future-usage="off_session"
  data-whop-checkout-return-url="https://yourfunnel.com/checkout/complete"
  data-whop-checkout-prefill-email="customer@example.com"
  data-whop-checkout-on-complete="onWhopComplete"
></div>
```

**React**:

```jsx
import { WhopCheckoutEmbed } from "@whop/checkout/react";

<WhopCheckoutEmbed
  planId={checkout.plan_id}
  sessionId={checkout.session_id}
  environment={checkout.environment}
  setupFutureUsage={checkout.setup_future_usage}
  returnUrl="https://yourfunnel.com/checkout/complete"
  prefill={{ email: checkout.prefill.email }}
  onComplete={(planId, receiptId) => captureOnYourServer(receiptId)}
/>
```

Two settings matter for how the rest of the funnel behaves:

- **`setup_future_usage` = `off_session`** — saves the customer's payment method so one-click upsells can charge it without sending them back through checkout. Pass the value returned in the response; omitting it breaks upsells.
- **`environment`** — must match the environment on your connection. A sandbox plan ID will not load in a production embed and vice versa.

> **Tip**: Use the `prefill` values from the response so the customer does not retype the email your funnel already collected.

### 4c. Capture the payment

When the embed reports completion, call **Capture** (`POST /checkout/orders/capture`) from your server.

```bash
curl -X POST "https://api.sparkcrm.io/checkout/orders/capture" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
"order_number": "ORD-260803-00042",
"payment_id": "pay_XXXXXXXXX"
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `order_number` | Yes | The order returned by Create Order |
| `payment_id` | No | The receipt ID from the embed's completion callback. Recommended — it confirms that exact payment instead of searching for one. |

Capture creates the subscription, creates fulfillments, records coupon usage, fires order events, and moves the order to **Processing**.

| Status | Body | What to do |
|--------|------|-----------|
| `200` | `payment_status: "completed"` | Payment confirmed. Advance the customer to the first upsell. |
| `202` | `payment_status: "pending_external_confirmation"` | Whop has the money but has not settled yet. **Not a decline.** Advance the customer; the webhook completes the order. |
| `422` | `Payment capture failed` | The payment genuinely failed, or the receipt ID does not belong to this order. |
| `404` | `No capturable transaction found for this order` | The order has no pending Whop transaction. |

Capture is idempotent — calling it twice returns `Payment already captured` and does not double-charge or duplicate the order. It is also safe to retry a `202`.

---

## Handling Asynchronous Confirmation

Whop returns success before the charge is final. Two things confirm a payment, and either one may arrive first:

1. Your **Capture** call
2. The **`payment.succeeded` webhook**

Spark CRM completes the order exactly once regardless of which wins the race, so you never need to coordinate them.

What this means for your funnel:

- **Never treat `202` as a failure.** The customer has paid. Show them the upsell or confirmation page.
- **Do not block the customer waiting for `completed`.** The webhook will finish the order within seconds.
- **Do not poll Capture in a tight loop.** One call, then let the webhook do its work.

---

## Upsells

One-click upsells charge the payment method Whop saved during checkout. No redirect, no re-entry of card details.

Call **Process Upsell** (`POST /checkout/orders/upsell`) as usual:

```bash
curl -X POST "https://api.sparkcrm.io/checkout/orders/upsell" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
"order_number": "ORD-260803-00042",
"upsell_id": "1",
"quantity": 1
  }'
```

| Status | Meaning |
|--------|---------|
| `202` | Accepted — `payment_status: "pending_external_confirmation"`. Whop confirms by webhook, and the upsell product is added to the order then. |
| `402` | Declined by Whop. Move the customer to the next offer. |
| `422` | Card fields were sent. A Whop order cannot be charged with a new card. |

> **Important**: Do **not** send a `payment` block with card details on a Whop upsell. The order was paid through Whop and must stay on Whop; card fields are rejected with a validation error.

A `202` upsell is not on the order yet. The product, the cart line, and the order total are added when the `payment.succeeded` webhook arrives. If you display an order summary immediately after an upsell, expect the totals to catch up moments later.

---

## Subscriptions

When the cart contains a recurring or trial offer, Spark CRM creates a Whop renewal plan as part of the checkout, and **Whop owns the billing schedule from then on**.

The subscription is marked externally managed, which changes what Spark CRM does:

| Action | Behavior on a Whop subscription |
|--------|--------------------------------|
| Automatic rebilling | Whop bills it. Spark CRM's nightly rebill queue skips it entirely. |
| Manual **Rebill Now** | Not available — "This subscription is managed by a third party and cannot be billed manually." |
| **Cancel** | Sent to Whop first. If Whop rejects it, nothing changes locally and the error surfaces. |
| **Pause / Restart** | Sent to Whop. The un-pause control on the subscription page is labelled **Restart**. |
| **Reactivate** | Available on a cancelled subscription — un-cancels the membership on Whop. |
| **Next billing date** | Mirrored from Whop, never calculated locally. |
| **Sync from Provider** | A button on the subscription page that pulls Whop's current state onto the record. Use it if a webhook was missed. |
| Renewal payments | Recorded as a new rebill order with its own transaction and fulfillment, driven by the `payment.succeeded` webhook. |
| Failed renewals | The subscription moves to **Past Due**. Whop runs its own retries; Spark CRM mirrors the outcome. |

### Trials and pricing

- A **trial offer** sends its trial length to Whop, so the first renewal is delayed until the trial ends.
- The **renewal price** sent to Whop is the recurring price × quantity, plus recurring shipping — matching what a card rebill would charge.

> **Note**: A Whop checkout carries a **single plan**. If one cart mixes several recurring offers with different billing intervals, they become one renewal on the shortest interval. Where separate billing schedules matter, sell those offers in separate orders.

---

## Refunds, Voids, and Disputes

### Refunds

Refund from the order exactly as with any other payment method — full or partial. The refund is submitted to Whop's API.

Because refunds settle asynchronously, the amount recorded when you issue the refund is provisional. The `refund.created` webhook carries the settled amount and corrects the record. If Whop settles for less than requested, the refund transaction is adjusted to the real amount.

### Voids

Void is available on Whop transactions and is processed through Whop's API.

### Disputes

`dispute.created` and `dispute.updated` are handled automatically:

- The order is **flagged for QA** with the dispute status and reason, and a system note is added
- Once the dispute moves past the early-warning stage and is not won, the chargeback is recorded against the transaction

---

## Sandbox Testing

1. Create an account at [sandbox.whop.com](https://sandbox.whop.com) — it is separate from your live account
2. Create a sandbox API key and note the sandbox company ID
3. Create a **separate sandbox webhook** pointing at the same Spark CRM URL, subscribed to the same events
4. In Spark CRM, create a second payment method with **Environment** set to `Sandbox`
5. Set `environment` on the embed to `sandbox` and use the sandbox `plan_id` returned by the API

> **Warning**: A new payment method is created **Active**, and an order that does not pin a connection resolves to an arbitrary Active Whop connection for the team — production or sandbox. Keep only one Whop connection **Active** at a time, or send `payment.external_payment_id` on every live order. A live order that lands on the sandbox connection collects no real money.

### Test cards

| Card | Result |
|------|--------|
| `4242 4242 4242 4242` | Succeeds |
| `4000 0000 0000 0002` | Declines |
| `4000 0000 0000 0341` | Saves successfully, later charges decline — use this to test upsell declines |
| `5385 3083 6013 5181` | Requires 3D Secure (code `Checkout1!`) |

Any future expiry, any CVC, any billing address.

> **Note**: Sandbox supports **card payments only** — no Apple Pay or Google Pay — and payouts are unavailable.

---

## What to Know Before Going Live

- **Whop connections are team-level, not per-campaign.** Unlike gateways, a Whop connection is not assigned to a campaign. If you have more than one, pin the right one per request with `payment.external_payment_id`.
- **Whop orders bypass gateway features.** No orchestrator routing, no decline salvage, no BIN blacklist, no gateway failover — Whop makes the approval decision.
- **Whop's merchant-of-record fees are not imported.** Fees charged by Whop are not reflected in Spark CRM reporting; reconcile them from your Whop dashboard.
- **The webhook is required.** Without a valid signing secret, asynchronous payments, renewals, refunds, and disputes are never confirmed.

---

## Troubleshooting

### "Whop is not configured for this team"

The order was sent with `payment.method: whop` but no **Active** Whop payment method exists for the team. Check the payment method's status, and confirm you are not pinning a deleted or inactive connection with `payment.external_payment_id`.

### Validation error on `payment.return_url`

`return_url` is required for Whop orders. Add it to the `payment` block.

### The embed does not load

- Confirm you passed the `plan_id` returned by Create Order, not a plan ID copied from the Whop dashboard
- Confirm `environment` on the embed matches the environment on the connection — a sandbox plan will not load in a production embed
- Confirm the loader script is present in the page `<head>`

### Orders stay in "Pending External Confirmation"

A confirming webhook is not arriving. Check that:

- The webhook exists in the Whop dashboard for **this** environment and points at the correct URL
- The **Webhook Secret** on the payment method matches that webhook's signing secret exactly
- `payment.succeeded` and `payment.created` are both among the subscribed events
- The Whop dashboard's delivery log shows attempts

### Upsells fail with "No saved Whop payment method found"

The checkout did not save a payment method. Confirm the embed was mounted with `setup_future_usage` set to `off_session` — the value is returned in the Create Order response for exactly this purpose.

### A subscription cannot be cancelled

If you see "This subscription is billed by Whop but has no membership reference", the Whop membership was never linked — usually a missed `payment.succeeded` webhook. Fix the webhook, then use **Sync from Provider** on the subscription page.

### A Whop subscription shows the wrong status or billing date

Click **Sync from Provider** on the subscription page to pull Whop's current state.

---

## Related Topics

- [Alternative Payment Methods](/payment-processing/alternative-payments) — PayPal Wallet and manual payment types
- [Basic DTC Checkout Flow](/integrations/dtc-checkout-flow) — the standard order, upsell, and completion sequence
- [Payment Gateways](/payment-processing/gateways) — traditional credit card processing
- [API Tokens](/settings/api-tokens) — create and manage authentication tokens

Source: https://docs.sparkcrm.io/payment-processing/whop/index.mdx
