Test an upsell funnel

Build a one-click upsell funnel and run it end to end in test mode

A one-click upsell shows the buyer a second offer straight after they pay, and charges it without asking for their card again. In this guide you build a two-page funnel with the API, run it in test mode once accepting the offer and once declining it, and check the records each run created. No money is taken.

While test mode is on, anyone who visits these pages gets the products without paying. Switch it off on both pages as soon as you finish testing.

Prerequisites

To get the most out of this guide, you'll need:

  • A Checkout Page API key
  • A product for the first page and one for the offer. The requests below create both, so you only need a name and a price for each.
  • Your Stripe account connected, with card payments turned on. One-click upsells charge the buyer's saved card.

1. Create the upsell page

Create the page that makes the offer first. The first page's upsell step must point at a page that already exists.

curl -X POST https://api.checkoutpage.com/v1/checkout-pages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Workshop recording upsell",
    "productData": {
      "title": "Workshop recording",
      "price": { "amount": 2900, "currency": "usd" }
    },
    "testmode": true
  }'

amount is in the smallest currency unit, so 2900 is $29.00. Note the id in the response. It's the {upsellPageId} in the next steps.

2. Create the first page with an upsell step

Now create the page the buyer starts on, with a funnelSteps entry that points at the upsell page:

curl -X POST https://api.checkoutpage.com/v1/checkout-pages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pricing strategy workshop",
    "productData": {
      "title": "Pricing strategy workshop",
      "price": { "amount": 19900, "currency": "usd" }
    },
    "funnelSteps": [
      {
        "type": "upsell",
        "order": 1,
        "enabled": true,
        "config": { "action": "checkout", "pageId": "{upsellPageId}" }
      }
    ],
    "testmode": true
  }'

Note the id and url in the response. The id is the {firstPageId} in the next steps.

The checkout action shows the offer on the same checkout, using the page in pageId. In the dashboard, this is One-click upsell set to Show one-click upsell, with After payment step set to Show upsell on same page.

A checkout action upsell picks up test mode from the first purchase, so switching on the first page alone is enough for a test run. Switching both, as here, also lets you test the upsell page on its own.

To add an upsell to a page you already have, send funnelSteps in a PATCH to /v1/checkout-pages/{pageId}. An update replaces the whole funnelSteps array, so include every step you want to keep.

3. Run the funnel and accept the offer

  1. Open the first page's url. Check that you see the Test payments only banner.
  2. Pay with a new email address and the test card 4242 4242 4242 4242, with any future expiry date and any CVC.
  3. When the offer appears, accept it. It goes through without asking for the card again.

4. Run it again and decline

  1. Open the first page's url again.
  2. Pay with a different new email address and the same test card.
  3. When the offer appears, decline it with the No thanks, I don't want this link, or the label you set for it.

5. Check the records

List the test payments on each page with livemode=false:

# The first page: one payment per run
curl "https://api.checkoutpage.com/v1/payments?pageId={firstPageId}&livemode=false" \
  -H "Authorization: Bearer YOUR_API_KEY"

# The upsell page: one payment, from the run where you accepted
curl "https://api.checkoutpage.com/v1/payments?pageId={upsellPageId}&livemode=false" \
  -H "Authorization: Bearer YOUR_API_KEY"

What to expect:

  • The first page has two test payments, one for each email address.
  • The upsell page has one test payment, from the run where you accepted. It has upsell: true, upsellChargeId set to that run's first payment, and upsellPageId set to the first page, where the funnel started.
  • Declining creates no record, so the run where you declined has nothing on the upsell page.
  • Without livemode=false, a live API key leaves these payments out of both lists.

The upsell payment, trimmed:

{
  "data": [
    {
      "id": "{upsellPaymentId}",
      "pageId": "{upsellPageId}",
      "customerEmail": "first-run@example.com",
      "amount": 2900,
      "currency": "usd",
      "status": "paid",
      "livemode": false,
      "upsell": true,
      "upsellChargeId": "{firstPaymentId}",
      "upsellPageId": "{firstPageId}"
    }
  ]
}

