Payment Testing Guide

How to Test Paddle Checkout with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Paddle Checkout with Playwright. The merchant-of-record overlay, sandbox transactions, localized pricing and tax display, subscription lifecycle, license key delivery via webhook, and the pitfalls that break real Paddle test suites.

5,000+

Paddle processes payments for over 5,000 software companies worldwide, acting as the merchant of record and handling tax compliance in 200+ countries and territories.

Paddle

0+Tax jurisdictions handled
0Checkout scenarios covered
0Webhook events verified
0%Fewer lines with Assrt

Paddle Checkout End-to-End Flow

BrowserYour AppPaddle.jsPaddle OverlayPaddle APIWebhook EndpointClick Buy / SubscribePaddle.Checkout.open()Render checkout overlayUser enters payment detailsProcess paymentTransaction approvedShow success screentransaction.completed webhookProvision access / license

1. Why Testing Paddle Checkout Is Harder Than It Looks

Paddle is a merchant-of-record platform, which means Paddle itself is the legal seller of your software. Unlike Stripe or PayPal where you are the merchant and the payment processor handles the transaction, Paddle sits between you and the buyer, handling sales tax, VAT, invoicing, and compliance. This architectural difference has profound implications for testing.

The Paddle Checkout experience loads as an overlay iframe injected by Paddle.json top of your page. Your test cannot directly interact with elements inside the Paddle overlay using standard page locators. You need to locate the iframe, switch into its context using Playwright's frameLocator, and then interact with the form fields inside. The overlay DOM structure is controlled entirely by Paddle and can change without warning.

Beyond the iframe challenge, there are five structural reasons this flow is difficult to test reliably. First, Paddle.js dynamically injects the overlay and its internal structure varies by product type (one-time vs. subscription), payment method, and buyer location. Second, Paddle calculates and displays localized pricing with tax included or excluded depending on the buyer's country, so your assertions must account for variable totals. Third, the sandbox environment uses specific test card numbers and has its own set of behaviors that differ subtly from production. Fourth, critical business logic (license provisioning, access grants) happens asynchronously via webhooks after checkout completion, not in the browser. Fifth, subscription lifecycle events (upgrades, downgrades, cancellations, pauses) are managed through the Paddle API or the hosted customer portal, adding more surfaces to test.

Paddle Checkout Overlay Lifecycle

🌐

Your Page

User clicks pricing CTA

⚙️

Paddle.js

Paddle.Checkout.open()

📦

Overlay Iframe

Checkout form renders

💳

Payment Entry

Card or alternative method

⚙️

Paddle Processes

Tax + payment + invoice

Success Callback

Overlay closes, app updates

🔔

Webhook

transaction.completed fires

Why Paddle Is Different: Merchant-of-Record Model

🌐

Buyer

Pays Paddle, not you

🔒

Paddle (MoR)

Collects payment + tax

⚙️

Tax Authority

Paddle remits VAT/GST

🔔

Your App

Receives webhook + payout

License/Access

You provision the product

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

2. Setting Up Your Paddle Sandbox Environment

Paddle provides a dedicated sandbox environment at sandbox-vendors.paddle.com that mirrors the production experience without processing real payments. Every Paddle account gets sandbox access automatically. Before writing any tests, configure your sandbox with the products, prices, and webhook endpoints your tests will exercise.

Paddle Sandbox Setup Checklist

  • Create a Paddle sandbox account at sandbox-vendors.paddle.com
  • Generate sandbox API keys (client-side token and API key)
  • Create at least one catalog product with a one-time price
  • Create at least one subscription product with monthly and annual prices
  • Set up a notification destination pointing to your test webhook endpoint
  • Configure Paddle.js to use sandbox mode (environment: 'sandbox')
  • Whitelist sandbox test card numbers in your test data fixtures
  • Enable all relevant webhook event types (transaction.completed, subscription.created, etc.)

Environment Variables

.env.test

Paddle.js Initialization

Paddle.js must be initialized in sandbox mode for tests. The initialization happens once when your page loads. Your application code should read the environment and pass the correct token. In your test environment, always use the sandbox client-side token and explicitly set environment: 'sandbox'.

lib/paddle.ts

Playwright Configuration for Paddle

The Paddle overlay is an iframe injected by Paddle.js. Playwright needs enough time for the iframe to load and render its internal form. Set a generous action timeout and ensure your config loads the sandbox environment variables.

playwright.config.ts

Paddle Sandbox Test Card Numbers

Paddle's sandbox accepts specific test card numbers. Unlike Stripe, Paddle does not use the generic 4242 4242 4242 4242 number. The sandbox test cards listed in Paddle's documentation must be used for successful and declined transactions.

Paddle Sandbox Test Cards

3. Scenario: One-Time Purchase Through the Overlay

