Skip to content
OpenStock
OpenStock

Get started

Developer platformAuthenticationAPI quickstart

Checkout

Hosted checkoutCheckout embedCheckout Elements

Events

Webhooks
Dashboard
  1. Developers
  2. Checkout

Checkout Elements

Build your own checkout layout from frames that match your site, with payment still handled by OpenStock.

Checkout Elements put the OpenStock checkout inside your own page, one piece at a time. You place the cart summary, email field, payment methods and pay button wherever your design wants them and style them to match. Payment still runs on OpenStock: card numbers, codes and orders never touch your page or your server.

<div id="checkout"></div>

<script src="https://openstock.dev/elements.js"></script>
<script>
  const elements = OpenStockElements({
    store: 'acme',
    items: [{ product: 'PRODUCT_ID', quantity: 1 }],
  })

  elements.create('checkout').mount('#checkout')
</script>

Want a modal instead?

The checkout embed opens the full checkout over your page with one script tag. Elements are for when the checkout should be part of the page itself.

How it works#

One group, many elements

OpenStockElements() creates a group for one cart. Every element in the group shares the same email, payment method, coupon and total, so you can split them across your layout.

Each element is a small frame

Elements load from your store's domain (acme.openstock.dev) and size themselves to their content. A hidden frame runs the actual checkout; the elements only show its state and send it what the buyer does.

Payment

The pay element, or your own button calling confirmPayment(), starts the payment. Methods with their own page (card processors, PayPal, crypto) send the buyer there and back to the order page. Balance and gift card payments finish in place and send the buyer to returnUrl with ?order= added.

Elements#

TypeShows
checkoutEverything below in one frame. Pass { showSummary: false } to leave the cart out.
summaryProducts, quantities, discounts and the total.
emailThe address that gets the receipt and the delivery.
paymentThe store's payment methods, including the gift card code field.
couponA coupon field that shows the saving once applied.
consentTerms and privacy acceptance, plus the marketing opt-in when the store has it on.
payThe pay button with the live amount and anything still missing.

Each type can be created once per group. create() returns an element with mount(target), unmount(), update(options), focus() and on(event, handler).

Build your own layout#

const elements = OpenStockElements({
  store: 'acme',
  items: [
    { product: 'PRODUCT_ID', quantity: 2 },
    { product: 'OTHER_ID', variant: 'VARIANT_ID' },
  ],
  locale: 'auto',
})

elements.create('summary').mount('#summary')
elements.create('email').mount('#email')
elements.create('payment').mount('#payment')
elements.create('coupon').mount('#coupon')
elements.create('consent').mount('#consent')

// Your own button instead of the pay element:
document.querySelector('#buy').addEventListener('click', async () => {
  const { error, order } = await elements.confirmPayment({
    returnUrl: 'https://acme.com/thanks',
  })
  if (error) showMessage(error.message)
})

Options#

OptionDescription
storeYour store slug, the part before .openstock.dev. Required.
itemsUp to 20 of { product, quantity?, variant? }. Prices always come from your store; quantities are capped at 99.
brandMode, tokens, fonts and rules. See below.
localeen, es, fr, de, ja, zh or auto (your page's lang, then the browser).
returnUrlWhere to go after an in-place payment. Can also be passed to confirmPayment().
storeUrlYour custom domain, if the store has one.

The group also has update({ brand, items }), edit() to clear a failed payment's message, getState() and destroy().

Branding#

OpenStockElements({
  store: 'acme',
  items,
  brand: {
    mode: 'auto',                         // 'light' | 'dark' | 'auto'
    fonts: ['Inter:wght@400;500;600'],    // Google Fonts only
    tokens: {
      accent: '#7c3aed',
      radius: '12px',
      font: 'Inter, sans-serif',
    },
    rules: {
      '.os-button-primary': { 'border-radius': '999px' },
      '.os-method[data-selected]': { 'border-color': '#7c3aed' },
      '.os-input:focus': { 'box-shadow': '0 0 0 3px #ede9fe' },
    },
  },
})

// Restyle later, e.g. when your site switches theme:
elements.update({ brand: { mode: 'dark' } })

Tokens#

accent, accentText, text, textMuted, border, surface, surfaceSunken, danger, success, warning, radius, gap, font, fontMono, fontSize. Colors take hex, rgb(), hsl(), oklch() or a named color. When you set accent without accentText, the text on it is picked for contrast.

Rules#

A rule targets one class from the list below, optionally with one state ([data-selected], [data-disabled], [data-loading], [data-invalid], [data-checked], [data-locked]) and one pseudo-class (:hover, :focus, :focus-visible, :focus-within, :active, :disabled, :checked, ::placeholder). Rules always win over the defaults.

.os-element .os-checkout .os-section .os-heading .os-field .os-label .os-input .os-hint .os-error .os-button .os-button-primary .os-button-secondary .os-link .os-summary .os-line .os-line-image .os-line-name .os-line-meta .os-line-price .os-row .os-row-label .os-row-value .os-total .os-total-label .os-total-amount .os-payment .os-methods .os-method .os-method-icon .os-method-label .os-method-detail .os-method-note .os-method-panel .os-coupon .os-coupon-applied .os-consent .os-check .os-check-box .os-check-label .os-pay .os-alert .os-skeleton

What a rule can't do

A checkout that can be restyled freely can be made to show a different price than it charges. So rules can't use position, offsets, z-index, display, visibility, opacity, transform, clip-path, masks, filters, content, pointer-events or url(). Text can't be transparent or smaller than 11px, and !important is removed. Anything refused is reported as a trace event and in the console.

Fonts load from Google Fonts only: pass Family:wght@400;600, a { family, weights } object or a fonts.googleapis.com URL, and the link is rebuilt from the family and weights. Set preset: 'bare' to drop the default surfaces and borders and style everything yourself.

Events#

elements.on('ready', () => hideSpinner())
elements.on('change', (state) => {
  buyButton.disabled = !state.complete
  totalLabel.textContent = state.total
})
elements.on('error', (error) => console.warn(error.code, error.message))
elements.on('navigate', (event) => {
  // event.url, event.reason: 'payment' | 'complete' | 'hosted-checkout'
  // Call event.preventDefault() to handle the navigation yourself.
})
elements.on('trace', ({ message }) => console.debug(message))
EventWhen
readyEvery mounted element and the checkout behind them have loaded.
change{ complete, empty, email, method, total, currency, missing, loading }
error{ type, code, message, element, recoverable }. type is validation, payment or load.
completeAn order was paid without leaving the page (balance or a gift card).
navigateThe buyer is about to go to a payment page, a delivery page or the hosted checkout.
resizeAn element changed height. Frames resize themselves; this is for your layout.
focus / blurFocus moved into or out of an element.
traceA brand token, rule or font was refused, with the reason.
loaderrorAn element or the checkout did not load within 15 seconds.

Good to know#

  • Orders with shipped items or a required Discord connection show a button to the full checkout instead of the pay button, because those steps need it.
  • elements.js adds no style or script tags to your page, so it works under a strict Content Security Policy. Allow frame-src https://*.openstock.dev (or your custom domain).
  • Messages from the frames are only accepted from your store's origin, and the frames only talk to each other inside the group.

Checkout embed

The whole checkout in a modal, from one script tag.

Webhooks

Get a signed event when an order is paid.

Was this page helpful?

PreviousCheckout embedNextWebhooks
© 2026 OpenStockTermsPrivacy