If the offer sells a subscription, the upsell record is a subscription: list /v1/subscriptions?pageId={upsellPageId}&livemode=false instead. If the first page sells a subscription, the upsell record has upsellSubscriptionId, the first subscription, in place of upsellChargeId.

6. Switch test mode off

Switch test mode off on both pages when you finish. While it's on, anyone who visits gets the product without paying.

curl -X PATCH https://api.checkoutpage.com/v1/checkout-pages/{firstPageId} \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "testmode": false }'

curl -X PATCH https://api.checkoutpage.com/v1/checkout-pages/{upsellPageId} \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "testmode": false }'

Upsells on your own site (redirect action)

With the redirect action, the offer lives on your own site: the buyer is sent to config.redirect.url, a page of yours with a Checkout Page embed or popup for the offer. In the dashboard, this is One-click upsell set to Show one-click upsell, with After payment step set to Redirect to url with upsell.

{
  "funnelSteps": [
    {
      "type": "upsell",
      "order": 1,
      "enabled": true,
      "config": {
        "action": "redirect",
        "redirect": { "url": "https://example.com/special-offer" }
      }
    }
  ]
}

To test it, follow the steps above with these differences:

  • Switch test mode on for both pages: the first page, and the page embedded at the redirect URL. A redirect upsell doesn't pick up test mode from the first purchase.
  • Match the URL exactly. The URL in the step must match the page the checkout is embedded on: same origin and same path, including any trailing slash. https://example.com/special-offer/ doesn't match https://example.com/special-offer, and https://www.example.com doesn't match https://example.com. The query string is ignored.
  • Use one checkout host. Both steps must load the checkout from the same host: your custom domain or your checkoutpage.com subdomain, not one of each. Check the checkout URL in both embed codes.
  • Stay on one site. Embed the first checkout and the offer on the same website.

Test with an AI agent

If you use the Checkout Page MCP server, the test_upsell_funnel prompt tests a funnel you already have. It doesn't create pages, so build the funnel first, with steps 1 and 2 or in the dashboard. Give it the first page's id as initialPageId, and optionally the offer's page as upsellPageId. Your AI assistant then:

  1. Finds the first page's upsell step. If there is no enabled upsell step, it stops and tells you.
  2. Notes both pages' current test mode, then switches test mode on for both.
  3. Runs the funnel once accepting and once declining the offer, each with a new email address.
  4. Checks the records with livemode: false.
  5. Sets each page's test mode back to the value it noted. A page that was already in test mode stays in test mode.

In Claude Code, for example, type the prompt name followed by the first page's id:

/mcp__checkoutpage__test_upsell_funnel {firstPageId}

In clients without prompts, ask in plain words, for example "Test the upsell funnel on my Pricing strategy workshop checkout in test mode."

To pay, your assistant needs a browser tool, such as Claude in Chrome, a Playwright MCP server or ChatGPT agent mode. Without one, it gives you the link to pay yourself, then carries on.

Troubleshooting

The upsell only works when both steps load from the same checkout host (your custom domain or your checkoutpage.com subdomain, not a mix), and the buyer reaches the offer within an hour of the first payment. Most problems come down to one of these rules.

The offer never appears

  • The two steps load the checkout from different hosts. Use the same one for both.
  • More than an hour passed between the first payment and the offer.
  • Redirect action: the URL in the step doesn't exactly match the page the offer is embedded on. Check the trailing slash, www. and https.
  • The upsell step isn't enabled, or isn't the first enabled upsell step in funnelSteps.

The offer asks for the card again

  • One-click charges only work when the first payment was made by card. After Apple Pay, Google Pay, Link or a bank payment, the offer shows a card form. Test purchases never charge a card, so this shows up on live payments only.
  • In a test run, a card form usually means the offer page loaded as a normal checkout instead of an offer. Check the rules under "The offer never appears".

Declining the offer does nothing

If No thanks, I don't want this (or your own label for it) does nothing, the offer can't find the first payment. This happens when the two steps load the checkout from different hosts, or when the embeds sit on different websites. Load both from the same checkout host, on the same site.

Next steps

On this page