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: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.
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 everycart.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:
- Vanilla
- React
- Web components
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:
no-top-navigation). Verify both on the deployed origin.
Production checklist
Keys and environment
Keys and environment
- 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. apiUrlandcheckoutUrlleft unset (production defaults).
Checkout and return
Checkout and return
- The storefront is served over https;
successUrlandbackUrlare 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; Vercelrewrites; nginxtry_files). timeoutis rendered as “still processing”, never as a payment failure.- A page that embeds the checkout renders it on load and keeps
qb_checkout_sessionandqb_checkout_shopon its URL. - The shop has activated Quickbutik Payments, or every checkout is a demo.
Catalog and money
Catalog and money
- Images render
image.url(or the kit’s image components), neverimage.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: nullis treated as purchasable.- Every product the shop should sell has
visible: true.
SEO and compliance
SEO and compliance
- One
<title>owner per page;noIndexon cart, success, search and filtered pages. baseUrl/NEXT_PUBLIC_SITE_URLfixed by config, not derived fromHost.- 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 passesshopkit.consent.read()asinitialState. 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, andframe-src https://pay.quickbutik.comfor the embedded checkout. With analytics on, alsoscript-src https://www.googletagmanager.com https://connect.facebook.net,connect-src https://www.google-analytics.com https://*.analytics.google.com https://www.facebook.comandimg-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 underhttps://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.