Skip to main content

Test the whole loop

The loop to prove: add to cart → cart → checkout → hosted checkout → back to /success/<orderNumber> → confirmation.

1. Pre-check the handoff

From a throwaway cart, check that the shop, the key’s write scopes and your URLs are fine, without any UI:
A url back means the setup works, and checkout tells you which checkout the shop runs. Nothing is ordered until someone pays, so the session can be left alone.

2. Demo mode or live

A shop that has not activated Quickbutik Payments runs the checkout in demo mode: demo payment methods, no money moves, no order is created. It proves the handoff and the checkout UI, but not the return leg. shop.get() reports demo.enabled: true on the wire. Activating payments in the admin (Settings → Payments) changes nothing in your code.

3. The zero-total path

Cards cannot be driven through an API. On a shop with payments activated, use the one case where the platform places an order without a payment: a total of exactly 0.
1

Set up in the admin

Create a 100 % discount code (Discounts) and a shipping method priced 0, or a free-shipping rule (Shipping).
2

Run the flow

Add a cheap product, go to checkout, enter the code, pick free shipping, place the order.
3

Check the return

You should land on /success/<orderNumber>, and pollConfirmation() / useOrderConfirmation() / <qb-order-confirmation> should report completed.
4

Clean up

Deactivate the code and the free shipping, and cancel the test order under Orders.
Never test with a real card payment. Use the zero-total path, or abandon the checkout.

Building inside an AI app builder preview

Google AI Studio, Lovable, v0, Replit, StackBlitz and similar tools render your app in a sandboxed iframe on another origin. Two things change there, and both go away once the app is deployed. Cookies are blocked or partitioned. Nothing is remembered, so every cart.add() creates a new cart and start() without cartId creates an empty one, which the handoff refuses with 400 "Cannot hand off an empty cart to the checkout". Keep the cart in memory and pass its id:
storage: "localStorage" keeps the cart across reloads in a preview where local storage works. Switch back to the default before deploying with a server, which reads the cart from cookies. Navigating to the checkout. Plain location.assign(url) works: it navigates the preview frame itself. Never use window.top.location; the sandbox blocks it. Only if the builder blocks cross-origin navigation of its frame, open a tab synchronously in the click handler:
The return leg cannot be verified in a preview, and the embedded checkout falls back to the redirect there (no-top-navigation). Verify both on the deployed origin.

Production checklist

  • The publishable key is the one from Custom storefront in the admin (or one minted with the five storefront scopes).
  • No qb_pat_ token anywhere in frontend code, a client bundle or a public repo.
  • apiUrl and checkoutUrl left unset (production defaults).
  • The storefront is served over https; successUrl and backUrl are on that origin.
  • The thank-you route answers a top-level GET at /success/<orderNumber>. SPA and static hosts have a rewrite (Netlify / Cloudflare Pages _redirects: /success/* /success.html 200; Vercel rewrites; nginx try_files).
  • timeout is rendered as “still processing”, never as a payment failure.
  • A page that embeds the checkout renders it on load and keeps qb_checkout_session and qb_checkout_shop on its URL.
  • The shop has activated Quickbutik Payments, or every checkout is a demo.
  • Cookies are not httpOnly when browser code reads the cart.
  • The same storageKeyPrefix and cookie attributes on server and browser.
  • One client per request on the server, one per app in the browser; configure() called once.
  • Images render image.url (or the kit’s image components), never image.path.
  • Money is rendered from minor units and formatted with the currency the response states (product.currency, cart.currency), never a constant.
  • On a shop with several currencies: cached catalog data is keyed on the currency, and a "display" currency says the checkout charges the shop’s currency. See Currencies.
  • stock: null is treated as purchasable.
  • Every product the shop should sell has visible: true.
  • One <title> owner per page; noIndex on cart, success, search and filtered pages.
  • baseUrl / NEXT_PUBLIC_SITE_URL fixed by config, not derived from Host.
  • JSON-LD has a currency, and is read in the shop’s own (charged) currency.
  • The cookie banner is on (the default) with your language and a privacyPolicyUrl, a privacy policy page exists, and the footer has a cookie-settings button (<ConsentSettingsButton> / <qb-consent-settings>). On Next.js the layout passes shopkit.consent.read() as initialState. See Consent and analytics.
  • The shop’s GA4, GTM or Meta ids are set in the Quickbutik admin if the merchant wants analytics; with none, the banner shows and nothing is sent.
  • CSP allows the script host, connect-src https://commerce.quickbutik.com, img-src https://cdn.quickbutik.com, and frame-src https://pay.quickbutik.com for the embedded checkout. With analytics on, also script-src https://www.googletagmanager.com https://connect.facebook.net, connect-src https://www.google-analytics.com https://*.analytics.google.com https://www.facebook.com and img-src https://www.facebook.com https://www.google-analytics.com.
  • Script-tag pages pin the CDN URL to an exact version.

Building with AI

Quickbutik publishes instructions written for coding agents at https://quickbutik.com/agents.md, with the kit guide as an installable agent skill under https://quickbutik.com/agents/kit/. It covers creating a shop when there is no key, building every page in order, a design pass and a self-check. Add two lines to your project’s AGENTS.md or CLAUDE.md so every session starts from the current instructions:

Build with AI

More on using AI tools with Quickbutik.