Guide 08 · Developer docs

Commerce

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

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:

StateWhat the page doesWhat the shopper sees
No checkout cookieRedirects to the cartThe cart
Succeeded: the checkout has become an orderRedirects 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 madeRenders the page, with checkoutYour waiting message, while the page polls
Failed, or not fully paidRedirects to the checkoutThe checkout, to pay again
Requires actionHandled before this page: the checkout page completes the challenge in Stripe's script, then sends the shopper hereThe 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.

StatusBodyWhat it means
202{"status":"pending","message":"Waiting for payment to be confirmed","shouldPoll":true}Still waiting: poll again
200{"status":"completed","redirectUrl":"…","shouldPoll":false}Resolved: go to redirectUrl, the order-complete page when the order exists, otherwise the checkout
404No checkout has that uuid
JavaScript js/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:

ProviderEndpointHow an event is validatedSet-up
Stripe/cdna-api/invoices/payment-webhook/stripeThe Stripe-Signature header of every event is validated before the event is trustedNone 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-donateEach notification is confirmed before it is trustedNone. 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

  1. Validates it: the Stripe signature, or the PayPal Donate confirmation.
  2. Reads it into one common event shape, whatever the provider.
  3. Sends a refund event down its own path (below).
  4. Finds the invoice the event names. An event for an invoice the site does not have is ignored.
  5. For a PayPal Donate hosted button, whose amount is known only when the donation arrives, completes the draft invoice with the event's amount.
  6. 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.
  7. Otherwise records the provider's result as a new transaction on the invoice.
  8. 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.

CaseStatusWhat follows
The event was handled200Done
The event was ignored: a repeat, an unknown invoice, an invoice no longer open, or no payment waiting200Done; nothing to send again
A provider name the site does not handle404Logged
The event failed validation400, with no reason givenA forged request learns nothing from the answer
Anything else went wrong while recording it500, with the error messageThe provider's own retry schedule sends the event again
Go deeper

Was this page helpful?

Your answer goes to the docs team.