// FREE TOOL · STATIC SITE CHECKOUT

_catalog.json generator and validator

A static site can take payments without a shop platform, but it cannot be trusted with the price: anything written into the HTML can be edited in the browser before it reaches your checkout endpoint. The fix is a catalogue the server reads and the customer cannot touch. Paste your products below — or an existing file to check — and this page generates the _catalog.json a Clize-hosted store prices every basket against, plus the add-to-cart markup that goes with it. The browser only ever sends a SKU and a quantity; the amount is looked up server-side from the file you deployed. Free, runs in your browser, nothing uploaded.

FreeNo signupValidator + generatorRuns in your browser
_catalog.json · generator

One product per line: sku | name | price | image | description | month or year. Only the first three are required. Paste a whole _catalog.json instead and it switches to validating that.

Physical goods: the countries checkout may collect an address for. Leave it empty for digital or in-person delivery and no address is collected.

The field exists in the file format, but hosted checkout charges in USD today. Anything else is recorded and ignored.

Errors and warnings
    _catalog.json
    
                
    Add-to-cart markup
    
              

    How to add checkout to a static site

    Three steps, no shop platform and no embedded widget. The catalogue is a file you deploy; the checkout is a POST from your own domain.

    1. List your products. One per line above: sku, name and price are required, and an image path, a description and a billing interval are optional. Or paste a catalogue you already have and the panel validates it instead of generating one.
    2. Deploy the file at the site root. Download the generated _catalog.json, put it beside your index.html, and deploy. A bad catalogue is refused at deploy time rather than at checkout, so a broken price cannot reach a customer.
    3. Paste the markup into the page that sells. One button per SKU carrying data-clize-add, a checkout button, a cart counter, and the cart script. The browser sends only SKUs and quantities; the server prices the basket from the deployed file.

    Why the price must not live in your HTML

    Almost every walkthrough for putting a payment button on a static site ends at the same place: an amount written into the page, a little JavaScript that posts it to an endpoint, and an endpoint that creates a checkout session for whatever amount it received. It works on the first try, which is the problem — a tampered payment succeeds exactly as smoothly as a real one, and nothing in your logs looks unusual afterwards.

    The rule underneath is short. The browser may say what is being bought. It must never say how much it costs. Anything reachable from the page — a data attribute, a hidden input, a JSON blob fetched at load, a value in localStorage — is editable in ten seconds by anyone with developer tools open, and no amount of minification changes that. Prices have to be resolved on the server, from a source the customer cannot reach.

    This is also why the hosted options that do work, work. A Stripe Payment Link or Buy Button is safe because the price lives in Stripe's account, not in your markup; the page only carries an identifier. The unsafe version is the do-it-yourself middle ground, where your own function accepts an amount and trusts it.

    A Clize-hosted site takes the same shape, with the catalogue as the trusted source. The page posts { items: [{ sku, quantity }] } to /_clize/checkout on your own domain. The server loads the _catalog.json that was deployed with the site, looks up each SKU, and builds the Stripe Checkout line items from the catalogue price. An unknown SKU fails the order. A non-positive price fails the order. Quantities are clamped to a sane range. The number the browser sent, if it sent one at all, never enters the calculation.

    One consequence worth planning for rather than discovering: because the price lives in a deployed file, changing a price is a deploy, not an edit. That is a real trade against a dashboard you can click in, and it buys you a catalogue that is diffable, reviewable and in version control next to the page it prices.

    The catalogue contract, field by field

    _catalog.json is a small object with an optional currency, optional shipping countries, and an array of products. Nothing else is read, and unknown keys are ignored rather than rejected — so you can annotate the file for your own tooling if you want.

    FieldRequiredWhat it does
    products[].skuyesThe only value the browser sends. Must be unique across the file; goes into an HTML attribute, so keep it to letters, digits, dot, dash and underscore.
    products[].nameyesThe line item the buyer reads on the Stripe checkout page and on the receipt.
    products[].priceyesA positive number in major units — 25 means twenty-five dollars, not twenty-five cents. Rounded to cents when the session is created.
    products[].imagenoA path in your own site or an absolute URL, shown on the checkout page.
    products[].descnoA short description carried alongside the name.
    products[].intervalnomonth or year makes the product a subscription. See the caveat below before you use it.
    shipping.countriesnoTwo-letter ISO codes. Present, and checkout collects a delivery address restricted to those countries; absent, and it collects none.
    currencynoRecorded in the file. Hosted checkout charges in USD today, so any other value is ignored rather than honoured.

    Two honest caveats, both of which the panel raises as warnings rather than burying in a footnote. Subscriptions need the direct payout mode, where charges settle into a merchant's own connected Stripe account — and that mode is not switched on at the platform level, so a checkout containing a product with an interval comes back with an explicit error instead of quietly charging once. Subscriptions and one-off products also cannot share a basket, because Stripe Checkout will not mix the two modes in one session; a mixed cart is refused rather than split.

    The same contract is printed by the CLI if you would rather read it from the tool that consumes it:

    $ clize shop init

    It returns the catalogue shape, the markup and the next steps for the store bound to the current project.

    Wiring it to checkout

    Three attributes and one script, and there is no cart library to choose:

    <button data-clize-add="tee-blk-m">Add to cart</button>
    <button data-clize-add="poster-a2" data-clize-qty="2">Add two posters</button>
    
    <button data-clize-checkout>Checkout</button>
    <span data-clize-count></span>
    
    <script src="/_clize/cart.js"></script>

    The script is served from your own domain, keeps the basket in localStorage, and delegates clicks, so buttons rendered after page load still work. It also exposes window.clizeCart with add, checkout, count and clear, which is what you want if your page is built by a framework rather than by hand. Pressing checkout posts the basket to /_clize/checkout — same origin, so no CORS, no preflight and no third-party script in your page — and the response carries the Stripe Checkout URL the browser then goes to.

    The failures you will actually meet, and what each one means:

    • No catalogue deployed. The site has no _catalog.json at its root yet. Deploy the file, not just the page.
    • Unknown SKU. The button says something the catalogue does not contain — usually a rename that shipped in the HTML but not in the file.
    • Mixed basket. A subscription and a one-off product in the same order. Split them into two checkouts.
    • Too many distinct items. One basket carries at most fifty different products; quantities are unlimited within a per-line ceiling.
    • Invalid order amount. The total came out at zero or above the order cap, which almost always means a price got mangled on the way in.

    None of these should reach a customer, because a malformed catalogue is rejected when you deploy rather than when somebody tries to buy. That is the point of validating the file before it ships — and the reason this page exists as a validator and not only as a generator. When you want to exercise the checkout itself afterwards, the Stripe test card table has the numbers for every decline, dispute and 3D Secure branch.

    What the validator flags — and what it cannot know

    Errors are the things that stop the file being deployed or stop a basket becoming an order. Warnings are things that are legal and still likely to bite you.

    CheckLevelWhy
    Parses as JSON, with a non-empty products arrayerrorDeploy refuses the file outright, so this fails loudly and early rather than at someone's checkout.
    Every product has a sku and a nameerrorThe SKU is the lookup key and the name is what the buyer sees on the receipt.
    price is a positive numbererrorZero, negative and "25 USD" are all rejected. Write a plain decimal.
    SKUs are uniqueerrorTwo rows with one key means the lookup is a coin toss, so it is refused instead.
    interval is month or yearerrorAny other value would silently become a one-off product.
    Shipping entries are two-letter ISO codeserror"USA" and "United States" are not country codes and collect no address.
    SKU is plain and shortwarningIt ends up inside an HTML attribute; spaces and quotes there are an unnecessary adventure.
    Price has at most two decimalswarningAnything finer is rounded to cents when the Stripe session is created, so your file and your invoice would disagree.
    Any product carries an intervalwarningRecurring billing needs the direct payout mode, which is not enabled today.
    Subscriptions mixed with one-off productswarningLegal in the file, refused in a single basket.
    No shipping countrieswarningCorrect for digital goods, quietly wrong if you post parcels — no address is collected at all.
    currency other than USDwarningThe field is read from the file and not yet used by hosted checkout.

    What it deliberately does not do: it never fetches your site, so it cannot tell you whether the file is actually deployed or whether an image path resolves. It does not know your stock — there is no inventory field and no stock count anywhere in this product, so overselling is possible and refunding is the remedy. It does not calculate tax. And it will not invent a SKU for a product you forgot; if a row is missing, the file is quietly, validly wrong, and only you can see that.

    What happens after they pay

    The catalogue is the front half. The back half is an order that has to be sourced, shipped and explained to a customer, and it is worth knowing it exists before you commit to an approach.

    On a Clize-hosted store, a paid checkout becomes an order the platform keeps: line items with their SKUs and the prices they were charged at, the delivery address if you asked for one, and a status that only Stripe's webhook can move to paid. From there clize shop todo lists what needs doing today, clize shop fulfill --tracking records a shipment and registers it with a tracking service, and every site serves a buyer-facing lookup at /orders that answers "where is my order" without an email to you.

    Two things there are gated on purpose: refunding and emailing a customer both return a preview first and need an explicit confirmation flag, which matters most when the thing running the commands is an agent rather than a person. The Agent Storefront overview covers the loop end to end, and if the site itself is not built yet, hosted deploys are the other half of the same product.

    // FAQ

    How do I add checkout to a static site?

    Publish a catalogue file the server can read, then add buttons that reference products by SKU rather than by price. On a Clize-hosted site that file is _catalog.json at the site root, the buttons carry data-clize-add with the SKU, and the cart script posts the basket to /_clize/checkout on your own domain, which prices it from the deployed catalogue and returns a Stripe Checkout URL.

    Can a static site take payments without Shopify or a third-party widget?

    Yes. What a static site lacks is a trusted place to hold prices and a server to create the checkout session — not a shopping platform. A deployed catalogue file supplies the first and a same-origin checkout endpoint supplies the second, which leaves your pages as plain HTML with no embedded storefront and no vendor script in them.

    Why can I not just put the price in a data attribute?

    Because anyone can edit it before it is sent. A price written into the page, a hidden input or a fetched JSON blob is all client-side state, and a checkout endpoint that accepts an amount will happily charge one cent for a two-hundred-dollar item. The browser should send what is being bought; the server must decide what it costs.

    What fields does _catalog.json need?

    Each product needs sku, name and a positive price. image, desc and interval are optional. At the top level, shipping.countries is an optional list of two-letter ISO codes that turns on address collection, and currency is recorded but not yet used, because hosted checkout charges in USD today.

    Can I sell subscriptions from a static site this way?

    Not today. Marking a product with interval: month or interval: year is valid in the file, but recurring billing requires the direct payout mode, which is not switched on at the platform level, so a checkout containing a subscription returns an explicit error. Subscriptions also cannot share a basket with one-off products, because Stripe Checkout will not mix the two in one session.

    How do I change a price?

    Edit the catalogue and deploy again. Because the price lives in a deployed file rather than in a dashboard, a price change is a version-controlled diff — slower than clicking a field, and it means the price a customer was charged is always traceable to a specific deploy.

    Is anything I paste here uploaded?

    No. The validator and the generator are a small script that runs entirely in your browser, with no account, no signup and no network request. You can load this page once and use it offline, and the download is assembled in the page rather than fetched.

    clize shop init — the integration contract, printed

    Ship the file, and the checkout comes with the hosting.

    Deploy a static site with Clize, put the catalogue at its root, and the pricing endpoint, the orders and the buyer-facing lookup page are already there — no widget in your markup and no server to run.

    $ npm i -g @clize/clize && clize login
    $ clize deploy --handle yourshop
    $ clize shop init
    [ Agent Storefront → ]