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
The payment-processing page and webhooks
Taking paymentPage 16 of 31Updated
A payment the provider confirms later ends on the payment-processing page you build, and becomes an order when the provider's webhook arrives. Every state of that page, and how a webhook is validated and reconciled.
Who this is for. Developers building the page a shopper waits on while a payment is confirmed, and anyone tracing how a payment that completed after the shopper left became an order. After this page you can build the waiting page, say what it shows in each state, and follow a webhook from the provider to the order.
The payment-processing page
GET /checkout/payment-processing renders checkouts/payment-processing.html, which you supply, with checkout. The purchase call sends the shopper there whenever a payment is pending or a challenge has been completed, and a shopper coming back from a provider's own page lands on the same template.
The page decides again every time it loads, so a refresh, the back button or a reopened tab is always safe:
State
What the page does
What the shopper sees
No checkout cookie
Redirects to the cart
The cart
Succeeded: the checkout has become an order
Redirects to /checkout/complete/<order uuid>
The order-complete page
Pending: a payment is still being confirmed, or the checkout is fully paid and its order is being made
Renders the page, with checkout
Your waiting message, while the page polls
Failed, or not fully paid
Redirects to the checkout
The checkout, to pay again
Requires action
Handled before this page: the checkout page completes the challenge in Stripe's script, then sends the shopper here
The challenge, then this page
A checkout that is already fully paid counts as pending while its order is being made, so the shopper waits a moment longer and is then sent to the order. While the shopper waits, the checkout is split from its cart, so a change to the cart in another tab cannot touch the checkout being paid: the shopper can go on shopping while the payment completes.
Polling for the result
While the page is showing, poll GET /cdna-api/checkout/<uuid>/payment-processing. Its answer is never cached. Hand the checkout's uuid to the script as on The client-side handler.
Status
Body
What it means
202
{"status":"pending","message":"Waiting for payment to be confirmed","shouldPoll":true}
Resolved: go to redirectUrl, the order-complete page when the order exists, otherwise the checkout
404
No checkout has that uuid
JavaScriptjs/checkout/payment-processing.js
// On checkouts/payment-processing.html: poll until the payment is resolved.
const EVERY_MS = 2000;
const STOP_AFTER = 60; // two minutes
async function poll(id, tries = 0) {
const res = await fetch(`/cdna-api/checkout/${id}/payment-processing`, {
headers: { Accept: 'application/json' },
});
if (!res.ok) return showStillWaiting();
const body = await res.json();
if (!body.shouldPoll) {
location.href = body.redirectUrl;
} else if (tries < STOP_AFTER) {
setTimeout(() => poll(id, tries + 1), EVERY_MS);
} else {
showStillWaiting();
}
}
function showStillWaiting() {
document.getElementById('still-waiting').hidden = false;
}
poll(window.paymentConfig.id);
The platform sets no time limit on the wait, so decide a stopping point in the page. After it, tell the shopper that the payment may still complete, for example: "This is taking longer than usual. Your payment may still complete, and we'll email you when it does, so there is no need to pay again." Offer no second payment button on this page.
The order-complete page
GET /checkout/complete/<order uuid> renders checkouts/complete.html, which you supply, with order. It needs no sign-in and is never cached, and it answers 404 once the order is more than a day old. Print what the shopper needs there, such as the order number, items and totals, and not the person record behind the order. What the order holds is in the orders unit.
Webhooks
A webhook is how a payment that the provider confirms later, or that completes after the browser has gone, still becomes an order. It prevents the worst outcome in commerce, money taken with no order, and every Stripe and PayPal Donate payment relies on it.
The provider calls POST /cdna-api/invoices/payment-webhook/<provider>; your pages never call it. Two providers use it:
Provider
Endpoint
How an event is validated
Set-up
Stripe
/cdna-api/invoices/payment-webhook/stripe
The Stripe-Signature header of every event is validated before the event is trusted
None for the site. Stripe runs through Connect, so the webhook and its signing secret are set up once for the platform, and a site registers no webhook of its own
PayPal Donate
/cdna-api/invoices/payment-webhook/paypal-donate
Each notification is confirmed before it is trusted
None. The notification URL travels with each donation request
PayPal captures its payment during the purchase call, so it does not use a webhook.
Why a Stripe payment always waits for its webhook
The purchase call answers a Stripe payment as pending even when Stripe has already accepted it: the platform makes every Stripe order from the webhook, not only the slow ones. That is why PAYMENT_PENDING and the processing page are the normal path for Stripe.
What the platform does with an event
Validates it: the Stripe signature, or the PayPal Donate confirmation.
Reads it into one common event shape, whatever the provider.
Finds the invoice the event names. An event for an invoice the site does not have is ignored.
For a PayPal Donate hosted button, whose amount is known only when the donation arrives, completes the draft invoice with the event's amount.
Ignores the event when the invoice is no longer open (already paid or voided), when the same transaction is already recorded on it, or when the invoice has no payment waiting for confirmation.
Otherwise records the provider's result as a new transaction on the invoice.
When the invoice belongs to a checkout, checks whether it is fully paid and makes the order, exactly as the purchase call does. When it belongs to a form submission, the form's own completion runs instead: see Taking payments.
Because repeats and settled invoices are skipped, an event the provider sends twice is recorded once.
Refund events
A refund event is matched by transaction. A refund made outside the site carries no invoice id: it is matched on its external transaction id, so a repeat is skipped, traced through its parent transaction to the invoice it belongs to, and recorded there as a new transaction. A refund that names an invoice and is not yet recorded on it is recorded directly. Make refunds from the site's admin; refunds themselves are covered in the orders unit.
What the endpoint answers
The answer tells the provider whether to send the event again: any 2xx means it was received.
Case
Status
What follows
The event was handled
200
Done
The event was ignored: a repeat, an unknown invoice, an invoice no longer open, or no payment waiting
200
Done; nothing to send again
A provider name the site does not handle
404
Logged
The event failed validation
400, with no reason given
A forged request learns nothing from the answer
Anything else went wrong while recording it
500, with the error message
The provider's own retry schedule sends the event again