Payment Testing Guide

How to Test Lemon Squeezy Checkout with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Lemon Squeezy checkout with Playwright. Overlay iframes, sandbox test cards, subscription billing, one-time purchases, license key delivery, custom checkout fields, and webhook verification for production-grade e2e coverage.

$500M+

β€œLemon Squeezy has processed over half a billion dollars in creator revenue, handling payments, tax compliance, and license fulfillment for thousands of digital product businesses worldwide.”

Lemon Squeezy

0xiframe nesting depth
0Checkout scenarios covered
0%Fewer lines with Assrt
0sAvg test execution time

Lemon Squeezy Checkout Flow

BrowserYour AppLS OverlayLS Payment APIWebhook EndpointClick Buy / SubscribeOpen overlay iframeRender checkout formFill card + submitProcess paymentPayment confirmedShow success + license keyPOST webhook eventActivate license / entitlement

1. Why Testing Lemon Squeezy Checkout Is Harder Than It Looks

Lemon Squeezy is a merchant of record that handles payments, tax collection, and digital product delivery for software creators. Unlike Stripe, where you embed elements directly and have full DOM control, Lemon Squeezy renders its entire checkout experience inside an overlay iframe injected into your page. That iframe is cross-origin, served from *.lemonsqueezy.com, which means Playwright cannot directly query elements inside it using standard locators. You must use frameLocator() to pierce the iframe boundary, and the overlay itself may contain nested iframes for the payment card input (powered by Stripe under the hood).

The complexity compounds in five specific ways. First, the overlay iframe loads asynchronously after a JavaScript initialization call, so your test must wait for both the overlay container and the inner checkout form to become interactive. Second, Lemon Squeezy uses a sandbox environment with test mode API keys, but the sandbox behavior differs from production in subtle ways: webhook delivery timing, license key generation delays, and subscription lifecycle events all behave slightly differently. Third, the checkout supports both one-time purchases and recurring subscriptions, and each type has a distinct post-purchase confirmation flow. Fourth, license key delivery happens asynchronously after the payment completes, so your test cannot simply assert the key on the success screen without waiting for the fulfillment callback. Fifth, custom checkout data (fields you add via the API or dashboard) renders dynamically inside the overlay, and the field selectors are generated rather than semantic.

These five structural challenges mean that a naive approach of clicking buttons and filling inputs will produce flaky tests that fail intermittently in CI. The sections below address each challenge with specific, runnable Playwright code that handles the iframe nesting, async timing, and sandbox quirks correctly.

Lemon Squeezy Checkout Overlay Architecture

🌐

Your Page

LemonSqueezy.Url.Open() call

πŸ“¦

Overlay Injected

Cross-origin iframe

βš™οΈ

Checkout Form

Email, name, card fields

πŸ“¦

Nested Stripe Frame

Card number, expiry, CVC

πŸ’³

Payment Processed

Stripe charges via LS

βœ…

Success Screen

Receipt + license key

Sandbox vs Production Differences

πŸ”’

API Keys

Test mode prefix: test_

πŸ””

Webhooks

Delayed delivery in sandbox

πŸ“§

License Keys

Generated but not enforceable

πŸ’³

Card Validation

Uses Stripe test card numbers

βš™οΈ

Tax Calculation

Simplified in sandbox mode

A solid Lemon Squeezy test suite covers all of these surfaces. The sections below walk through each scenario with runnable Playwright TypeScript you can copy directly into your project.

2. Setting Up a Reliable Sandbox Environment

Lemon Squeezy provides a test mode that mirrors the production checkout flow without processing real charges. Every Lemon Squeezy store has a test mode toggle in the dashboard. When test mode is active, the API uses test-prefixed keys, checkout overlays display a β€œTEST MODE” banner, and all payments use Stripe test card numbers. You need to configure your store, create test products, and set up your environment variables before writing any scenarios.

Lemon Squeezy Sandbox Setup Checklist

  • Enable test mode in the Lemon Squeezy dashboard (Settings > General)
  • Copy your test mode API key (starts with test_)
  • Create a test product with at least one variant (one-time and subscription)
  • Enable license key generation on the one-time purchase variant
  • Configure a webhook endpoint URL pointing to your local tunnel or CI preview
  • Subscribe to order_created, subscription_created, and license_key_created events
  • Note the signing secret for webhook verification
  • Add custom checkout fields if testing custom data collection

Environment Variables

.env.test

Lemon Squeezy API Helper for Test Setup