The most fundamental Paddle test is a one-time purchase that completes successfully. Your test clicks a buy button on your pricing page, waits for the Paddle overlay iframe to appear, fills in the email and card details inside the iframe, submits the payment, and confirms the success state both in the overlay and in your application after the overlay closes.

The critical technique here is using Playwright's frameLocator to reach into the Paddle iframe. The overlay is injected as an iframe with a name attribute or a URL pattern matching paddle.com. Once you have the frame reference, all standard locator methods work inside it.

1

One-Time Purchase: Happy Path

Straightforward

Goal

Starting from your pricing page, complete a one-time purchase through the Paddle overlay using a sandbox test card, and confirm the transaction succeeded in both the overlay and your application's post-purchase state.

Preconditions

  • App running with Paddle.js in sandbox mode
  • A one-time price ID configured in the environment
  • Webhook endpoint listening for transaction.completed

Playwright Implementation

paddle-checkout.spec.ts

What to Assert Beyond the UI

  • Your webhook endpoint received transaction.completed
  • The transaction status in your database is "completed"
  • Any license key or entitlement was provisioned correctly

One-Time Purchase: Playwright vs Assrt

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

test('one-time purchase: happy path', async ({ page }) => {
  await page.goto('/pricing');
  await page.getByRole('button', { name: /buy now/i }).click();

  const paddleFrame = page.frameLocator(
    'iframe[name*="paddle"], iframe[src*="paddle.com"]'
  );

  await paddleFrame.getByPlaceholder(/email/i)
    .fill(`buyer+${Date.now()}@example.com`);
  await paddleFrame.getByRole('button', { name: /continue/i }).click();

  const cardFrame = paddleFrame.frameLocator('iframe[title*="card"]');
  await cardFrame.locator('[name="cardnumber"]').fill('4000 0566 5566 5556');
  await cardFrame.locator('[name="exp-date"]').fill('12/34');
  await cardFrame.locator('[name="cvc"]').fill('100');

  await paddleFrame.getByRole('button', { name: /pay/i }).click();
  await expect(paddleFrame.getByText(/thank you/i))
    .toBeVisible({ timeout: 30_000 });
});
53% fewer lines

4. Scenario: Subscription Signup and Plan Selection

Subscription purchases differ from one-time purchases in several ways. The Paddle overlay displays recurring pricing, a billing interval, and often allows the buyer to toggle between monthly and annual plans. After a successful subscription, Paddle fires both transaction.completed and subscription.created webhooks. Your test needs to verify the correct plan was selected, the recurring total is accurate, and your application creates the subscription record.

A common integration pattern is to pass the priceId to Paddle.Checkout.open(), which pre-selects the plan. Your test should verify that the overlay displays the expected plan name, billing frequency, and total before submitting payment.

2

Subscription Signup with Plan Selection

Moderate

Playwright Implementation

paddle-subscription.spec.ts

Subscription Signup: Playwright vs Assrt

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

test('subscription signup: monthly plan', async ({ page }) => {
  await page.goto('/pricing');
  await page.getByRole('button', { name: /subscribe monthly/i }).click();

  const paddleFrame = page.frameLocator('iframe[name*="paddle"]');
  await expect(paddleFrame.getByText(/month/i)).toBeVisible();

  await paddleFrame.getByPlaceholder(/email/i)
    .fill(`sub+${Date.now()}@example.com`);
  await paddleFrame.getByRole('button', { name: /continue/i }).click();

  const cardFrame = paddleFrame.frameLocator('iframe[title*="card"]');
  await cardFrame.locator('[name="cardnumber"]').fill('4000 0566 5566 5556');
  await cardFrame.locator('[name="exp-date"]').fill('12/34');
  await cardFrame.locator('[name="cvc"]').fill('100');

  await paddleFrame.getByRole('button', { name: /subscribe/i }).click();
  await expect(paddleFrame.getByText(/thank you/i))
    .toBeVisible({ timeout: 30_000 });
});
52% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Localized Pricing and Tax Display

One of Paddle's core features as a merchant of record is automatic tax calculation and localized pricing. When a buyer from Germany opens the checkout, Paddle displays the price in EUR with 19% VAT included. A buyer from the US sees USD with sales tax calculated based on their ZIP code. This localization happens inside the Paddle overlay, and your tests need to verify it works correctly for your key markets.

Playwright can simulate different geolocations using the geolocationcontext option. While Paddle uses the buyer's IP address (not browser geolocation) for tax jurisdiction, you can also pass a locale or address in the Paddle.Checkout.open() call to force a specific country for testing purposes.

3

Localized Pricing: Tax Display by Country

Moderate

Playwright Implementation

paddle-localized-pricing.spec.ts

6. Scenario: Subscription Update and Cancellation

After a subscription is created, your users need to upgrade, downgrade, or cancel. Paddle provides two ways to handle this: through the Paddle API (server-side) or through a hosted customer portal that Paddle renders. Testing the API-driven approach is more reliable for CI because it avoids another set of iframes. The test creates a subscription via the API, then updates or cancels it and verifies the resulting webhook events and your application's state.

