Skip to main content

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

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_method

No card attached yet

requires_confirmation

Card attached, not charged

requires_action

3DS challenge needed

processing

Bank is processing it

requires_capture

Authorized, not captured

succeeded

Funds captured — done

canceled

Dead — 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.

Which events your server needs to handle
EventWhen it firesWhat to doRequired
payment_intent.succeededFunds capturedFulfill the order. This is the only safe fulfillment trigger. required
payment_intent.payment_failedCard declinedAsk the customer to retry with a different card. required
payment_intent.requires_action3DS challenge triggeredRedirect to next_action.redirect_to_url.url. required
payment_intent.processingSubmitted to bankShow a loading state. Wait for succeeded or failed.—
payment_intent.canceledPI cancelled via APILog 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

Request parameters
ParameterTypeRequiredDescription
amountintegerrequired

Amount in smallest currency unit (e.g., 1000 = $10.00 for USD)

e.g. 1000
currencystringrequired

Three-letter ISO currency code in lowercase

e.g. usd
descriptionstringoptional

An arbitrary string for your reference

e.g. Payment for Order #12345
customerstringoptional

Customer identifier for your reference

e.g. cus_12345
metadataobjectoptional

Key-value pairs for storing additional information

e.g. {"order_id": "12345"}
receipt_emailstringoptional

Email address for the receipt

e.g. customer@example.com

Response fields

Response fields
FieldTypeDescription
idstringUnique identifier for the PaymentIntent
statusenumStatus of the PaymentIntent
client_secretstringSecret for client-side confirmation
createdintegerUnix timestamp of creation time
latest_chargestringID of the latest charge (generated on success)

Questions about Stripe PaymentIntents

Other gateway templates

Looking for a different provider?