Before each test run, use the Lemon Squeezy API to create fresh checkout URLs programmatically. This avoids relying on hardcoded checkout links that can expire or change when you update product configurations. The API also lets you pre-populate customer email and custom data, which reduces the number of fields your test needs to fill inside the overlay.

test/helpers/lemonsqueezy-api.ts

Playwright Configuration for Lemon Squeezy

The checkout overlay is a cross-origin iframe, so Playwright needs permission to interact with frames from *.lemonsqueezy.com. Set generous timeouts for iframe loading and payment processing, because sandbox payments can take 3 to 8 seconds to confirm.

playwright.config.ts
Verifying Sandbox Configuration

3. Scenario: One-Time Purchase Happy Path

The most common Lemon Squeezy flow is a single product purchase. The user clicks a buy button on your site, the checkout overlay slides in from the right side of the screen, the user enters their email and card details, submits payment, and sees a success confirmation. The critical testing challenge here is the iframe nesting: the overlay itself is one iframe, and the card input fields live inside a second, nested Stripe iframe within the overlay. Your test must pierce both layers using chained frameLocator() calls.

1

One-Time Purchase Happy Path

Moderate

Goal

Starting from your product page, open the Lemon Squeezy checkout overlay, complete a purchase with a test card, and confirm the success screen shows a receipt with the correct product name and price.

Preconditions

  • App running at APP_BASE_URL with Lemon Squeezy JS SDK loaded
  • Test product published in sandbox with a one-time variant
  • Stripe test card 4242 4242 4242 4242 available

Playwright Implementation

checkout-onetime.spec.ts

What to Assert Beyond the UI

  • The Lemon Squeezy API returns the order with status paid for the test email
  • Your webhook endpoint received an order_created event
  • The user record in your database has the correct entitlement

One-Time Purchase: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('one-time purchase: happy path', async ({ page }) => {
  const testEmail = `buyer+${Date.now()}@example.com`;
  await page.goto('/products/my-tool');
  await page.getByRole('button', { name: /buy now/i }).click();

  const overlayFrame = page.frameLocator(
    'iframe[src*="lemonsqueezy.com"]'
  );
  await overlayFrame.getByLabel(/email/i).fill(testEmail);

  const stripeFrame = overlayFrame.frameLocator(
    'iframe[src*="js.stripe.com"]'
  );
  await stripeFrame.locator('[name="cardnumber"]').fill('4242424242424242');
  await stripeFrame.locator('[name="exp-date"]').fill('1230');
  await stripeFrame.locator('[name="cvc"]').fill('123');

  await overlayFrame.getByLabel(/name on card/i).fill('Test Buyer');
  await overlayFrame.getByLabel(/zip|postal/i).fill('94105');
  await overlayFrame.getByRole('button', { name: /pay/i }).click();

  await expect(
    overlayFrame.getByText(/thank you|order confirmed/i)
  ).toBeVisible({ timeout: 30_000 });
});
53% fewer lines

4. Scenario: Subscription Checkout with Recurring Billing

Subscription products in Lemon Squeezy follow a different post-purchase flow than one-time purchases. After the initial payment succeeds, Lemon Squeezy creates a subscription object with its own lifecycle: active, paused, cancelled, past_due, and expired. The checkout overlay shows subscription-specific details like billing interval (monthly or yearly), trial period information, and the next billing date. Your test needs to verify not just that the payment succeeds but that the subscription was correctly created with the right plan and interval.

In sandbox mode, Lemon Squeezy does not actually charge the test card on a recurring basis. The subscription is created with an active status, but renewal events must be triggered manually via the API or dashboard. This means your test can verify the initial subscription creation but cannot passively wait for a renewal event. To test renewal, cancellation, or pause flows, use the Lemon Squeezy API to mutate the subscription state and then verify your application responds correctly to the resulting webhook events.

2

Subscription Checkout with Recurring Billing

Moderate

Playwright Implementation

checkout-subscription.spec.ts

Testing Subscription Lifecycle Events

After the initial subscription is created, you can simulate lifecycle events using the Lemon Squeezy API. This lets you test how your application handles cancellation, pause, and renewal without waiting for actual billing cycles.

test/helpers/subscription-lifecycle.ts

