Test Stripe Payments — PaymentIntents, 3DS and Webhooks
Stripe's requires_action
fails silently. Test yours
before your users find it.
When a bank requires 3DS, Stripe sets the PaymentIntent to requires_action. If your code doesn't read next_action and redirect the customer, the payment stalls. No error. No log entry. Just a broken checkout.
6
PI statuses
5
webhook events
0
accounts needed
{
"status": "requires_action",
"next_action": {
"type": "redirect_to_url",
"redirect_to_url": {
"url": "https://bank.../3ds..."
}
}
}
// redirect customer to url ↑ for 3DS
Stripe moved from the Charges API to PaymentIntents in 2019. The main reason was 3D Secure. European banks started requiring it under SCA (Strong Customer Authentication), and the old Charges API had no way to handle it. Every PaymentIntent goes through up to six statuses. Your code needs to handle all of them — not just succeeded.
The most common mistake is ignoring requires_action. When a bank needs 3DS, Stripe sets the PaymentIntent to that status and puts the bank's authentication URL inside next_action.redirect_to_url.url. You have to redirect the customer there. If you don't, the payment stalls. No error appears. No log entry. The customer sees a broken checkout and you have no idea why.
This mock lets you trigger every status an auto-capture checkout runs through, on demand. Pick the outcome on the hosted checkout page — requires_action, succeeded, declined, canceled — and your webhook endpoint gets a real event. No Stripe account, no test card numbers needed.
Deciding where this fits next to Stripe's own test environment? Compare Stripe test mode with MockGateway.
Why does requires_action break Stripe payments for European customers?
When you build a Stripe integration in the US and test with US cards, everything works. You create a PaymentIntent, confirm it, get succeeded. Done.
Then a customer in Germany pays. The bank requires 3D Secure. Stripe moves the PaymentIntent to requires_action and sets next_action.redirect_to_url.url to the bank's 3DS page. Your code never reads it. The payment stalls.
You see no error in your logs. The customer sees the checkout spinning or timing out. The PaymentIntent stays at requires_action until it expires. This happens silently in production for every European card that requires SCA.
After confirming a PaymentIntent, always check the status. If it's requires_action, redirect the customer immediately:
// After stripe.confirmCardPayment()
if (pi.status === 'requires_action') {
window.location.href =
pi.next_action.redirect_to_url.url
}
// Then wait for payment_intent.succeeded webhook
// Never fulfill on the return_url callback alone
This mock triggers requires_action on demand so you can validate this redirect path before a real German customer hits it.
Which PaymentIntent statuses should your integration handle?
Every PaymentIntent lives in exactly one of these states. Your integration needs to handle all of them — including the ones you never see when testing with US cards.
requires_payment_methodNo card attached yet
requires_confirmationCard attached, not charged
requires_action3DS challenge needed
processingBank is processing it
requires_captureAuthorized, not captured
succeededFunds captured — done
canceledDead — create a new PI
Important: A PaymentIntent mostly moves forward through this state machine, with one exception you have to handle: a declined card or a failed 3DS challenge sends it back to requires_payment_method, so the same PaymentIntent can be retried with another card. Only succeeded and canceled are terminal — after those you create a new PaymentIntent. requires_action is the only status where the customer must do something on an external page before the payment can proceed.
Which Stripe webhook events does your server need to handle?
Stripe sends these events to your webhook endpoint. The first three are required for a correct integration. The last two are optional but useful for logging and edge case handling.
| Event | When it fires | What to do | Required |
|---|---|---|---|
payment_intent.succeeded | Funds captured | Fulfill the order. This is the only safe fulfillment trigger. | required |
payment_intent.payment_failed | Card declined | Ask the customer to retry with a different card. | required |
payment_intent.requires_action | 3DS challenge triggered | Redirect to next_action.redirect_to_url.url. | required |
payment_intent.processing | Submitted to bank | Show a loading state. Wait for succeeded or failed. | — |
payment_intent.canceled | PI cancelled via API | Log and create a new PI if customer retries. | — |
How do Stripe API requests and responses look?
curl -X POST https://mockgateway.com/api/base/stripe/v1/payment_intents \
-H "Authorization: Bearer YOUR_MOCK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999,
"currency": "usd",
"description": "Acme Pro — monthly",
"metadata": { "order_id": "ORD-001" },
"automatic_payment_methods": { "enabled": true }
}'New to testing webhooks locally? Read how webhook delivery works →
Which Stripe scenarios can this mock test?
Trigger any of these scenarios on demand from the hosted checkout page. No test card numbers needed — just pick the outcome you want to test.
Outcomes you can trigger
- ✓ succeeded — funds captured, fulfill the order
- ✓ requires_action — 3DS triggered, redirect required
- ✓ payment_failed — card declined by bank
- ✓ canceled — PI cancelled, create a new one
- ✓ processing — submitted, waiting for result
Webhook events delivered
- ✓ payment_intent.succeeded
- ✓ payment_intent.payment_failed
- ✓ payment_intent.requires_action
- ✓ payment_intent.processing
- ✓ payment_intent.canceled
3DS / SCA flows
- ✓ Full redirect to 3DS challenge page
- ✓ next_action.redirect_to_url correctly populated
- ✓ Return URL redirect after 3DS complete
- ✓ 3DS authentication failure scenario
Decline reasons
- ✓ insufficient_funds — customer needs another card
- ✓ card_declined — generic bank decline
- ✓ expired_card — card has expired
- ✓ incorrect_cvc — wrong security code
Request parameters
Sent to POST /api/base/stripe/v1/payment_intents
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | integer | required | Amount in smallest currency unit (e.g., 1000 = $10.00 for USD) e.g. 1000 |
currency | string | required | Three-letter ISO currency code in lowercase e.g. usd |
description | string | optional | An arbitrary string for your reference e.g. Payment for Order #12345 |
customer | string | optional | Customer identifier for your reference e.g. cus_12345 |
metadata | object | optional | Key-value pairs for storing additional information e.g. {"order_id": "12345"} |
receipt_email | string | optional | Email address for the receipt e.g. customer@example.com |
Response fields
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the PaymentIntent |
status | enum | Status of the PaymentIntent |
client_secret | string | Secret for client-side confirmation |
created | integer | Unix timestamp of creation time |
latest_charge | string | ID of the latest charge (generated on success) |
Questions about Stripe PaymentIntents
Other gateway templates
Looking for a different provider?
PayPal
Test PayPal payments without credentials.
2Checkout
Test 2Checkout payments without credentials.
Braintree
Test Braintree payments without credentials.
Adyen
Test Adyen payments without credentials.
Square
Test Square payments without credentials.
Mollie
Test Mollie payments without credentials.
Authorize.Net
Test Authorize.Net payments without credentials.
Checkout.com
Test Checkout.com payments without credentials.
Razorpay
Test Razorpay payments without credentials.
Worldpay
Test Worldpay payments without credentials.
Helcim
Test Helcim payments without credentials.
Skrill
Test Skrill payments without credentials.
Payoneer
Test Payoneer payments without credentials.