For Website Developers

Buy Now Buttons

Sell a single product from a branded page on your own domain - indexed, tracked, and paid through Stripe Checkout

Buy Now Buttons

A Buy Now button sells a single product immediately - no shopping cart. The shopper clicks it and goes straight to checkout for a one-time payment, a monthly subscription, or a buyer-chosen amount (donations).

The button lives on a page on your site built from your template. That page is:

  • Indexed - the product page can be crawled and found independently.
  • Tracked - your analytics and conversion pixels fire on the page before the shopper leaves for checkout.
  • On-brand - full landing-page copy, images, and design in your site's look.
  • Recorded - sales appear in the dashboard alongside cart orders, and the product is defined once in your site content.

Use Buy Now for single-product landing pages, a "pay for this" link you share, or anywhere a full cart is overkill. For multi-item orders, discount codes, or quantity selection, use the shopping cart instead.

Good fits

Buy Now is ideal whenever the shopper pays one fixed amount, once, for one thing - and never combines it with another item. Common cases:

  • Deposits and retainers - a flat fee to hold a service appointment (photographer, consultant, contractor, salon).
  • Single-service payment - a fixed-price consultation, coaching session, or one-off deliverable.
  • Reservation / RSVP fee - a fixed charge to reserve a place in a workshop or class.
  • Donations and tipping - preset "Give $25" / "Give $100" buttons, an "enter any amount" field (custom amounts), or both - one-time or monthly.
  • Sponsorships - a fixed sponsorship level (e.g., sponsor a kennel for $250).
  • Memberships - a recurring monthly charge for supporter tiers or member content (monthly subscriptions).
  • Product launch pages - a focused pitch for one hero product with a tracked, shareable landing page.

Rule of thumb: if a shopper pays for one thing and never combines it with another item, Buy Now is the right fit.

How it works

  1. A shopper submits a small HTML form (the Buy Now button) that POSTs a product id to /app/shop/buy on your own domain.
  2. The server looks the product up in your built HTML (the same crawl the cart uses), confirms the price, and creates a pending order.
  3. The shopper is redirected straight to Stripe Checkout to pay.
  4. After payment they land on the standard success page, and the order is recorded in the dashboard.

Buy Now buttons reuse the same connected Stripe account, the same server-side price validation, and the same order records and success pages as the shopping cart integration. The differences: one product, quantity one, no cart, and no JavaScript - and backing out of checkout returns the shopper to the page they started from (see Backing out of checkout).

Prerequisites

  1. The website owner has connected Stripe - see Connecting Stripe.
  2. A product exists in your built HTML - the button itself carries the product data the server crawls (below). No catalog upload, no dashboard setup.

Unlike the cart, Buy Now buttons require no JavaScript.

The button

A Buy Now button is a plain HTML form that POSTs the product id to the hosted endpoint:

<form method="post" action="/app/shop/buy">
  <input type="hidden" name="product" value="tshirt-001">
  <button type="submit"
          data-product-id="tshirt-001"
          data-name="Classic T-Shirt"
          data-price="2500">
    Buy now
  </button>
</form>

That's the whole integration. No <script> is required; the button works with JavaScript disabled.

Data attributes

The data-* attributes are the same ones the server crawls for the cart, so a product used for Buy Now is validated identically (and can also be added to the cart if you expose both).

Required

  • data-product-id - Unique product identifier. Must match the hidden product field.
  • data-name - Product display name (shown on Stripe Checkout).
  • data-price - Price in cents (e.g., 2500 = $25.00).

Optional

  • data-currency - Three-letter currency code (default: usd).
  • data-image-url - Product image, shown on the Stripe checkout page.
  • data-shippable - Set to "false" for digital goods so no shipping fee or address is collected (default: true). See Shipping.
  • data-description - Short description shown on Stripe Checkout.
  • data-interval - Set to "month" to charge the price every month instead of once. Requires data-shippable="false". See Monthly subscriptions.
  • data-custom-amount - Set to "true" to let the buyer choose the amount. See Custom amounts.
  • data-min-amount / data-max-amount - Bounds in cents for custom amounts (defaults: 100 / 100000, i.e. $1-$1,000).

The hidden product field

The form's hidden product field carries the id in the POST body and must match data-product-id. The server reads the price, name, currency, and shippable flag from your crawled HTML by that id - it never trusts a price submitted by the browser, so a tampered hidden field can't change what's charged.

SKUs must be static. The server validates the product by finding its data-product-id in your built HTML. Don't generate or rewrite the id from JavaScript - a product that isn't present in the built page fails with "Product not found".

Quantity

Buy Now always purchases exactly one unit. There is no quantity selector, and any qty field is ignored. Shoppers who want multiples use the cart.

Pricing

Prices are read from data-price in cents and verified server-side against your built HTML. If you change a price, rebuild the site - the old price is used until the new HTML is published.

Monthly subscriptions

Add data-interval="month" and the button charges the price every month through a Stripe subscription instead of a one-time payment:

<form method="post" action="/app/shop/buy">
  <input type="hidden" name="product" value="supporter">
  <button type="submit"
          data-product-id="supporter"
          data-name="Monthly Supporter"
          data-price="500"
          data-interval="month"
          data-shippable="false">
    Become a supporter - $5/month
  </button>
</form>
  • Digital only. Recurring products must set data-shippable="false" - subscriptions can't carry a shipping fee. A shippable recurring product fails at buy time with a clear error.
  • Managing the subscription. Every receipt email for a subscription includes a Manage subscription link, where the subscriber can cancel or update their card via Stripe's customer portal.
  • Renewals are recorded. Each monthly charge appears in the dashboard as its own order and (if configured) sends a branded receipt.
  • Only month is supported. Recurring products can't be added to the cart.

