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 PaymentIntent status 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.
Why requires_action breaks production 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.
The 6 PaymentIntent statuses
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
succeededFunds captured — done
canceledDead — create a new PI
Important: A PaymentIntent can only move forward through this state machine, not backward. Once it reaches succeeded or canceled, it is terminal — you create a new PaymentIntent for any retry. requires_action is the only status where the customer must do something on an external page before the payment can proceed.
Which events your server needs 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. | — |
API request and response examples
curl -X POST https://mockgateway.dev/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 and retries work →
Everything this mock covers
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.
PaymentIntent statuses
- ✓ 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?