---
title: How to use the Whop sandbox
slug: whop-sandbox
excerpt: You can test payments end to end on your project without moving real money using the Whop sandbox. Learn how to in this guide.
customExcerpt: You can test payments end to end on your project without moving real money using the Whop sandbox. Learn how to in this guide.
featureImage: "https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/08/HQ-sandbox.png"
status: published
publishedAt: "2026-08-12T17:46:22.000Z"
updatedAt: "2026-08-12T17:49:18.000Z"
createdAt: "2026-08-12T17:46:24.092Z"
tags:
  - { name: Tutorials, slug: tutorials }
  - { name: Developers, slug: developers }
authors:
  - { name: East, slug: east }
  - { name: Destinee Walston, slug: destinee }
---

# How to use the Whop sandbox

## Key takeaways

- You can rehearse every payment path your app has against the Whop sandbox, a complete parallel Whop with its own accounts and fake money, without a registered business or a real card.
- One environment variable does the switching, pointing the SDK at the sandbox API host and the checkout embed at the sandbox frame, so the same code runs in both environments.
- A refund gives the money back but does not revoke access, so the membership stays valid until you cancel it yourself.

<div class="ai-prompt-widget">
  <div class="ai-prompt-widget__header">
    <span class="ai-prompt-widget__icon">✨</span>
    <span class="ai-prompt-widget__title">Build this with AI</span>
  </div>
  <p class="ai-prompt-widget__description">Open the tutorial prompt in your favorite AI coding tool:</p>
  <div class="ai-prompt-widget__buttons" id="ai-prompt-buttons"></div>
</div>

The Whop sandbox is an environment that you can use to see how the Whop integrations and systems work with your project. The sandbox has its own accounts, data, and fake money.

In this tutorial, we're going to point an existing Next.js app at it, test payments, verify a webhook, and show you how you can switch to production at the end.