Custom amounts (donations)

Add data-custom-amount="true" and the buyer chooses the amount. The form gains an amount field (dollars, e.g. 25 or 12.34):

<form method="post" action="/app/shop/buy">
  <input type="hidden" name="product" value="donate">
  <input type="number" name="amount" min="1" step="0.01" placeholder="Amount" required>
  <button type="submit"
          data-product-id="donate"
          data-name="Donation"
          data-price="0"
          data-custom-amount="true"
          data-shippable="false">
    Donate
  </button>
</form>
  • Bounds are enforced server-side. data-min-amount / data-max-amount are authored in cents and read from your built HTML - never from the form. Defaults: $1 minimum, $1,000 maximum. The platform caps custom amounts at $1,000 regardless of what you author.
  • data-price may be 0 (the buyer picks the amount) or a suggested default.
  • Preset buttons are just HTML. Radio buttons or preset <button> elements that fill the single amount field give you "Give $10 / $25 / $50 / other" with no extra platform support.
  • Custom-amount products can't be added to the cart.

Monthly donations: combine both flags - data-interval="month" data-custom-amount="true" - and the buyer's chosen amount is charged every month.

Shipping

Physical products honor your site's flat-rate shipping config and collect a shipping address at Stripe, exactly like the cart. Add a data-shop-config element once in a shared layout:

<div data-shop-config data-shipping-flat="500" data-shipping-countries="US"></div>
  • data-shipping-flat - Flat fee in cents, charged once per order. 0 (or no element) disables shipping.
  • data-shipping-countries - Comma-separated ISO country codes the shopper may ship to (default: US).

Mark digital products data-shippable="false" so they never trigger a shipping fee or address collection. See Shop Integration → Shipping for the full reference.

Backing out of checkout

If the shopper clicks Back on the Stripe Checkout page instead of paying, they return to the page where they clicked the Buy Now button - not the shop's generic cancel page. This works automatically: the browser sends the originating page's URL with the form POST (the Referer header), and the server uses it as the checkout's cancel destination. Only same-site destinations are honored.

Two cases need attention:

  • Strict referrer policies. If your site sets Referrer-Policy: no-referrer (or an equivalent <meta name="referrer"> tag), the browser won't send the originating URL and the shopper falls back to the site's default cancel page.
  • Forcing a specific return page. Add an optional hidden back field with a site-relative path; it takes precedence over the referrer:
<form method="post" action="/app/shop/buy">
  <input type="hidden" name="product" value="tshirt-001">
  <input type="hidden" name="back" value="/products/classic-tshirt/">
  ...
</form>

The back value must be a path on your own site (starting with /). Full URLs and off-site values are ignored.

What's not supported

Buy Now is intentionally minimal. For any of these, use the shopping cart:

  • More than one item in a single payment
  • Discount codes
  • Per-line customizations (data-customizations)
  • E-sign before payment

And note the reverse: recurring (data-interval) and custom-amount (data-custom-amount) products work only with Buy Now - the cart rejects them.

Errors and failures

If a purchase can't proceed - the product isn't found, a price changed but the site wasn't rebuilt, or the owner hasn't connected Stripe - the shopper sees a short error page and is not charged. No Stripe session is created until the product validates, so a bot or link preview that never submits the form never creates an order.

Authoring Buy Now buttons in the CMS

There is no separate "buy now" dashboard - a Buy Now button is just a product in your content, rendered as a form. Wire it once, and the website owner creates and edits products from the CMS like any other content. (A link you can paste in an email is simply the URL of a product page that contains a Buy Now button.)

1. Add a products collection to your admin/config.yml so owners get an editor for products:

collections:
  - name: products
    label: Products
    label_singular: Product
    folder: _products
    create: true
    slug: '{{id}}'
    fields:
      - { name: id, label: Product ID, widget: string, hint: "Unique code, e.g. tshirt-001 - don't change once live" }
      - { name: name, label: Name, widget: string }
      - { name: price_cents, label: Price (cents), widget: number, value_type: int, hint: "2500 = $25.00" }
      - { name: image, label: Image, widget: image, required: false }
      - { name: shippable, label: Ships physically, widget: boolean, default: true }
      - { name: body, label: Description, widget: markdown, required: false }

Storing the price in cents matches what the server expects and avoids rounding surprises. If you'd rather let owners type dollars, store a price field and convert in the template with {{ page.price | times: 100 | round }}.

2. Render the Buy Now form from those fields. Add _includes/buy-now.html:

<form method="post" action="/app/shop/buy">
  <input type="hidden" name="product" value="{{ include.id }}">
  <button type="submit" class="btn btn-primary"
          data-product-id="{{ include.id }}"
          data-name="{{ include.name | escape }}"
          data-price="{{ include.price_cents }}"
          {% if include.image %}data-image-url="{{ include.image }}"{% endif %}
          data-shippable="{{ include.shippable }}">
    Buy now
  </button>
</form>

Then use it on your product layout:

{% include buy-now.html id=page.id name=page.name price_cents=page.price_cents image=page.image shippable=page.shippable %}

3. The owner adds a product in the CMS (Products → New Product), fills in the name, price, and image, and publishes. After the site rebuilds, the product page has a working Buy Now button. Publishing the product is publishing the Buy Now page - to share it, the owner copies the product page's URL.

The owner-facing steps live in Creating Buy Now Buttons.

Troubleshooting

"Product not found"

The product's data-product-id isn't in the built HTML. Make sure the button (or the rendered product page) is published and the site has been rebuilt, and that the hidden product field matches data-product-id.

The price is wrong

Prices come from your built HTML, not the browser. Update data-price (or the CMS price_cents field) and rebuild the site.

"Not set up to take payments"

The website owner hasn't connected Stripe. See Connecting Stripe.

See also