Payment Testing Guide
How to Test Stripe Payment Links with Playwright: Complete 2026 Guide
A scenario-by-scenario walkthrough of testing Stripe Payment Links with Playwright. Prefilled URL parameters, custom fields, quantity adjustments, hosted checkout on Stripe's domain, success and cancel URL redirects, and the pitfalls that silently break payment link test suites.
“Stripe processed over one trillion dollars in total payment volume in 2023, and Payment Links account for a rapidly growing share of that volume as businesses adopt no-code checkout flows.”
Stripe 2024 Annual Letter
Stripe Payment Link End-to-End Flow
1. Why Testing Stripe Payment Links Is Harder Than It Looks
Stripe Payment Links are no-code checkout URLs that Stripe hosts entirely on its own domain. When a customer clicks a payment link, the browser navigates to checkout.stripe.com, which renders a fully hosted payment form. Your application never sees the checkout page DOM. That cross-domain handoff is the first structural challenge for automated testing: your Playwright test starts on your domain, transitions to Stripe's domain for the entire payment flow, and then lands back on your domain at the success or cancel URL.
The complexity goes deeper than a simple redirect. Payment Links support prefilled URL parameters (customer email, quantity overrides, locale, promotional codes), custom fields that collect additional data before payment, adjustable quantities on the hosted page, and both success and cancel URL redirects with session ID placeholders. Each of these features introduces its own testing surface. A prefilled email parameter that renders correctly in development might silently fail in production if the URL encoding is wrong. A custom dropdown field might not appear if the payment link was created without it enabled in the Stripe Dashboard.
There are five structural reasons this flow is hard to test reliably. First, the checkout page lives on Stripe's domain, so you cannot control the DOM, inject test helpers, or mock network calls. Second, Stripe uses iframes for sensitive card fields (card number, CVC, expiry), and those iframes have their own origin and security restrictions. Third, prefilled parameters are passed as URL query strings that Stripe parses and renders server-side, so you cannot verify them until the page loads. Fourth, the success URL redirect includes a {CHECKOUT_SESSION_ID} template variable that Stripe replaces at redirect time, meaning your test cannot predict the final URL. Fifth, webhooks fire asynchronously after payment confirmation, so verifying the end-to-end flow requires polling or listening on your webhook endpoint.
Stripe Payment Link Redirect Flow
Payment Link URL
User clicks link
Stripe Hosted Page
checkout.stripe.com
Card Iframe
Secure card entry
Payment Processing
Stripe processes charge
Webhook Event
checkout.session.completed
Success Redirect
Back to your app
Prefilled Parameter Resolution
Construct URL
Append query params
Stripe Parses
Server-side rendering
Prefilled Fields
Email, qty, promo code
Customer Sees Form
Some fields read-only
A thorough Stripe Payment Links test suite covers all of these surfaces. The sections below walk through each scenario you need, with runnable Playwright TypeScript code you can paste directly into your project.
2. Setting Up a Reliable Test Environment
Stripe provides a complete test mode that mirrors production behavior without moving real money. Every Stripe account has test mode API keys (prefixed with sk_test_ and pk_test_) and a set of test card numbers that simulate various payment outcomes. Payment Links created with test mode keys behave identically to production links, including the hosted checkout page, redirect behavior, and webhook events.
Stripe Payment Link Test Setup Checklist
- Use Stripe test mode API keys (sk_test_ and pk_test_ prefix)
- Create a dedicated test product and price in the Stripe Dashboard
- Create a Payment Link from the test price with custom fields enabled
- Configure success_url with {CHECKOUT_SESSION_ID} placeholder
- Configure cancel_url pointing to your cancellation page
- Set up a webhook endpoint for checkout.session.completed events
- Install Stripe CLI for local webhook forwarding during development
- Store the Payment Link URL and API keys as environment variables
Environment Variables
Stripe CLI for Local Webhook Forwarding
The Stripe CLI forwards webhook events from Stripe's servers to your local development server. This is essential for testing the full payment flow end to end, because the checkout.session.completed event is how your backend knows a payment succeeded.
Playwright Configuration for Stripe Payment Links
Stripe's hosted checkout page uses iframes for card entry fields. Playwright needs a generous navigation timeout to handle the redirect to Stripe's domain and the subsequent redirect back to your success URL. The card fields live inside a nested iframe, so your tests will use frameLocator to interact with them.
Stripe Test Card Numbers
Stripe publishes a comprehensive set of test card numbers for simulating different payment outcomes. These are the ones most relevant to Payment Link testing.
3. Scenario: Basic Payment Link Happy Path
The foundational scenario for any Payment Link test suite is the happy path: navigate to the payment link URL, fill in the card details on Stripe's hosted checkout page, submit payment, and verify you land on the success URL. This is your smoke test. If this breaks, no customer can complete a purchase through your payment link.
Basic Payment Link Happy Path
StraightforwardGoal
Navigate to a Stripe Payment Link, complete payment with a test Visa card, and confirm the browser redirects to your success URL with a valid checkout session ID.
Preconditions
- Payment Link created in Stripe test mode with a success URL configured
- App server running to serve the success page at the configured URL
- Stripe CLI forwarding webhooks (optional for this scenario, but recommended)
Playwright Implementation
What to Assert Beyond the UI
- The URL contains a valid
cs_test_session ID - Your backend received the
checkout.session.completedwebhook (verified in Section 8) - The checkout session status is
completewhen retrieved via Stripe API
Happy Path: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import { TEST_CARDS } from '../helpers/stripe-test-data';
test('payment link happy path', async ({ page }) => {
await page.goto(process.env.PAYMENT_LINK_URL!);
await page.waitForURL(/checkout\.stripe\.com/);
await page.waitForLoadState('networkidle');
await page.getByLabel('Email').fill('test@example.com');
const cardFrame = page.frameLocator(
'iframe[name*="__privateStripeFrame"]'
).first();
await cardFrame.getByPlaceholder('1234 1234 1234 1234')
.fill('4242424242424242');
await cardFrame.getByPlaceholder('MM / YY').fill('12/34');
await cardFrame.getByPlaceholder('CVC').fill('123');
await page.getByTestId('hosted-payment-submit-button')
.click();
await page.waitForURL(/\/success/, { timeout: 45000 });
expect(page.url()).toContain('session_id=cs_test_');
});4. Scenario: Prefilled URL Parameters
Stripe Payment Links accept URL query parameters that prefill fields on the hosted checkout page. The most common prefill parameters are prefilled_email, prefilled_promo_code, and quantity. These parameters are appended to the payment link URL as standard query strings. For example: https://buy.stripe.com/test_abc123?prefilled_email=user@co.com. Stripe parses these server-side and renders the checkout page with the values already populated.
Testing prefilled parameters matters because encoding errors are silent. If your application constructs the payment link URL dynamically and fails to properly encode an email address with a plus sign (like user+tag@example.com), the checkout page may show the email with a space instead of a plus, or Stripe may ignore the parameter entirely. The only way to catch these bugs is to verify the rendered form state after the page loads.
Prefilled Email and Promo Code
ModerateGoal
Append prefilled_email and prefilled_promo_code to the payment link URL, verify the checkout page renders with those values prefilled, and complete a payment with the promotional discount applied.
Preconditions
- A promotional code (e.g.,
TESTPROMO) created in Stripe test mode and attached to the payment link - The payment link is configured to allow promotional codes
Playwright Implementation
5. Scenario: Custom Fields and Metadata
Stripe Payment Links support up to two custom fields that appear on the checkout page before the payment form. These fields can be text inputs, dropdowns, or numeric inputs, and the collected data is attached to the checkout session as custom_fields metadata. Common use cases include collecting a company name, order reference number, T-shirt size, or delivery instructions.
Custom fields are configured when creating the Payment Link in the Stripe Dashboard or via the API. They cannot be prefilled with URL parameters. This means your test must interact with these fields on the hosted checkout page itself, which requires knowing the field labels and types ahead of time.
Custom Text and Dropdown Fields
ModerateGoal
Fill in custom fields (a text input for “Company Name” and a dropdown for “T-Shirt Size”) on the checkout page, complete payment, and verify the custom field values are captured in the checkout session metadata.
Playwright Implementation
What to Assert Beyond the UI
- The checkout session's
custom_fieldsarray contains the expected key-value pairs - Your webhook handler correctly parses and stores the custom field data
- Submitting with an empty required custom field shows a validation error
Custom Fields: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
test('custom fields captured', async ({ page }) => {
await page.goto(process.env.PAYMENT_LINK_URL!);
await page.waitForURL(/checkout\.stripe\.com/);
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Company name').fill('Acme Corp');
await page.getByLabel('T-shirt size').selectOption('Large');
const frame = page.frameLocator(
'iframe[name*="__privateStripeFrame"]'
).first();
await frame.getByPlaceholder('1234 1234 1234 1234')
.fill('4242424242424242');
await frame.getByPlaceholder('MM / YY').fill('12/34');
await frame.getByPlaceholder('CVC').fill('123');
await page.getByTestId('hosted-payment-submit-button')
.click();
await page.waitForURL(/\/success/, { timeout: 45000 });
const sid = new URL(page.url()).searchParams
.get('session_id')!;
const s = await stripe.checkout.sessions.retrieve(sid);
expect(s.custom_fields).toBeDefined();
});6. Scenario: Quantity Adjustments and Line Item Totals
Payment Links can be configured to allow customers to adjust the quantity of the item on the checkout page. When adjustable_quantity is enabled, the checkout page renders a quantity selector. Customers can also set an initial quantity via the URL parameter ?quantity=5. The total price updates dynamically as the quantity changes.
Testing quantity adjustments catches a category of bugs that are invisible until a customer orders more than one item. The line item total must reflect the correct multiplication, any per-unit discount from a promotional code must scale correctly, and the webhook payload must carry the correct quantity so your fulfillment system provisions the right number of items.
Quantity Adjustment and Total Verification
ModerateGoal
Navigate to a payment link with a prefilled quantity, adjust it on the hosted page, verify the total updates, complete payment, and confirm the checkout session carries the correct quantity.
Playwright Implementation
7. Scenario: Cancel URL Redirect and Back Navigation
Every Payment Link has a cancel URL that Stripe redirects to when the customer clicks the back arrow or closes the checkout. Unlike the success URL, the cancel URL does not receive a session ID parameter, because no payment was completed. Your application should handle this gracefully, typically by showing a message like “Your payment was not completed” and offering a link to try again.
Testing the cancel flow is important because some applications create pending orders or hold inventory when the checkout session starts. If the customer cancels, your backend needs to release that hold. The cancel redirect is also where customers land if their session expires (Stripe checkout sessions expire after 24 hours by default, or after the custom expiration time you set).
Cancel Redirect and Session Expiry
StraightforwardGoal
Navigate to the payment link, click the back/cancel button on Stripe's hosted page, and verify the browser redirects to your configured cancel URL.
Playwright Implementation
8. Scenario: Webhook Verification After Successful Payment
The success URL redirect confirms the customer completed checkout, but the webhook is the authoritative signal that money moved. Stripe sends a checkout.session.completed event to your webhook endpoint after a successful payment. Your backend should use this webhook (not the success URL redirect) to fulfill orders, provision access, or update records. Testing the webhook integration closes the loop on the entire payment flow.
Webhook Receipt and Processing
ComplexGoal
Complete a payment link checkout, then verify that your webhook endpoint received and processed the checkout.session.completed event correctly by checking your application's database or status API.
Playwright Implementation
Alternative: Stripe CLI Trigger
For isolated webhook handler testing without running a full browser checkout, use the Stripe CLI to trigger test events directly.
Webhook Verification: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
test('webhook processed', async ({ page, request }) => {
await page.goto(process.env.PAYMENT_LINK_URL!);
await page.waitForURL(/checkout\.stripe\.com/);
// ... fill card details, submit ...
await page.waitForURL(/\/success/);
const sid = new URL(page.url()).searchParams
.get('session_id')!;
const s = await stripe.checkout.sessions.retrieve(sid);
expect(s.payment_status).toBe('paid');
// Poll for webhook processing
for (let i = 0; i < 10; i++) {
const res = await request.get(
`/api/orders/by-session/${sid}`
);
if (res.ok()) {
const o = await res.json();
if (o.status === 'fulfilled') break;
}
await new Promise(r => setTimeout(r, 2000));
}
});9. Common Pitfalls That Break Payment Link Test Suites
Iframe Selector Fragility
Stripe's card entry fields live inside iframes whose name attribute includes a dynamically generated hash. If you target the iframe by its exact name, your selector will break the next time Stripe updates their checkout page. Use a partial match like iframe[name*="__privateStripeFrame"] instead of matching the full name. Even this selector may change if Stripe refactors their frame embedding strategy. Pin your Playwright version and monitor for breakages after Stripe deploys.
URL Encoding of Prefilled Parameters
If your application dynamically constructs payment link URLs with customer data, encoding errors are the most common source of silent failures. Email addresses with plus signs, non-ASCII characters, and ampersands in company names will all cause issues if not properly encoded with encodeURIComponent. Stripe will either ignore the malformed parameter silently or display a truncated value. Always use encodeURIComponent for every parameter value, and add a test that verifies the rendered value matches the intended input.
Checkout Session Expiry in CI
Stripe checkout sessions expire after 24 hours by default. If your CI pipeline creates a checkout session in a setup step and a later test step runs against it after a delay (due to queue time, flaky retries, or manual approvals), the session may have expired. For Payment Links specifically, each visit to the URL creates a new session, so this is less of a concern than with Checkout Sessions created via the API. However, if you store session IDs between test steps, verify the session is still active before asserting against it.
Race Condition: Success Redirect Before Webhook
The success URL redirect happens almost instantly after Stripe confirms the payment, but the webhook delivery can take one to five seconds (or longer under load). If your success page immediately queries your backend for order status, the webhook may not have arrived yet. Your test will see a “pending” order status on the success page and fail. The fix is twofold: your success page should poll or use a websocket for order status updates, and your test should retry the status check with a reasonable timeout (ten to twenty seconds).
Payment Link Deactivation
Payment Links can be deactivated in the Stripe Dashboard, and once deactivated they return a 404 or an error page. If someone on your team deactivates the test payment link without updating the test environment, your entire test suite will fail with unhelpful timeout errors. Store the payment link ID (not just the URL) in your environment variables, and add a pre-flight check that verifies the link is active before running the suite.
Pre-Flight Checks Before Running Payment Link Tests
- Verify Stripe API keys are test mode (sk_test_ prefix)
- Confirm the payment link is active via Stripe API
- Ensure webhook endpoint is reachable (Stripe CLI or deployed)
- Check that promotional codes are active and attached to the link
- Do NOT hardcode iframe selectors by full name attribute
- Do NOT skip URL encoding for prefilled parameters
- Do NOT assert order status immediately after redirect (allow webhook delay)
- Do NOT use production payment links in test suites
10. Writing These Scenarios in Plain English with Assrt
Every scenario above involves navigating to Stripe's hosted checkout, locating iframes with fragile selectors, filling card fields inside those iframes, waiting for server-side redirects, and then querying the Stripe API to verify backend state. That is a lot of implementation detail for what is conceptually simple: “pay with a test card and confirm the order was created.” Assrt lets you describe the scenario in plain English, handles the iframe detection and card filling automatically, and regenerates selectors when Stripe updates their checkout page layout.
The webhook verification scenario from Section 8 is a strong example. In raw Playwright, you need to know the exact iframe selector pattern, the placeholder text for each card field, the test ID for the submit button, and the URL pattern for the success redirect. You also need to write a polling loop for the webhook processing. In Assrt, you describe the intent and the expected outcome.
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 Stripe updates their iframe structure, changes the submit button's test ID, or modifies the checkout page layout, Assrt detects the failure, analyzes the new DOM, and opens a pull request with updated locators. Your scenario files stay untouched.
Start with the happy path scenario. Once it is green in your CI pipeline, add the prefilled parameter test, then custom fields, then quantity adjustments, then the cancel flow, and finally the webhook verification. In a single afternoon you can have comprehensive Stripe Payment Links coverage that catches encoding bugs, discount calculation errors, and webhook processing failures before they reach production.
Related Guides
How to Test Apple Pay on Web
A practical, scenario-by-scenario guide to testing Apple Pay on the web with Playwright....
How to Test BigCommerce Checkout
A scenario-by-scenario guide to testing BigCommerce checkout with Playwright. Covers...
How to Test Google Pay on Web
Step-by-step guide to testing Google Pay web integration with Playwright. Covers Payment...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.