You can try and see how the sandbox works in our [companion demo](https://nextjs-whop-sandbox-demo.vercel.app) and its [repository](https://github.com/whopio/whop-tutorials/tree/main/whop-sandbox).

## How the sandbox maps to production

Switching between production and sandbox means changing the dashboard and the API host:

<table>
<tbody><tr><th>What</th><th>Production</th><th>Sandbox</th></tr>
<tr><td>Dashboard and checkout</td><td><code>whop.com</code></td><td><code>sandbox.whop.com</code></td></tr>
<tr><td>API base URL</td><td><code>https://api.whop.com/api/v1</code></td><td><code>https://sandbox-api.whop.com/api/v1</code></td></tr>
<tr><td>Checkout embed script</td><td><code>js.whop.com</code></td><td><code>js.whop.com</code>, unchanged</td></tr>
</tbody></table>

![Breakdown of Whop hosts](https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/08/SandboxGuide-hosts.webp)

And you don't have to rewrite any of this since the endpoints, the SDK calls, and the webhook messages are identical. The only thing that knows which environment you're in are your environment variables.

## Set up

To set up the Sandbox, first, you should go and sign up at [sandbox.whop.com](https://sandbox.whop.com). This will be a fresh account, even if you have an account on Whop.com, since the production and sandbox accounts are separate.

Once you have an account, you should create a Whop using the Start a business button on the left sidebar, then go to its dashboard and copy the company ID from the URL (starts with `biz_`).

Then, in the dashboard, go to the Developer page and create a company API key using the Create button under the Company API keys section.

While creating, give the API key these permissions: `payment:basic:read`, `payment:manage`, `plan:create`, and `access_pass:create`.

Now, install the packages needed:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">Terminal</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-bash">npm install @whop/sdk @whop/checkout zod</code></pre>
  </div>
</div>

Then add these to `.env.local`:

<table>
<tbody><tr><th>Variable</th><th>Example</th><th>How to get it</th></tr>
<tr><td><code>WHOP_COMPANY_API_KEY</code></td><td><code>apik_...</code></td><td>Sandbox dashboard &gt; Developer &gt; Company API keys.</td></tr>
<tr><td><code>WHOP_COMPANY_ID</code></td><td><code>biz_...</code></td><td>From the sandbox dashboard URL.</td></tr>
<tr><td><code>WHOP_SANDBOX</code></td><td><code>true</code></td><td>Set manually. Remove it in production.</td></tr>
<tr><td><code>WHOP_WEBHOOK_SECRET</code></td><td><code>ws_...</code></td><td>Created with the webhook endpoint below. Leave unset until then.</td></tr>
<tr><td><code>APP_URL</code></td><td><code>http://localhost:3000</code></td><td>Your app origin, used for the embed's return URL.</td></tr>
</tbody></table>

## The SDK client

Now, let's create the file that decides which Whop you're talking to. Go to `lib/` and create a file called `whop.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">whop.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import Whop from &quot;@whop/sdk&quot;;
import { getEnv } from &quot;@/lib/env&quot;;

let cached: Whop | null = null;

export function getWhop(): Whop {
  if (cached) return cached;
  const env = getEnv();
  cached = new Whop({
    apiKey: env.WHOP_COMPANY_API_KEY,
    baseURL: env.WHOP_SANDBOX
      ? &quot;https://sandbox-api.whop.com/api/v1&quot;
      : &quot;https://api.whop.com/api/v1&quot;,
  });
  return cached;
}</code></pre>
  </div>
</div>

![Breakdown of Whop sandbox flags](https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/08/SandboxGuide-sandbox-flag.webp)

> 

## Create a product to test with

Now, before we can start testing, we need something to buy. So, let's go to your new whop's dashboard and create a product (in the Products section) with a one-time $10 price.

Then, copy its `prod_` and `plan_` IDs using the context menu of the product listing (in the Products and Checkout links) sections and copy those IDs into a constants file.

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">whop-ids.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">export const WHOP_IDS = {
  productId: &quot;prod_XXXXXXXXXXXXX&quot;,
  planId: &quot;plan_XXXXXXXXXXXXX&quot;,
} as const;</code></pre>
  </div>
</div>

If you'd rather stay in the terminal, the [Whop CLI](https://whop.com/blog/cli-guide/) creates the same product and plan.

One thing to know first: the CLI talks to production by default and has no sandbox mode, so every command needs `WHOP_API_BASE_URL` pointed at the sandbox API, including the login that saves your key.

Leave it off a single command and your sandbox key goes to production, where it fails with a 401.

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">Terminal</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-bash">npm install -g @whop/cli

export WHOP_API_BASE_URL=https://sandbox-api.whop.com/api/v1

whop auth login --method api-key --api-key apik_XXXXXXXX --profile sandbox
whop products create --title &quot;Demo product&quot;
whop plans create --product_id prod_XXXXXXXXXXXXX --plan_type one_time --initial_price 10</code></pre>
  </div>
</div>

> 

## Take a test payment

When testing payments in sandbox, instead of real cards, we're going to use Whop's test cards.

There are four of them for different outcomes and all of them can be used with any future expiry and any CVC:

<table>
<tbody><tr><th>Card number</th><th>What it does</th></tr>
<tr><td><code>4242 4242 4242 4242</code></td><td>Payment succeeds.</td></tr>
<tr><td><code>4000 0000 0000 0002</code></td><td>Payment declines. A payment record is still created and <code>payment.failed</code> fires.</td></tr>
<tr><td><code>5385 3083 6013 5181</code></td><td>Requires 3D Secure. Approving the challenge completes the payment.</td></tr>
<tr><td><code>4000 0000 0000 0341</code></td><td>Saving the card succeeds, but every charge on it fails.</td></tr>
</tbody></table>

![Breakdown of Whop sandbox test cards](https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/08/SandboxGuide-test-cards.webp)

When embedding a checkout, keep in mind that it takes an `environment` prop, which is the client-side part of the `WHOP_SANDBOX` flag.

Go to `app/test-lab/` and create a file called `page.tsx`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">page.tsx</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-tsx">import { SandboxCheckout } from &quot;@/components/SandboxCheckout&quot;;
import { WHOP_IDS } from &quot;@/constants/whop-ids&quot;;
import { getEnv } from &quot;@/lib/env&quot;;

export default function TestLabPage() {
  const env = getEnv();
  return (
    &lt;main&gt;
      &lt;h1&gt;Sandbox test lab&lt;/h1&gt;
      &lt;SandboxCheckout
        planId={WHOP_IDS.planId}
        environment={env.WHOP_SANDBOX ? &quot;sandbox&quot; : &quot;production&quot;}
        returnUrl={`${env.APP_URL}/test-lab`}
      /&gt;
    &lt;/main&gt;
  );
}</code></pre>
  </div>
</div>

`SandboxCheckout` is a component in our repo that renders Whop's `WhopCheckoutEmbed` and passes the receipt ID to a route that confirms it with `payments.retrieve`.

Now, go to `/test-lab` and pay with the 4242 card. The payment should appear on the Whop dashboard instantly.

> 

## Break payments on purpose

Now, let's test failed payments. This is one of the most important steps you should follow so that you know exactly what happens in failed payments.

When you pay with the decline card, the checkout should show a decline message and stay open. Whop records the payment still, and fires `payment.failed`, so a declined card proves your failure handling runs.

When you try the 3D Secure card, you'll see a bank challenge inside the checkout. You'll be able to see the password in the hint text. Once you approve, the payment will be completed normally.

Saving a card and charging it are two different actions, and this card, `4000 0000 0000 0341`, allows you to test saving the card while failing to charge. On a regular checkout it looks like a decline so you should use it on a free trial, where the card is accepted, and the first renewal fails.

## Refund it

![Breakdown of refund process on Whop sandbox](https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/08/SandboxGuide-refund-access.webp)

The refund flow is usually quite complex and can cost real money. With the Whop sandbox, however, you can do all the refund tests for free.

On the server you can refund with a single line: `await whop.payments.refund(receiptId)`. Refunds settle in the background so you should read the payment a moment later.

It lands on `status: "paid"` with `substatus: "refunded"` and `refundable: false`.

One thing you should keep in mind is that refunds don't remove access and memberships stay valid. If the payment unlocked content, you can relock it with `memberships.cancel`.

## Verify your webhooks

![Breakdown of Whop's webhook paylods](https://storage.ghost.io/c/12/7b/127b828b-bdc2-4972-9cf2-de857df9c324/content/images/2026/08/SandboxGuide-webhook-delivery-1.webp)

Webhooks are how Whop lets your app know when something happens. After you register a URL, Whop sends a payload to it every time one of those events happens.

To test it, you should deploy the app for a public URL, then create a webhook on the Developer page of your whop dashboard. While creating, you should subscribe to the events `payment.succeeded`, `payment.failed`, and `refund.created`.

Make sure the URL the webhook points at is your URL plus `/api/webhooks/whop`. Once the webhook is created, put its secret (starts with `ws_`) in `WHOP_WEBHOOK_SECRET`.

Now, go to `app/api/webhooks/whop/` and create a file called `route.ts`:

<div class="ucb-box">
  <div class="ucb-header">
    <span class="ucb-title">route.ts</span>
    <button class="ucb-copy" onclick="
      const code = this.closest('.ucb-box').querySelector('code').innerText;
      navigator.clipboard.writeText(code);
      const originalText = this.innerText;
      this.innerText = 'Copied!';
      setTimeout(() => this.innerText = originalText, 2000);
    ">Copy</button>
  </div>
  <div class="ucb-content">
    <pre class="ucb-pre"><code class="language-typescript">import Whop from &quot;@whop/sdk&quot;;
import { getEnv } from &quot;@/lib/env&quot;;

export async function POST(request: Request) {
  const env = getEnv();
  if (!env.WHOP_WEBHOOK_SECRET) {
    return new Response(&quot;Webhook secret not configured&quot;, { status: 503 });
  }

  const whop = new Whop({
    apiKey: env.WHOP_COMPANY_API_KEY,
    webhookKey: Buffer.from(env.WHOP_WEBHOOK_SECRET).toString(&quot;base64&quot;),
  });

  const bodyText = await request.text();
  const headers = Object.fromEntries(request.headers);

  let event: ReturnType&lt;typeof whop.webhooks.unwrap&gt;;
  try {
    event = whop.webhooks.unwrap(bodyText, { headers });
  } catch {
    return new Response(&quot;Invalid signature&quot;, { status: 401 });
  }

  switch (event.type) {
    case &quot;payment.succeeded&quot;:
      console.error(&quot;[whop] payment.succeeded&quot;, event.data.id);
      break;
    case &quot;payment.failed&quot;:
      console.error(&quot;[whop] payment.failed&quot;, event.data.id);
      break;
    case &quot;refund.created&quot;:
      console.error(&quot;[whop] refund.created&quot;, event.data.id);
      break;
    default:
      console.error(&quot;[whop] event&quot;, event.type, event.id);
  }

  return new Response(&quot;OK&quot;, { status: 200 });
}</code></pre>
  </div>
</div>

At this point when you make a payment action, like a successful payment with 4242, the `payment.succeeded` event should reach your logs in a second.

> 

## Switch to production

To switch your app to the production environment, the only thing you should swap is the environment variables and the data. You should:

- Create a new production company
- Get its ID (from the dashboard URL)
- Create a new company API key
- Put the new company ID and API key in your env vars
- Remove (or set to `false`) the `WHOP_SANDBOX` variable
- Re-create a product
- Create a new webhook with the same events
- Put the new webhook's secret in `WHOP_WEBHOOK_SECRET`

## There are many more ways you can use Whop

Now you know how to use the Whop sandbox in your projects to test how the Whop rails interact with your app.

Our other tutorials like [integrating a KYC API](https://whop.com/blog/integrate-kyc-api/) into your project or [adding user authentication](https://whop.com/blog/add-user-authentication/) also use the sandbox-first approach.

If you want to learn the entire flow of building a new project from the ground up using Whop rails and sandbox, then switching to production and publishing it, check out our tutorials like [building a Fiverr clone](https://whop.com/blog/build-fiverr-clone/).

To learn more about all the features Whop offers, visit the [Whop developer docs](https://docs.whop.com/).

**[Go to Whop developer docs](https://docs.whop.com/)**