Subscription Checkout: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('subscription checkout: monthly plan', async ({ page, request }) => {
  const testEmail = `sub+${Date.now()}@example.com`;
  await page.goto('/pricing');
  await page.getByRole('button', { name: /subscribe monthly/i }).click();

  const overlay = page.frameLocator('iframe[src*="lemonsqueezy.com"]');
  await overlay.getByLabel(/email/i).fill(testEmail);
  await expect(overlay.getByText(/\/month|monthly/i)).toBeVisible();

  const stripeFrame = overlay.frameLocator('iframe[src*="js.stripe.com"]');
  await stripeFrame.locator('[name="cardnumber"]').fill('4242424242424242');
  await stripeFrame.locator('[name="exp-date"]').fill('1230');
  await stripeFrame.locator('[name="cvc"]').fill('123');

  await overlay.getByLabel(/name on card/i).fill('Test Subscriber');
  await overlay.getByLabel(/zip|postal/i).fill('94105');
  await overlay.getByRole('button', { name: /subscribe|pay/i }).click();

  await expect(
    overlay.getByText(/thank you|subscription active/i)
  ).toBeVisible({ timeout: 30_000 });

  const subRes = await request.get(/* ... API call ... */);
  expect(subscription.attributes.status).toBe('active');
});
55% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started β†’

5. Scenario: License Key Delivery and Activation

Many Lemon Squeezy sellers distribute software licenses after purchase. When a buyer completes checkout for a product with license key generation enabled, Lemon Squeezy creates a license key and includes it on the success screen inside the overlay. The key is also delivered via the license_key_created webhook event and is queryable via the API. The testing challenge is that the license key appears asynchronously: the success screen may render before the key is fully generated, especially in sandbox mode where there can be a 1 to 3 second delay.

Your test should verify both the UI display of the license key and the API-level activation flow. Lemon Squeezy provides a license validation endpoint that accepts the key and an instance name, returning activation status, allowed instances, and expiry information. Testing this end-to-end ensures your application correctly stores and validates keys after purchase.

3

License Key Delivery and Activation

Complex

Playwright Implementation

checkout-license-key.spec.ts

What to Assert Beyond the UI

License Key Verification Checklist

  • License key appears on the checkout success screen
  • Key validates successfully via the /licenses/validate endpoint
  • Key activates correctly with an instance name
  • license_key_created webhook event was received by your endpoint
  • Your application database stores the key linked to the customer
  • Attempting to activate beyond the instance limit returns an error

6. Scenario: Custom Checkout Fields and Variants

Lemon Squeezy allows you to add custom data fields to the checkout experience. These fields appear inside the overlay alongside the standard email and card inputs. Common use cases include collecting a GitHub username for a developer tool, a company name for a team license, or a preferred subdomain for a SaaS product. The fields are configured either in the Lemon Squeezy dashboard or programmatically via the API when creating a checkout URL.

Testing custom fields is tricky because their selectors are dynamically generated. Unlike the email field, which has a stable label, custom fields use auto-generated IDs and labels based on the field configuration. Your test must locate these fields by their visible label text rather than relying on stable data attributes. Additionally, product variants (such as different license tiers or seat counts) change the checkout total and may show different custom fields depending on the selected variant.

4

Custom Checkout Fields and Variants

Moderate

Playwright Implementation

checkout-custom-fields.spec.ts

7. Scenario: Declined Card and Error Handling

Testing the error path is just as important as testing the happy path. Lemon Squeezy uses Stripe under the hood, which means you can use the standard Stripe test cards for decline scenarios. The card number 4000 0000 0000 0002 always declines, and 4000 0000 0000 9995 declines with insufficient funds. When a card is declined, the checkout overlay displays an error message and keeps the form active so the user can retry with a different card.

Your test must verify three things: the error message appears with a user-friendly description, the form remains interactive (not locked in a loading state), and no order or subscription was created in the Lemon Squeezy API for the declined attempt. The last point is critical because some payment processors create pending orders on failed attempts, and your application logic should not treat a declined charge as a partial success.

5

Declined Card and Error Handling

Straightforward

Playwright Implementation

checkout-declined.spec.ts

Declined Card Handling: Playwright vs Assrt

test('declined card shows error and allows retry', async ({ page }) => {
  const testEmail = `decline+${Date.now()}@example.com`;
  await page.goto('/products/my-tool');
  await page.getByRole('button', { name: /buy now/i }).click();

  const overlay = page.frameLocator('iframe[src*="lemonsqueezy.com"]');
  await overlay.getByLabel(/email/i).fill(testEmail);

  const stripeFrame = overlay.frameLocator('iframe[src*="js.stripe.com"]');
  await stripeFrame.locator('[name="cardnumber"]').fill('4000000000000002');
  await stripeFrame.locator('[name="exp-date"]').fill('1230');
  await stripeFrame.locator('[name="cvc"]').fill('123');

  await overlay.getByLabel(/name on card/i).fill('Decline Tester');
  await overlay.getByLabel(/zip|postal/i).fill('94105');
  await overlay.getByRole('button', { name: /pay/i }).click();

  await expect(overlay.getByText(/declined|failed|try again/i))
    .toBeVisible({ timeout: 15_000 });
  await expect(overlay.getByRole('button', { name: /pay|retry/i }))
    .toBeEnabled({ timeout: 5_000 });
});
47% fewer lines

