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 03 · Building the checkout

Guest checkout and the captcha

Guests check out with an email and no login; members check out as themselves. What each path creates, how to offer an account after purchase, and the reCAPTCHA check that stops card testing at the purchase call.

Who this is for. Developers deciding how shoppers check out, with or without an account, and protecting the checkout's endpoints. After this page you can configure guest checkout, handle a guest who already has an account, offer an account after purchase, and place the captcha where it stops abuse without slowing real shoppers.

The guest checkout setting

Checkouts.guestCheckout is a switch, on by default. While it is on, a signed-out visitor can check out as a guest. When it is off, every checkout API route answers a signed-out visitor with a 401:

JSON
{"result":"error","message":"You need to login to view this"}

The setting applies to the checkout API routes. A site that turns it off also sends signed-out visitors to sign in before they reach /checkout, from the cart page's checkout button, so that they never see a checkout page whose calls are refused.

What each path creates

Guest checkoutAccount checkout
Who checks outA signed-out visitorA signed-in member
The checkout's personA new person record with the email the guest entered, and no login (no user)The member's own person, which cannot be changed to anyone else's
The emailAny address that does not belong to an existing accountThe member's own only: "The checkout email must match your profile email."
Members-only discountsNot available: a guest is in no member groupApplied by the member's groups

A shopper can sign in partway through. When a signed-in visitor opens a checkout that belongs to another person, GET /checkout links the checkout to the signed-in person, so a guest who signs in keeps their checkout and continues as a member.

A guest with a member's email

A guest who enters an email that belongs to an existing account is refused with "This email is already associated with another account" (error.email.already_in_use). A signed-in member who enters their own email is fine: the check refuses another member's account, not a repeat of the visitor's own address.

The production store this unit draws on asks the question earlier. When the shopper presses pay, it first sends the entered email to a job behind a webhook (Orchestration, formerly Hooks) that answers whether an account exists for it. If one does, the page shows "An account is already registered with your email address" with a sign-in link, and stops before it spends the captcha token or calls the purchase route. If the check itself cannot be reached, the page carries on: the checkout's own validation on PUT and at purchase is the real guard, and the early check only spares the shopper a wasted attempt.

JavaScript checkout/components/form-handler.js
const exists = await checkEmailHasAccount(emailField.value); // calls your job's webhook
if (exists) {
	showAlert('An account already exists for this email. <a href="/login">Log in</a> to continue.');
	return; // do not spend the captcha token or call /purchase
}

When a guest wants an account later

A guest's person record keeps their email, and an email belongs to one person per client. Registering later with the same address is therefore refused: "This email is already used." A guest has no login, so there is no password to reset either.

What carries across is the guest's existing person: their name, their email, and the addresses on their past orders and checkouts. The way to give a past guest an account is to attach a new login (a user) to that person, which an administrator can do, or a job; not to create a second person for the same shopper, which is what the register route does, since POST /register always creates a new person.

The simplest path for a shopper who wants an account is to decide before paying: offer "create an account" on the checkout page, register first, and pay signed in.

Offering an account after purchase

Entity events exist for orders, not for checkouts, so an account offered after purchase hangs off the order, once the checkout has been converted. It needs a job on the order's create event that, when the order's person has no user yet, creates a login for that same person and tells the shopper how to sign in. On the order-complete page (/checkout/complete/<order uuid>), whether the order's person has a user, $order->getPerson()->getUser(), tells the page which message to show: a note about the new account, or a link to sign in and see the order.

The captcha at purchase

Checkouts.recaptcha sets the check: disabled, checkbox or invisible, with checkbox as the default. Both modes are reCAPTCHA v2, with a site key for each under Environment.recaptcha.v2.checkbox.siteKey and Environment.recaptcha.v2.invisible.siteKey. The checkout page receives _recaptcha for rendering the widget.

The token is checked only at purchase, sent as _recaptchaToken with the payment method, and it is checked first, before the readiness check. A purchase refused as not ready has therefore used its token: reset the widget after every attempt, successful or not.

HTTP
POST /cdna-api/checkout/<id>/purchase
{"_recaptchaToken": "<token>", "_paymentMethodId": 3}

400 {"result":"error","message":"Checkout is not ready for payment"}

What it stops: card testing

The abuse the captcha is placed to stop is card testing: a bot runs a list of stolen card numbers through a store's payment step to find the ones that still work. The purchase call is the one endpoint that charges a card, so a check there stops the abuse at its one costly step. Every other checkout call, from addresses to shipping and codes, stays free of friction, and a real shopper meets the captcha once, at the end, as part of paying.

CSRF on every change

The captcha covers the purchase call. Every other checkout API route that changes something is CSRF-checked: only the read (GET /cdna-api/checkout/<id>) is not. The page's API layer sends the token in the X-Csrf-Token header on every call (One API layer).

Reset the widget after every attempt

Each purchase attempt uses its captcha token, whatever the outcome. Reset the widget in the same code that handles the purchase response, for errors and refusals as well as success, so a shopper who corrects an address and presses pay again has a fresh token.

Go deeper

Was this page helpful?

Your answer goes to the docs team.