Volley / Templates / Stripe PaymentIntents with 3D Secure
Payments template
Stripe PaymentIntent sequence diagram
A custom checkout on your own page with the Payment Element, including the 3D Secure challenge. Click an arrow to see the PaymentIntent request, the return_url redirect or the payment_intent.succeeded webhook body.
Click any arrow in the editor to see its request and response as a table or the raw body. Change the hosts, ids and fields to match your integration, then share a link or embed it in your README.
Step by step
- Your server creates a PaymentIntent. A form-encoded
POST /v1/payment_intentswithamount,currencyandautomatic_payment_methods[enabled]=true. Stripe answers with the intent and itsclient_secret. - The browser mounts the Payment Element with the client secret, and the customer enters a card.
- The browser confirms the payment with
stripe.confirmPaymentand areturn_url. Stripe.js talks to Stripe directly. - The bank asks for 3D Secure. The intent comes back as
requires_action, Stripe.js shows the challenge, and the customer approves it. - Stripe.js redirects to your
return_urlwithpayment_intentandredirect_statusin the query. - Stripe calls your webhook with
payment_intent.succeeded. VerifyStripe-Signature, fulfil once and answer 200.
Easy to get wrong
- Fulfil from the webhook. The redirect to
return_urlnever happens if the customer closes the tab during 3D Secure. - Set the amount on the server. Anything the browser sends can be changed.
- Treat the client secret as sensitive. It can complete the payment, so do not log it or store it.
The source
The whole diagram is this plain text. Copy it into your repo, or open it in the editor.
stripe-payment-intents.volley
title: Stripe PaymentIntents with the Payment Element and 3D Secure
order: Customer, Browser, Server, Stripe, Card Issuer
Customer -> Browser: Open the checkout page
Browser -> Server: POST /create-payment-intent
in: Cart
```json
{ "items": [{ "price": "price_1QAbc2Def3", "quantity": 1 }] }
```
Server -> Stripe: POST /v1/payment_intents
in: PaymentIntent request (form encoded)
amount: 4900
currency: usd
automatic_payment_methods[enabled]: true
metadata[order_id]: ord_42
note: Work out the amount on the server from the cart. Send an `Idempotency-Key` header so a retried request does not create a second PaymentIntent.
Stripe --> Server: 200 OK
out: PaymentIntent
```json
{
"id": "pi_3QAbc2Def3Ghi4",
"object": "payment_intent",
"amount": 4900,
"currency": "usd",
"status": "requires_payment_method",
"client_secret": "pi_3QAbc2Def3Ghi4_secret_Jk5Lm6No7Pq8"
}
```
Server --> Browser: 200 OK
out: Client secret
```json
{ "clientSecret": "pi_3QAbc2Def3Ghi4_secret_Jk5Lm6No7Pq8" }
```
Browser -> Browser: Mount the Payment Element with the client secret
Customer -> Browser: Enter a card and click **Pay**
Browser -> Stripe: stripe.confirmPayment()
in: Call
```text
stripe.confirmPayment({
elements,
confirmParams: { return_url: "https://shop.example.com/order/complete" }
})
```
note: Stripe.js confirms the PaymentIntent straight from the browser with the client secret. Your server is not involved.
Stripe --> Browser: Needs authentication
out: PaymentIntent
```json
{ "id": "pi_3QAbc2Def3Ghi4", "status": "requires_action", "next_action": { "type": "use_stripe_sdk" } }
```
=: {fas-shield-alt} 3D Secure
Browser -> Card Issuer: Open the 3DS challenge in a modal
...: {fas-mobile-alt} Customer approves the payment in their banking app
Card Issuer --> Browser: Challenge complete
Stripe -> Card Issuer: Authorize $49.00
Card Issuer --> Stripe: Approved
Browser -> Server: GET /order/complete (the return_url)
in: Query parameters
payment_intent: pi_3QAbc2Def3Ghi4
payment_intent_client_secret: <redacted>
redirect_status: succeeded
note: Stripe.js redirects here after the challenge. Retrieve the PaymentIntent to show its status, but do not fulfil here.
Server --> Browser: Order confirmation page
=: {fab-stripe} Fulfilment
Stripe -> Server: POST /webhooks/stripe (payment_intent.succeeded)
in: Event
```http
POST /webhooks/stripe HTTP/1.1
Content-Type: application/json
Stripe-Signature: t=1760886000,v1=<redacted>
{
"id": "evt_3QAbc9Xyz",
"object": "event",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_3QAbc2Def3Ghi4",
"object": "payment_intent",
"status": "succeeded",
"amount_received": 4900,
"currency": "usd",
"latest_charge": "ch_3QAbc2Def3Ghi4",
"metadata": { "order_id": "ord_42" }
}
}
}
```
Server -> Server: Verify the signature and fulfil ord_42 once
note: Check `Stripe-Signature` against the raw body with the endpoint's signing secret. Stripe can send the same event more than once.
Server --> Stripe: 200 OK