8. Scenario: Webhook Verification and Order Fulfillment

Lemon Squeezy delivers order and subscription events via webhooks signed with HMAC-SHA256 using your webhook secret. Your application must verify the signature before processing any event. In a test environment, you have two options: intercept webhook payloads by running a local webhook receiver, or simulate webhook delivery by constructing signed payloads yourself and posting them to your webhook endpoint.

The simulated approach is more reliable in CI because it does not depend on Lemon Squeezy delivering webhooks to your ephemeral test environment. You construct the exact JSON payload that Lemon Squeezy would send, compute the HMAC signature using your webhook secret, and POST it to your endpoint with the correct X-Signature header. This lets you test your webhook handler in isolation and verify that it correctly processes each event type.

6

Webhook Verification and Order Fulfillment

Complex

Playwright Implementation

webhook-verification.spec.ts

9. Common Pitfalls That Break Lemon Squeezy Test Suites

These pitfalls come from real issues reported in the Lemon Squeezy community forums, GitHub discussions, and Playwright issue tracker. Each one can silently break your test suite if you are not aware of it.

Pitfall 1: Forgetting the Nested Stripe Frame

The most common failure is trying to fill card details directly inside the Lemon Squeezy overlay iframe. The card input fields live in a second iframe embedded by Stripe, nested inside the LS overlay. You need two levels of frameLocator(): one for the LS overlay and one for the Stripe elements frame. If you only use one, Playwright will time out waiting for a [name="cardnumber"] selector that does not exist at that iframe depth.

Pitfall 2: Overlay Load Timing

The Lemon Squeezy overlay does not appear immediately when you call LemonSqueezy.Url.Open() or click a checkout link. The JavaScript SDK injects the overlay iframe asynchronously, and the iframe itself then loads the checkout page from Lemon Squeezy servers. In CI environments with slower network, this can take 5 to 10 seconds. Always use an explicit waitFor() on the first interactive element inside the overlay (typically the email field) with a generous timeout of at least 15 seconds.

Pitfall 3: Test Mode Detection

If your checkout URL or API key is not in test mode, the overlay will process real charges against real cards. There is no runtime warning in the overlay UI when test mode is off. Protect against this by asserting the presence of the test mode banner at the start of every checkout test, before filling any card details.

Pitfall 4: Webhook Replay and Idempotency

Lemon Squeezy may deliver the same webhook event multiple times, especially in sandbox mode where retry logic is aggressive. If your webhook handler is not idempotent, duplicate deliveries can create duplicate orders, grant multiple license keys, or corrupt subscription state. Always use the meta.event_name and data.id combination as a deduplication key in your handler.

Pitfall 5: License Key Timing in CI

License keys are generated asynchronously after payment confirmation. In production, the key typically appears within milliseconds. In sandbox mode, there can be a 1 to 3 second delay before the key is visible on the success screen or queryable via the API. If your test asserts the license key immediately after seeing the success message, it may fail intermittently. Add a polling mechanism or explicit wait when checking for the license key.

Common Error: Nested Frame Not Found
Lemon Squeezy Checkout Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above requires careful iframe nesting, explicit frame locator chains, and timeout tuning. Multiply that by six checkout scenarios and you have a substantial test file that breaks the moment Lemon Squeezy updates their overlay DOM structure, renames a label, or changes the Stripe integration. Assrt lets you describe the checkout intent in plain English, generates the equivalent Playwright code with correct frame locators, and regenerates the selectors automatically when the overlay changes.

The one-time purchase scenario from Section 3 demonstrates the power of this approach. In raw Playwright, you need to know the exact iframe selector for the Lemon Squeezy overlay, the nested Stripe iframe selector, the specific input names for card fields, and the correct wait timeouts. In Assrt, you describe the intent and the framework resolves all the iframe nesting and timing at runtime.

scenarios/lemon-squeezy-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When Lemon Squeezy updates their overlay iframe structure, renames a button label, or changes the Stripe integration, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.

Start with the one-time purchase happy path. Once it is green in your CI, add the subscription scenario, then the license key verification, then the declined card handling, then the webhook tests. In a single afternoon you can have complete Lemon Squeezy checkout coverage that most SaaS applications never manage to achieve by hand.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk