Article
Making payment method testing repeatable: building the Adyen Payment Simulator
Getting a green happy-path response during integration testing doesn't prove your integration will hold up under the conditions that actually matter at go-live. Learn why we built the Adyen Payment Simulator to help developers test payment scenarios repeatably.

Adyen supports merchants around the world, and our catalog of payment methods keeps growing. For merchants integrating with us, payments are the heart of the business, and the code that handles them is among the most consequential software they ship. A broken payment path isn't just an inconvenience; it's lost revenue and lost shopper trust.
Getting a green happy-path response during integration testing doesn't prove your integration will hold up under the conditions that actually matter at go-live. In live, many payment methods finish outside your checkout: the shopper approves in their banking or payment app, and your integration hears about the result later. A test that only checks the first response never sees that. How a payment finishes varies by payment method. That's why we built the Adyen Payment Simulator: to give developers and QA engineers a way to test the most common success and failure scenarios on demand, with the same result every time, from authorized, refused and canceled payments to post-payment modifications such as capture and refund. That way they can build confidence in their integration, before launch and whenever they need to reproduce a known outcome later, without setting up a separate test environment for every payment method.
Payment methods don't behave alike
A basic card payment can be completed in a single request and a single response. Many other payment methods can't. For those, the flow a merchant tests against follows a lifecycle: an initial call, an additional action the integration must perform, an asynchronous result, and a final state the merchant has to commit. Different payment methods interrupt that lifecycle in different places.
Three concrete examples:
UPI Intent, India’s real-time bank-to-bank payment method, where the shopper is redirected to a UPI app, the initial response is pending, and the result is established only when the integration receives a webhook message with the AUTHORISATION event type.
UPI QR, also on UPI, where the shopper scans a QR code to complete the payment in a UPI app, again with the outcome established through the same webhook message.
IRIS, a bank payment method in Greece, where the integration must handle both an issuer redirect and a QR-code additional action, and where the webhook message is also the authoritative outcome if the shopper does not return from the redirect.
Here is how this looks in practice with UPI Intent.
Your integration makes its usual /payments or /sessions request.
The response comes back as pending, with an action for the shopper to complete, which your integration, Drop-in or Components handles.
Instead of a real UPI app, the tester lands on a simulator page and selects the outcome: authorized, refused or canceled.
The simulator then reports the outcome to the Adyen platform, which sends your integration the AUTHORISATION webhook message.
Your integration then commits the resulting state, and you can go on to test post-payment modifications such as capture, refund or cancel.
We group these variations into a small number of broad flow shapes, for example, redirect and QR flows. What matters for the simulator is that the lifecycle contract is the same across these shapes: a known outcome, a continuation the integration must handle, and a webhook message that establishes the result, even where the continuation surfaces differ.
What a payment test must control
A payment test can pass and still miss the bugs that only show up in live. Three examples:
It never reaches a step it depends on, such as the shopper returning from a redirect.
It checks that the request succeeded but not that the order was updated afterwards.
It treats a pending response as final and ships the order too early.
In short, a payment test fails for one of three reasons: the conditions were not under the test's control, the assertion looked only at the first response, or the assertion never checked the merchant state that should follow the outcome.

