Skip to content

Whop Checkout

Connect Whop as an alternative payment method in Spark CRM and implement the embedded checkout, upsells, subscriptions, and refunds.

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.


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 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
  1. 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.

  1. 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: 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.

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:

{
  "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>:

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

Then mount the element:

<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:

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.

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:

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 — 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.


Navigation

Type to search…

↑↓ navigate↵ selectEsc close