Payments
On this page
Mock payment gateways
Test checkout, refunds and payment webhooks without a payment provider's sandbox account and without real cards. Testlie runs Stripe-, Adyen- and PayPal-compatible mock APIs: point your app's SDK or HTTP client at a gateway's base URL and it answers in the provider's own wire format, with its field names, ids, error shapes and signed webhooks. Test cards decide the outcome, hosted pages stand in for checkout, 3-D Secure and PayPal approval, and every call and webhook is logged.
No money moves and no card data leaves Testlie: the gateways never contact Stripe, Adyen or PayPal. Only test card numbers are accepted; card numbers and security codes are masked before calls are stored. Never enter a real card on a gateway's hosted pages.
Testlie is not affiliated with, endorsed by or sponsored by Stripe, Adyen or PayPal. Their names are trademarks of their owners, used here only to say which API each mock is compatible with.
Create a gateway
Open Payment gateways in the workspace, pick a provider and create a gateway. Each gateway gets:
| Base URL | https://testlie.com/mock/<slug>, used in place of the provider's API host. The slug is 14 hexadecimal characters; copy the whole URL from the gateway's page, or read baseUrl from GET /api/mocks/:id (it is not the gateway's id) |
| Test credentials | The provider's own kind: Stripe API keys and webhook secret, an Adyen API key, merchant account and HMAC key, PayPal client id and secret |
| Webhook URL | Where your app receives the provider's events (optional) |
| Scenarios | Force outcomes, outages, slow responses and webhook problems |
Admins can rotate a credential at any time; the old value stops working at once.
Stripe
Replace https://api.stripe.com with the base URL. The official Node library only lets you change the host, port and protocol, so it can call Testlie's host directly; the gateway's secret key (sk_test_testlie_…) tells the gateways apart:
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
host: "testlie.com",
protocol: "https",
});
const pi = await stripe.paymentIntents.create({
amount: 2000,
currency: "usd",
payment_method: "pm_card_visa",
confirm: true,
automatic_payment_methods: { enabled: true, allow_redirects: "never" },
});
// pi.status === "succeeded"(Add port too when Testlie runs on a port other than 443; the gateway page's snippet has the exact values.)
Python, Ruby, PHP and Go take the full base URL (stripe.api_base, Stripe.api_base, Stripe::$apiBase, stripe.BackendConfig.URL); the gateway page has a snippet for each.
Supported
| Resource | Endpoints |
|---|---|
| PaymentIntents | create (with confirm), retrieve, update, list, confirm, capture (also partial), cancel |
| PaymentMethods | create (type=card), retrieve; pm_card_* test ids and tok_* tokens |
| Charges | retrieve, list (created by PaymentIntents) |
| Refunds | create (full or partial, by payment_intent or charge), retrieve, update metadata, list |
| Customers | create, retrieve, update, delete, list |
| Checkout Sessions | create (mode=payment with price_data line items), retrieve, list, line items, expire, and a hosted checkout page |
| Events | retrieve, list |
| Identity VerificationSessions | create, retrieve (with expand[]=last_verification_report), list, update, cancel, redact, and a hosted capture page; see Stripe Identity |
| Identity VerificationReports | retrieve, list |
Requests are form-encoded as Stripe expects, Idempotency-Key replays the first response for 24 hours, and errors use Stripe's envelope: a declined card is a 402 with type: "card_error", code, decline_code and the payment_intent. Unknown ids answer 404 resource_missing.
Not supported yet: expand[] (except last_verification_report on Identity VerificationSessions), search, subscriptions, invoices, prices and products (use price_data), SetupIntents, Connect, Payment Links and Elements/Stripe.js confirmation in the browser (confirm server-side or use Checkout).
3-D Secure. Cards that require authentication return status: "requires_action" with a next_action.redirect_to_url pointing at a hosted challenge page. Choosing Complete or Fail there sends the browser to your return_url with payment_intent, payment_intent_client_secret and redirect_status, like Stripe.
Checkout. The session's url opens a hosted page with the test cards. Paying redirects to success_url (with {CHECKOUT_SESSION_ID} filled in) and sends checkout.session.completed; cancelling goes to cancel_url.
Webhooks are signed with the gateway's tl_whsec_… secret in a Stripe-Signature header (t=…,v1=…), so stripe.webhooks.constructEvent verifies them. Events: payment_intent.*, charge.succeeded|failed|captured|refunded, refund.created, checkout.session.completed|expired, customer.* and identity.verification_session.*.
Stripe test cards
| Card or PaymentMethod | Result |
|---|---|
4242 4242 4242 4242, pm_card_visa |
Succeeds (also Mastercard 5555 5555 5555 4444, Amex 3782 822463 10005 and the other brands Stripe lists) |
4000 0025 0000 3155, 4000 0027 6000 3184, pm_card_threeDSecure2Required |
3-D Secure, then succeeds |
4000 0082 6000 3178 |
3-D Secure, then declined for insufficient funds |
4000 0000 0000 0002, pm_card_chargeDeclined |
Declined: generic_decline |
4000 0000 0000 9995, pm_card_chargeDeclinedInsufficientFunds |
Declined: insufficient_funds |
4000 0000 0000 9987 / 9979 |
Declined: lost_card / stolen_card |
4000 0000 0000 0069 |
expired_card |
4000 0000 0000 0127 |
incorrect_cvc |
4000 0000 0000 0119 |
processing_error |
4100 0000 0000 0019 |
Blocked as fraudulent |
| Any other valid number | Declined: test_mode_live_card, as Stripe does in test mode |
Adyen
Use the base URL in place of https://checkout-test.adyen.com, keeping the version: https://testlie.com/mock/<slug>/v71/payments. The Node library has no base URL option, so set it on its services:
import { Client, CheckoutAPI } from "@adyen/api-library";
const checkout = new CheckoutAPI(new Client({ apiKey: process.env.ADYEN_API_KEY, environment: "TEST" }));
checkout.PaymentsApi.baseUrl = "https://testlie.com/mock/<slug>/v71";
checkout.ModificationsApi.baseUrl = "https://testlie.com/mock/<slug>/v71";Supported: POST /payments (card scheme payments with raw or test_-encrypted card fields), /payments/details (redirectResult after 3-D Secure), /sessions (with a hosted payment page) and GET /sessions/{id}, and the modifications /payments/{pspReference}/captures, /refunds, /cancels and /reversals. Calls are authenticated with X-API-Key and must name the gateway's merchant account. Errors use Adyen's status, errorCode, message, errorType shape.
As on Adyen, modifications answer 201 with status: "received"; the result arrives as a webhook. Captures happen automatically unless you send captureDelayHours: -1, additionalData.manualCapture: "true" or turn on the authorise only scenario.
Webhooks are standard notifications (live, notificationItems[].NotificationRequestItem) for AUTHORISATION, CAPTURE, REFUND, CANCELLATION and CANCEL_OR_REFUND, each signed in additionalData.hmacSignature with the gateway's HMAC key, so Adyen's hmacValidator verifies them. Reply [accepted].
Not supported yet: other payment methods (iDEAL, wallets, …), /paymentMethods, Drop-in/Components in the browser, tokenization, idempotency keys and the Management API.
Adyen test values
| Value | Result |
|---|---|
4111 1111 4555 1142, 5555 3412 4444 1115, 3700 0000 0000 002 |
Authorised |
4917 6100 0000 0000, 5454 5454 5454 5454, 3714 496353 98431 |
3-D Secure 2 challenge (RedirectShopper) |
Holder name DECLINED, NOT_ENOUGH_BALANCE, CARD_EXPIRED, FRAUD, CVC_DECLINED, BLOCK_CARD, ISSUER_UNAVAILABLE, … |
Refused with Adyen's matching refusalReason and code |
| Any other card number | Refused: Invalid Card Number |
PayPal
Use the base URL in place of https://api-m.sandbox.paypal.com. Get a token with the gateway's client id and secret, then use the Orders API:
curl -u "$PAYPAL_CLIENT_ID:$PAYPAL_CLIENT_SECRET" -d grant_type=client_credentials \
https://testlie.com/mock/<slug>/v1/oauth2/tokenSupported: POST /v1/oauth2/token, Orders v2 (create, get, capture, authorize), GET|capture|void authorizations, GET and refund captures, GET refunds, POST /v1/notifications/verify-webhook-signature and GET /v1/notifications/webhooks-events/{id}. Resending an event through the API is not supported: ignore the resend link in event links and use Send again on the gateway's page instead. Prefer: return=representation returns the full resource. Errors use PayPal's name, message, debug_id, details[].issue shape.
Orders with payment_source.paypal.experience_context (or the older application_context) return PAYER_ACTION_REQUIRED and a payer-action link to a hosted approval page. Approve redirects to your return_url with token and PayerID; Cancel goes to cancel_url; Approve (capture will be declined) makes the capture fail with INSTRUMENT_DECLINED.
PayPal's negative-testing header works too: PayPal-Mock-Response: {"mock_application_codes": "INSTRUMENT_DECLINED"} (or TRANSACTION_REFUSED, INTERNAL_SERVER_ERROR, …) makes that call fail.
Webhooks (CHECKOUT.ORDER.APPROVED, PAYMENT.CAPTURE.COMPLETED|PENDING|DENIED|REFUNDED, PAYMENT.AUTHORIZATION.CREATED|VOIDED) carry PayPal's PAYPAL-TRANSMISSION-ID, -TIME, -SIG, PAYPAL-CERT-URL and PAYPAL-AUTH-ALGO: SHA256withRSA headers. Verify them as on PayPal, either by calling the gateway's verify-webhook-signature endpoint or offline: the signature is RSA-SHA256 over <transmission id>|<transmission time>|<webhook id>|<CRC32 of the body> with the certificate at PAYPAL-CERT-URL. The webhook id is one of the gateway's credentials.
Not supported yet: card payment_source (use PayPal approval), PATCH orders, vaulting, subscriptions and payouts.
Scenarios
Each gateway has scenarios that apply to every new call, for tests that cannot pick a card:
| Scenario | Effect |
|---|---|
| Payment outcome | Follow the test card (default), every payment succeeds, every payment is declined (with the decline code you pick) or every payment needs 3-D Secure / approval |
| API failure | Every call answers 429 or 500 in the provider's error format |
| Response latency | Up to 10 seconds added to every call |
| Asynchronous steps | How long simulated background work takes, such as a Stripe Identity verification's processing. Default 2 seconds, up to an hour |
| Webhooks | Send right away, delay (up to an hour), never send (recorded as dropped), send twice, or sign with a wrong key |
| Authorise only | Stripe and Adyen payments wait for a capture call |
Calls and webhooks
The gateway page lists every API call with its request, response, status and timing, filterable by payment, and every webhook with the headers it was signed with, the attempts, your endpoint's answer and the timing. Webhooks that fail with a network error, 408, 429 or 5xx are retried on the webhook schedule; Send again replays any webhook, freshly signed, to the current webhook URL. Gateway webhooks work on every plan, Free included, and do not count against any quota. The payments tab shows the objects the gateway keeps (payment intents, charges, orders, refunds, customers).
Limits
The number of gateways and of mock API calls per month depend on your plan. Over the monthly limit, calls answer the provider's rate-limit error until the next month. Calls, webhooks and stored objects (payment intents, customers, orders...) follow the plan's retention, which is 3 days on Free; each gateway also keeps at most its newest 5,000 objects. See API mocks.