4

Subscription Cancellation via API

Complex

Playwright Implementation

paddle-subscription-lifecycle.spec.ts

7. Scenario: Webhook Delivery and License Provisioning

The most critical part of a Paddle integration is what happens after checkout. Paddle sends webhook notifications to your server when transactions complete, subscriptions start, and payments succeed or fail. Your application receives these events and provisions access: creating user accounts, generating license keys, unlocking features, or sending confirmation emails. If your webhook handler breaks, buyers pay but get nothing.

Testing webhooks end-to-end requires either a real webhook delivery (by exposing your local server via a tunnel like ngrok) or simulating the webhook payload in your test. The Paddle API also provides a Simulation API that can replay events to your endpoint. For CI, the most reliable approach is to verify webhook handling by calling your webhook endpoint directly with a properly signed payload.

5

Webhook Verification and License Provisioning

Complex

Playwright Implementation

paddle-webhook.spec.ts
Webhook Test Run Output

8. Scenario: Declined Payment and Error Recovery

Successful payments are only half the story. Your checkout experience must handle declined cards gracefully, showing the buyer a clear error message and letting them retry with a different card. Paddle's sandbox provides a specific declined test card number that triggers a predictable failure. Your test should verify that the error message appears inside the overlay and that no transaction record is created in your application.

6

Declined Payment and Error Recovery

Moderate

Playwright Implementation

paddle-declined.spec.ts

Declined Payment: Playwright vs Assrt

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

test('declined card: shows error', async ({ page }) => {
  await page.goto('/pricing');
  await page.getByRole('button', { name: /buy now/i }).click();

  const paddleFrame = page.frameLocator('iframe[name*="paddle"]');
  await paddleFrame.getByPlaceholder(/email/i)
    .fill(`declined+${Date.now()}@example.com`);
  await paddleFrame.getByRole('button', { name: /continue/i }).click();

  const cardFrame = paddleFrame.frameLocator('iframe[title*="card"]');
  await cardFrame.locator('[name="cardnumber"]').fill('4000 0000 0000 0002');
  await cardFrame.locator('[name="exp-date"]').fill('12/34');
  await cardFrame.locator('[name="cvc"]').fill('100');

  await paddleFrame.getByRole('button', { name: /pay/i }).click();

  await expect(paddleFrame.getByText(/declined|failed/i))
    .toBeVisible({ timeout: 15_000 });
});
51% fewer lines

9. Common Pitfalls That Break Paddle Test Suites

These are real problems pulled from GitHub issues, community forums, and production Paddle integrations. Each one has derailed a test suite that was working fine in development.

Pitfalls to Avoid

  • Using page.locator() instead of frameLocator() for elements inside the Paddle overlay. The overlay is an iframe, and standard page locators cannot reach its content. This is the single most common mistake.
  • Hardcoding price amounts in assertions without accounting for tax. Paddle displays tax-inclusive or tax-exclusive prices depending on the buyer's location, so your expected total may differ between test runs if the geolocation changes.
  • Not waiting for the Paddle overlay iframe to fully load before interacting. The iframe is injected asynchronously by Paddle.js and may take several seconds to render, especially in CI environments with slower network.
  • Forgetting that Paddle sandbox and production use different API endpoints and client tokens. A test passing against sandbox will silently fail if you accidentally deploy with production tokens in a test environment.
  • Ignoring webhook signature verification in tests. If your test sends unsigned webhooks, your handler accepts them, but production webhooks from Paddle are always signed. You should test the signature verification path.
  • Running parallel tests that create subscriptions with the same email. Paddle deduplicates by email in certain flows, causing unexpected 'customer already exists' errors.
  • Assuming the overlay DOM structure is stable. Paddle controls the checkout UI and updates it without notice. Use resilient selectors (roles, placeholders, text content) rather than fragile CSS class selectors.
  • Not cleaning up sandbox subscriptions between test runs. Accumulated test subscriptions can cause webhook storms and slow down subsequent test runs.
Common Error: Locator Timeout on Paddle Overlay

10. Writing These Scenarios in Plain English with Assrt

Every Playwright scenario above requires you to know the exact iframe selectors, the Paddle overlay structure, the correct test card numbers, and the webhook signature algorithm. With Assrt, you describe the checkout behavior in plain English and Assrt generates the Playwright TypeScript, including the frameLocator wiring, the sandbox card data, and the webhook verification logic.

Here is the full Assrt scenario file for a complete Paddle checkout test suite. Each scenario block compiles into real Playwright tests that run in CI just like the hand-written versions above.

scenarios/paddle-checkout.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 Paddle updates their overlay structure or changes their iframe attributes, Assrt detects the failure, analyzes the new DOM, and opens a pull request with 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 signup, then the declined card scenario, then the localized pricing verification, then the webhook integration test. In a single afternoon you can have complete Paddle 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