A Core dna store is a set of records and an API that moves a purchase from cart to order: products described in every language and priced in every currency the store sells in, a cart, a checkout that becomes an order, invoices, payments, tax and shipping. The site builds the storefront on top: the cart and checkout pages, their scripts, and the customer emails.
L2–L3
Unit 04 · Taking payment
3-D Secure, declines and retries
Taking paymentPage 18 of 31Updated
How a card challenge reaches your page and what to do after it, what each decline looks like and what to show the shopper, and a retry policy that lets a shopper try again without looking like card testing.
Who this is for. Developers handling the purchase call's answers that are not a straight success. After this page you can complete a 3-D Secure challenge, show the right message for each decline, and let the shopper retry safely.
3-D Secure and other actions
When a card needs the shopper to confirm the payment with their bank, Stripe asks for an action, and the purchase call answers in one of two ways:
Answer
When
What your page does
PAYMENT_AUTHENTICATION_REQUIRED
Stripe's script can complete the action on the page: a 3-D Secure challenge, a QR code to display, or another action Stripe's script handles
Passes action.data to Stripe's script, then goes to /checkout/payment-processing
PAYMENT_REDIRECT
The action is a plain redirect to a page of the provider or the bank
Follows action.url; the shopper comes back to the payment-processing page
action.data holds paymentIntentId, clientSecret, nextActionType and hostedInstructionsUrl. In the Stripe module of the client-side handler (The client-side handler), authenticate() hands the client secret to Stripe:
JavaScriptjs/checkout/payment-methods/stripe.js
// payment-methods/stripe.js, inside the handler class
async authenticate(data) {
// 3-D Secure, and any other action Stripe's script completes on the page.
await this.stripe.handleNextAction({ clientSecret: data.clientSecret });
// The dispatcher then sends the shopper to /checkout/payment-processing,
// which works out the outcome whatever happened in the challenge.
}
After either answer, the payment belongs to the payment-processing page (The payment-processing page). Do not call purchase again for the same attempt: the first call has already recorded a payment waiting on the invoice, and the webhook or the shopper's return settles it. A second call starts a second attempt.
Declines, and what to show
A decline answers with result: error, HTTP status 500 and a message. Show the message next to the payment form, leave the form filled in, and let the shopper try again or choose another method.
Stripe
The message is Stripe's own text for the decline, passed on as Stripe gives it.
PayPal
PayPal's reasons are mapped to messages written for the shopper, by the issue PayPal reports:
"Payment was declined. Please try a different payment method."
ORDER_NOT_APPROVED, PAYER_ACTION_REQUIRED
"Your payment could not be confirmed. Please re-enter your card details and try again."
ORDER_ALREADY_CAPTURED
"This payment has already been processed. No further action is needed."
Any other issue
"We couldn't process your payment. Please try again, or use a different payment method."
PayPal's own description of an issue is written for developers and goes to the log, never to the shopper. Treat every provider's detail the same way: show the message the response gives, and keep raw provider detail out of the page.
Requests refused before the provider
A request without a valid payment method or confirmation token answers HTTP 400 before any provider is asked, for example "Payment method was not provided or is invalid". It usually means the method was deactivated after the page loaded, or the script did not collect the token: load the list of methods again and ask the shopper to choose. A checkout that is not ready for payment is refused before this point too, with the warnings described on The checkout and when it can be paid.
Retries
A retry is the shopper's to make, from the payment step. Each retry is a new purchase call on the same checkout, and two things follow from it.
The invoice. If the shopper changed payment method, the open invoice for the old method is voided, so a late confirmation of the earlier attempt cannot mark it paid. With the same method, the open invoice is reused while its grand total, person, totals, items and discounts still match the checkout; if any of them changed, it is voided and a new invoice is created.
The Stripe payment intent. Every Stripe attempt opens a new payment intent, whether the invoice was reused or not.
A retry policy that stays clear of fraud rules
Many fast declines from one session is what card testing looks like, the abuse the captcha on the purchase call is placed to stop (What it stops: card testing). Let a real shopper retry without looking like it:
Disable the pay button while a purchase call is in flight, so a double click sends one request.
Allow a new attempt no faster than once every few seconds.
After a handful of declines in a row (three is a sensible number), suggest a different payment method rather than another attempt with the same card.
Keep the form filled in between attempts, so the shopper corrects only what was wrong.
JavaScriptjs/checkout/pay-button.js
// One request at a time, a few seconds between attempts,
// and another method suggested after three declines in a row.
const MIN_GAP_MS = 5000;
const SUGGEST_ANOTHER_AFTER = 3;
let lastAttempt = 0;
let declinesInARow = 0;
payButton.addEventListener('click', async () => {
if (payButton.disabled || Date.now() - lastAttempt < MIN_GAP_MS) return;
payButton.disabled = true;
lastAttempt = Date.now();
try {
const body = await pay(window.paymentConfig.id, api.purchase);
if (body.result === 'error') {
declinesInARow += 1;
showDecline(body); // the message from the response, next to the form
if (declinesInARow >= SUGGEST_ANOTHER_AFTER) suggestAnotherMethod();
}
} finally {
const wait = Math.max(0, MIN_GAP_MS - (Date.now() - lastAttempt));
setTimeout(() => { payButton.disabled = false; }, wait);
}
});
To see every path before launch, set a Mock Pay method's failure rate on a development site (Testing with it), and run the providers' own test modes with a declined card and a card that asks for a challenge. A payment taken on a form is covered on Taking payments.