Figure: the payment lifecycle for a payment method that finishes outside checkout. The numbers mark where each of the three failures below arises: (1) the conditions are not under the test’s control, (2) the assertion looks only at the first response, (3) the merchant state is never checked.
The first failure is the one the simulator is specifically designed to address. Adyen's general testing guidance is clear that a payment test should handle a redirect return path where one applies, and should establish the outcome through the AUTHORISATION webhook message. UPI's documentation is more specific: for UPI Intent, "the payment response is pending until the shopper completes payment in the UPI app" and the integration must wait for the AUTHORISATION webhook message. If a test relies on an external environment to produce that outcome at the right moment, an important variable has moved outside the suite's reach.
The second failure is about scope. A test that asserts the request returned the expected response shape can pass while leaving the integration's webhook handling, persistence, and checkout behavior completely untested. The lifecycle assertion is the one that catches an integration bug: given the response, the integration performed the next required action and ended in the correct merchant state.
The third failure is about state. In an asynchronous flow, a pending response is not necessarily an error. It is sometimes the correct intermediate state. An integration that treats it as final, whether by releasing an order too early or by leaving an order unresolved after the webhook message arrives, has a bug, not a quirk. The assertion for this third failure is: after the lifecycle resolves, the merchant's payment, checkout, and business state reflect the documented outcome.
The Payment Simulator's design follows directly from this. By separating the selection of a simulated outcome from the merchant's integration code, we let a test suite drive a known path through the lifecycle every run, and assert the state at each point.
Why we added a simulation layer alongside external sandboxes
Our earlier engineering blog on the Indian card payment simulator makes the case more concretely for one specific context. The team needed to simulate complex card-payment flows end to end, and wanted a consistent way to drive each outcome on demand. That same article describes deterministic simulation, with predefined test card details that map to specific payment outcomes, as part of a way to test end to end.
The same principle applies beyond that single context. For merchants’ testing and our own, we needed three things: a way to exercise a payment flow whether or not a dedicated test environment is available for it, a setup light enough for an automated test to carry, and coverage of more than a single happy path. When any of these is missing, an automated suite cannot reliably exercise the lifecycle it is meant to assert.
External sandboxes remain useful, and a payment method provider's own sandbox in particular can produce a more faithful end-to-end shopper experience once the shopper leaves the merchant's page. The cost is one integration per provider, and that cost grows with every provider you add. A simulation surface owned by us reduces that to a single integration for the flows it covers.
This mirrors Adyen’s broader proposition: a merchant should not have to integrate with every local or alternative payment method directly, or build a separate test setup for each one. The Payment Simulator takes the same shape of problem one level down.
These needs made it clear that we also wanted a test surface we control, alongside external sandboxes. To build a test surface that could scale across a rapidly growing catalog of payment methods, we had to establish clear design boundaries. Those requirements shaped the two core principles behind the Adyen Payment Simulator.
The Adyen Payment Simulator's design principles
We arrived at two principles for the simulator over time. We state them as principles rather than features because each one has a trade-off, and the trade-off is the more important engineering fact.
Outcome selection over state injection. The simulator exposes a small set of explicit outcomes the tester can drive, rather than a full payment engine that tracks the payment’s state through a long sequence of inputs. The UPI flow described above works this way. Outcome selection is more constrained, but it is also what makes assertions simple, repeatable, and reviewable.
Common lifecycle, payment-method-specific continuation. The lifecycle contract (known outcome, continuation, webhook message) is the same across methods; what differs is the additional action (a redirect, a QR scan, or another surface) that the integration must handle between the request and the webhook message. This separation is the design move that lets one simulator serve many payment methods while keeping each method's differences visible.
An architecture for many payment methods
The principles tell us what to simulate. Keeping them intact as the catalog grew meant balancing simulation accuracy, payment method coverage, and the cost of maintaining the simulator. As a result, the simulator could not remain as one implementation per payment method. The duplication of a lifecycle harness per method becomes a maintenance liability as the catalog grows, and it forces every payment method team to learn the simulator's internal contract.
The current design puts a response adapter for each payment method in front of the simulation logic. The adapter shapes the request and response for that payment method: what the shopper sees, the action type returned to the integration, and the response body and additional data the integration expects. Payment methods with common flows share generic simulation logic, while a payment method with more specific behavior, such as UPI, gets dedicated logic. Webhook message delivery is shared: whichever path handled the request, the simulator reports the outcome to the Adyen platform, which sends the AUTHORISATION webhook message to the integration endpoint.

Figure: simplified simulator architecture. A response adapter per payment method shapes each request and response, simulation logic runs behind it, and a shared component reports the outcome to the platform, which sends the AUTHORISATION webhook message to the integration endpoint.
The principle here is the same one Adyen's documentation describes for IRIS and UPI: the lifecycle contract is identical across payment methods, and the parts that differ (the redirect, the QR code) stay visible rather than hidden. Pushing too much into the shared abstraction would flatten those differences into a single happy-path mock and miss the transitions that make payment integrations difficult. Leaving too much in the per-method implementations would duplicate work that does not need to be done twice.
Each limit has a "go elsewhere" answer. Per-method provider sandboxes give the most faithful shopper experience beyond the merchant page. Quirks specific to a single payment method are deliberately left out because simulating them would add complexity to the whole simulator. Load tests give the throughput characteristics. Production telemetry gives the failure modes you cannot pre-script. The simulator's job is the layer in between: integration behavior over a documented lifecycle under known conditions.
Lessons
Some lessons are specific to Adyen's platform, and some are transferable to any system where the catalog of variants grows faster than the engineering team.
Test the payment lifecycle, not the first response. A green response shape does not tell you whether the integration will hold up when the webhook message arrives, and a webhook-driven outcome does not tell you whether the merchant state was correctly persisted. The lifecycle assertion is the one that catches integration bugs.
One shared harness cannot do the work of every domain-specific surface; neither can a separate harness per variant. The right separation is to share whatever is genuinely common behind a single interface and keep whatever is genuinely different explicit at the boundary. Payment methods are a useful example for any growing catalog of variants.
Coverage is a scaling problem before it is a product problem. We designed the Payment Simulator in its current shape to deliver broad coverage across a growing catalog of payment methods, while keeping depth where it matters most. The "broad quickly, deep selectively" trade-off is one that comes back if a system outgrows its initial abstraction.
If you are integrating a payment method in the Adyen TEST environment, check the test section of the integration guide for that payment method, since simulator availability depends on the payment method. For example, see how to test UPI payments with the Adyen Payment Simulator.