Audience: Developers, Finance Admins
Prerequisites: Stripe account/keys; webhook endpoint configured
Outcomes: Card collection with automatic SCA and webhook reconciliation
Environment
STRIPE_SECRET_KEY=sk_live_xxx STRIPE_WEBHOOK_SECRET=whsec_xxx
Payment Intents (server-controlled)
1) Create PI (server)
curl -X POST $API_BASE/api/payments/intents -b cookies.txt \ -H 'Content-Type: application/json' \ -d '{ "orderId":"ord_123", "amount":150000, "currency":"USD", "reference":"ORD123-DEP", "metadata":{"orderId":"ord_123"} }'
Returns PI details (and client_secret
if applicable).
2) Confirm on client (Elements)
Collect card data with Stripe.js Elements (backend never sees PAN)
Stripe handles SCA/3DS; handle
requires_action
by showing the modal
3) Reconcile via webhook (authoritative)
Orders move to DepositPaid only after verified
payment_intent.succeeded
(see Article 3)
Edge cases
requires_action
→ show 3DS, then confirmCanceled PI → create a new one with same reference (idempotent handler)
QA checklist
Success triggers
payment.succeeded
→ order advancesDeclines produce
payment.failed
with provider reason logged
Hosted Checkout (fastest path)
Create Checkout Session (server)
curl -X POST $API_BASE/api/payments/stripe/checkout-session -b cookies.txt \ -H 'Content-Type: application/json' \ -d '{ "orderId":"ord_123", "amount":150000, "currency":"USD", "successUrl":"https://app.example.com/success", "cancelUrl":"https://app.example.com/cancel", "reference":"ORD123-DEP" }' # => { "url": "https://checkout.stripe.com/c/..." }
Redirect buyer to url
. Trust webhook, not the success redirect.
Pros: Stripe hosts page and SCA; typically low PCI scope (confirm SAQ with assessor).
QA checklist
Test success/cancel paths
Verify
payment_intent.succeeded
webhook updates the order
Runbook: “3DS loops or fails”
Ensure client handles
requires_action
Test both 3DS required and non-3DS test cards