Docs
Dashboard
View as Markdown
On this page
  1. Register your site first
  2. 1. Your own checkout
  3. Read the options
  4. Start the checkout
  5. When the start says no
  6. Back on your site
  7. 2. Embedded checkout
  8. 3. Hosted checkout
  9. The return pages live on the cart
  10. Testing

Headless checkout

Your own checkout page, the embedded checkout or the hosted one, all from the same cart and back to your site.

Your site owns the cart and as much of the checkout as you want. The payment step is always ShopFast’s: the card form, the wallet buttons or the provider’s own page. Your payment methods, taxes, email verification, fraud checks, gift cards, store credit and limits per payment method all work as they do in your ShopFast store, and card data never touches your site. After paying, the buyer comes back to your site.

There are three ways in, and all three start from the same Storefront API cart:

Your own checkout Embedded Hosted
What you build The checkout page A button One redirect
The buyer stays on your site Until the payment step Yes, in an overlay No
Discount codes, gift cards and details already in the cart Carry over Carry over Carry over
Back to your site after paying returnUrl The success event returnUrl
Every payment method and check Yes Yes Yes

The Next.js starter ships all three, with its own checkout page as the default.

Register your site first

ShopFast sends the buyer back only to a site you registered. Add your site’s origin (https://shop.example.com, or http://localhost:3000 while you develop) to a storefront key’s allowed origins in Settings → Headless & MCP → Storefront keys. That also lets the embedded checkout open on it. A returnUrl, cancelUrl or embedOrigin on any other site is refused with 422.

The return page receives the order’s access token, so it must be a page you control. That is why the list is yours to keep.

1. Your own checkout

Draw the checkout from the cart’s options, start it with what the buyer filled in, then send the buyer to pay.

Read the options

const options = await shop.cart.checkoutOptions(cartId, { country: 'DE' });

The answer is everything your store’s own checkout would show for this cart, worked out for that country:

Field What to do with it
methods The ways to pay, in your order. Show them as choices. Send the chosen one’s provider, method and option (and manualMethodId for a manual method). adjustmentMinor is how much the subtotal changes with it.
unavailable Why some ways to pay are not offered for this total. Show the reasons under the list.
free Nothing to pay: start without a method.
fields name, country, phone and address, each required, optional or hidden.
questions Your checkout questions and the products’. Send each answer as customFields[id]. A hidden one is answered with its defaultValue.
consent The terms box. {link} in text is a link to linkUrl. Send consent: true.
marketingOptIn The newsletter box, when you turned it on.
withdrawalWaiver Buyers in the EU, the EEA and the UK tick this sentence before a paid order. Send withdrawalWaiver: true.
emailVerification required when a code is always asked for this cart, possible when a fraud check may still ask.
codes Whether to show the discount code and gift card fields. Apply codes to the cart with applyCode.
tips Tip presets for this cart, when you take tips. Send tipMinor.
seller Who sells, to show before the buyer pays.
totals, testMode The cart’s totals, and whether payments are simulated.

Read the options again when the buyer changes country: the ways to pay, the tax and the waiver can change with it.

Start the checkout

const session = await shop.cart.checkout(cartId, {
  email: '[email protected]', name: 'Robin', country: 'DE',
  provider: 'shopfast_pay', method: 'card', consent: true, withdrawalWaiver: true,
  customFields: { discord_username: 'robin#0001' },
  returnUrl: 'https://shop.example.com/checkout/complete',
  cancelUrl: 'https://shop.example.com/cart',
});
// { sessionId, payUrl, accessToken, orderUrl, paid, test }
redirect(session.payUrl);

Only email is required by the API. Your settings decide the rest, and the answer says what is missing.

  • payUrl opens the payment step on your store: the test payment, the card form, the pay page or the provider’s own page. Open it in the buyer’s browser. It brings the order access with it.
  • paid: true means nothing was left to pay, for example a gift card or a 100% discount. There is no payUrl: show the order right away.
  • Keep accessToken with the order, in an httpOnly cookie for example. It reads the order and its keys.

When the start says no

Answer What happened What to do
409 email-verification-required A 6-digit code was emailed to the buyer. Ask for it, then send the same body again with verificationCode.
422 invalid-code The code is wrong or expired. Ask again. A new start without the code sends a new one.
422 validation A field is missing or wrong. fields[].path names it: body.email, body.customFields.discord_username, body.withdrawalWaiver. Show the message next to that field.
409 payments-not-set-up The store cannot take payments yet. Say so calmly, without a pay button.
503 payment-provider-failed The provider failed before its page opened. Nothing was charged. Offer the next way to pay.
403 checkout-blocked A fraud rule stopped the checkout. Show detail.
429 rate-limited Too many tries. Wait Retry-After seconds.

With the SDK, error.is('email-verification-required') and error.fieldError('email') read them.

Back on your site

After paying, the buyer lands on your returnUrl with session and access added:

https://shop.example.com/checkout/complete?session=cs_…&access=…

Keep the access token, take it out of the address bar, and show the order:

const order = await shop.orders.get(sessionId, access);
// status: "open" until the payment is confirmed (crypto and bank transfers can take a while), then "paid"
// with keys and downloads

A buyer who backs out of paying lands on your cancelUrl with session added. Their cart is still there. The store sends the buyer back once, right after paying. The link in the receipt email still opens the order on your store.

2. Embedded checkout

The store’s checkout opens in an overlay on your page, with the same cart. Ask for a checkout link signed for your page’s origin:

const link = await shop.cart.checkoutLink(cartId, { embedOrigin: 'https://shop.example.com' });
// link.embed: { scriptUrl, store, cart }

Then open it in the browser:

<script src="https://your-store.shopfa.st/_sf/checkout.js" async></script>
<script>
  ShopFastCheckout.open({
    store: embed.store,
    cart: embed.cart,
    onSuccess: ({ sessionId, orderNumber, accessToken }) => {
      location.href = `/orders/${sessionId}?access=${accessToken}`;
    },
  });
</script>

embed.cart works for two hours, on that origin only. The overlay posts its events only to your page’s origin: shopfast:checkout:ready, success (with sessionId, orderNumber, accessToken, totalMinor and currency), close and error. See Embeds for buy buttons that open the checkout without the API.

3. Hosted checkout

Ask for a checkout link and send the buyer there:

const link = await shop.cart.checkoutLink(cartId, {
  returnUrl: 'https://shop.example.com/checkout/complete',
  cancelUrl: 'https://shop.example.com/cart',
});
redirect(link.checkoutUrl);

The link opens your store’s checkout with that cart. It is signed, works only for that cart and store, and expires after two hours. With returnUrl, the buyer comes back to your site after paying, as in your own checkout. Without it, they stay on your store’s order page.

The return pages live on the cart

returnUrl and cancelUrl are kept on the cart, so every checkout of that cart uses them. You can set them once when you create the cart:

await shop.cart.update(cartId, { returnUrl: 'https://shop.example.com/checkout/complete', cancelUrl: 'https://shop.example.com/cart' });

Send null to clear them. The cart answer shows them as returnUrl and cancelUrl.

Testing

While your store is in test mode (testMode in the options), payments are simulated: the payment step offers test cards, and nothing is charged. Orders are marked test. Agents use the hosted way: the shop MCP server’s get_checkout_link tool.

Was this page helpful?