Developers · Browser SDK

One script tag. Every price is a surprise.

quantum-billing.js adds eight drop-in web components and a small promise-based API to any page. No dependencies and no build step. Everything below the fold is rendered live by the library.

v1.0.0 · 0 dependencies · 8 elements · observed 0 times since you arrived

Install #

Drop in one script tag. The library defines its custom elements on load and exposes window.QuantumBilling. This page loads it from /sdk/v1/quantum-billing.js.

html
<script src="https://getquantumbilling.com/sdk/v1/quantum-billing.js"></script>

Or use a package manager. The package is the same file with a package.json around it.

shell
npm install @quantum-billing/js
javascript
import QuantumBilling from "@quantum-billing/js";

Single file, plain JavaScript, about 57 KB unminified. Works as a classic script, as a CommonJS module, and as a global. It does not touch the network.

Initialize #

Call QuantumBilling.init() once. Components find the latest instance through QuantumBilling.instance. If you never call it, they initialize themselves with a demo key and mention nothing.

javascript
const qb = QuantumBilling.init({
  publishableKey: "pk_superposed_8fQ2x",
  customer: "cus_halcyon",     // optional
  observer: "you",             // optional, appears in events
  collapse: "on-observe",      // "on-observe" | "aggressive" | "reduced"
  currency: "auto",            // "auto" or a fixed ISO code
});

Prefer markup? Put the same options on the script tag and skip the JavaScript.

html
<script src="https://getquantumbilling.com/sdk/v1/quantum-billing.js"
        data-publishable-key="pk_superposed_8fQ2x"
        data-customer="cus_halcyon"></script>
OptionDefaultDescription
publishableKeynoneAny string. A console warning appears if it does not start with pk_. The key is not verified.
customerrandomCustomer id, e.g. cus_halcyon. Their name, plan and seats are observed fresh each time.
observer"you"Label for whoever is looking. Appears in the portal.
collapse"on-observe"on-observe: components collapse on hover, focus and scroll-back. aggressive: also every 6 to 10 seconds. reduced: only on an explicit observe(). Animations are separately reduced when the OS asks for reduced motion.
currency"auto"auto picks a currency per collapse (USD-weighted). Or fix one: USD, GBP, EUR, JPY, BTC, QBC.

Try the collapse modes

Live · this page's instance
collapse
Hover or focus any demo on this page.
  1. collapsewaiting for something to be observed…

Pricing table #

Plan cards with an interval toggle. Prices and features re-collapse on hover, on focus, and every time the table scrolls back into view. Annual billing sometimes costs more than twelve months. Choosing a plan fires qb:select, where the plan you receive may differ from the one you requested.

Live demohover a card, scroll away and back
html
<qb-pricing-table plans="starter,growth,enterprise" interval="month" highlight="growth"></qb-pricing-table>

<script>
  document.querySelector("qb-pricing-table").addEventListener("qb:select", (e) => {
    const { requested, selected } = e.detail;   // selected sometimes differs
    // e.preventDefault() skips the checkout modal
  });
</script>
AttributeDefaultDescription
plansstarter,growth,enterpriseComma-separated plan ids, in display order.
intervalmonthmonth or year. Also settable by the built-in toggle.
highlightnonePlan id to emphasize. The plan's name and badge may drift anyway.
DOM eventDetailDescription
qb:select{ requested, selected }Bubbles, composed, cancelable. Unless prevented, qb.checkout.open({ plan: selected }) follows.

Checkout #

A three-step modal: review the plan, choose which universe to be billed in, confirm. There is no card field, no CVC and no password. The payment method is read-only and belongs to someone else. The confirmation total will not match the quote.

Live demoopens a modal
Upgrade to GrowthUpgrade to Enterprise
html
<qb-checkout-button plan="growth">Upgrade</qb-checkout-button>

<script>
  qb.on("checkout", ({ plan, universe, total }) => console.log(plan, universe, String(total)));
  // or open it yourself:
  const result = await qb.checkout.open({ plan: "growth" });  // { status, plan, universe, total }
</script>
AttributeDefaultDescription
plangrowthPlan id to check out. Slotted content is the button label.

The button also dispatches qb:checkout (bubbles, composed) with the result when an order completes.

Customer portal #

A mini portal in a modal: plan, entitlements, recent invoices and a cancel button. The cancel button fails in a different way each time, and occasionally moves.

Live demoopens a modal
Manage billing
html
<qb-portal-button>Manage billing</qb-portal-button>

<script>qb.portal.open();</script>

Entitlement gate #

Show different content depending on whether a feature is on, off or maybe. It collapses when viewed or hovered. If you provide no maybe slot, the default content is shown with a small ◐ ribbon explaining the maybe.

Live demohover it. then hover it again.
SSO is enabled. Sign in with your identity provider.
SSO is not available on your plan. Today.
SSO is available to some of you.
Audit logs are on. They record what happened, in one of the universes.
Audit logs are off, so nothing happened.
You may cancel. Press the red button.
Cancellation is not available. See: every other feature.
html
<qb-entitlement feature="sso">
  <div>shown when on</div>
  <div slot="off">shown when off</div>
  <div slot="maybe">shown when maybe</div>
</qb-entitlement>
Attribute / eventDetailDescription
featurefeature ide.g. sso, audit_logs, api_access, cancel. The cancel feature is heavily weighted off.
qb:entitlement{ feature, state, note }Bubbles, composed. Fires every time the gate collapses. The host also reflects state as an attribute for CSS.

Usage meter #

A meter for any metric. The limit is also observed, so the bar may exceed 100%. Overage is shown in the same colour as concern.

Live demorole="meter"
html
<qb-usage-meter metric="api_calls" label="API calls"></qb-usage-meter>
AttributeDefaultDescription
metricapi_callsAny metric id. Known: api_calls, seats, storage, events, observations. Unknown metrics are measured in "units of measure".
labelmetric nameDisplay label.

Invoice list #

Recent invoices as rows. Click a row to open a preview whose total disagrees with the row. Both are correct as of their observation.

Live democlick a row
html
<qb-invoice-list limit="4"></qb-invoice-list>
AttributeDefaultDescription
limit4Number of rows, 1 to 24.

Observation badge #

A small pill showing this page's live observation count, with an amber ping on every collapse. Adding it to a page increases the number it shows.

Live demostay still and it will not change. you cannot stay still.
html
<qb-observation-badge></qb-observation-badge>

Inline price #

A price you can put in a sentence. It re-collapses each time it scrolls into view, so the sentence stays technically true.

Live demoinline

Growth is and Enterprise is , billed however we feel.

html
<p>Growth is <qb-price plan="growth"></qb-price>.</p>
AttributeDefaultDescription
plangrowthPlan id.
intervalmonthmonth or year. The returned interval may be "/fortnight" or "/observation".

Events #

Subscribe with qb.on(event, handler). It returns an unsubscribe function. Handlers run synchronously, inside the observation.

javascript
const off = qb.on("collapse", ({ target, kind, before, after, observation }) => {
  console.log(`#${observation}`, kind, before, "→", after);
});
off();   // stop watching. it will not help.
EventPayloadWhen
collapse{ target, kind, before, after, observation }Any observation: every API read, component render, and observe(). target is a string (tag#id for elements, or a resource name); before/after are plain JSON-serializable snapshots; kind is e.g. plans.list.
select{ requested, selected }A plan was chosen. selected sometimes differs.
checkout{ plan, universe, total }An order was confirmed. total prints as a string and also has amount and currency.
entitlement{ feature, state, note }An entitlement was checked. state is on, off or maybe.

DOM events

Components also dispatch bubbling, composed events: qb:select, qb:entitlement, qb:checkout, qb:portal.

JavaScript API #

Every read resolves after 40 to 400 ms of simulated latency, increments qb.observations and emits collapse. About 3% of results carry superposed: true.

MethodResolves toTry it
qb.observationsnumber, collapses on this page
qb.on(event, fn)unsubscribe function
qb.observe(target?)Promise of the observation number. Forces a collapse of one element, a selector, or everything.
qb.plans.list()[{ id, name, price: { amount, currency, interval, display }, features: [{ id, label, state }], badge }]
qb.customer.get(){ id, name, plan, seats: { used, total }, nextCharge: { date, display }, paymentMethod: { brand, last4, display }, balance: { amount, currency, display } }. date is YYYY-MM-DD; last4 is null for non-card methods. Each nested object prints as its display.
qb.entitlements.check(id){ feature, state, note, observation }
qb.entitlements.list()array of the above, for every feature
qb.invoices.list()[{ id, period, amount, display, status }]
qb.usage.get(metric){ metric, used, limit, unit, percent }. percent may exceed 100.
qb.checkout.open({ plan })Promise of { status, plan, universe, total } when the modal closes
qb.portal.open()Promise of { status } when the modal closes
output
// press Run. the result will be different from the one in the docs.

Additions

  • qb.select(planId): what qb-pricing-table calls. Resolves { requested, selected } and emits select.
  • qb.invoices.get(id): a single invoice preview, with a total that disagrees with the list.
  • qb.configure({ collapse, currency }) changes modes at runtime. qb.destroy() detaches the instance.
  • Every read accepts a trailing options object. plans.list({ plans, interval }) and invoices.list({ limit }) narrow the result.
  • Money-like results (balance, checkout.total) coerce to their display string when printed.

Theming #

Components render in Shadow DOM and read CSS custom properties from their ancestors. Set them on :root or on any wrapper. Defaults follow prefers-color-scheme. Modals take the properties of the element that opened them.

PropertyLight defaultDescription
--qb-accent#3047ffButtons, highlights, focus rings.
--qb-bg#ffffffCard and modal background.
--qb-fg#0d1726Primary text.
--qb-muted#58657eSecondary text.
--qb-border#d4dbe8Borders and dividers.
--qb-radius12pxCard radius. Buttons derive from it.
--qb-fontsystem sansText face.
--qb-font-monosystem monoNumbers and labels.

Bonus: --qb-accent-fg sets the text colour on accent buttons.

Live theme
Upgrade
css
qb-pricing-table, qb-usage-meter, qb-invoice-list {
  --qb-accent: #e8590c;
  --qb-radius: 4px;
  --qb-font: "Iowan Old Style", serif;
}

Guarantees #

The SDK makes the following commitments. Each was true when you read it.

  1. Every render is final, until the next one.Whatever you see is the value. The next thing you see is also the value.
  2. Reading counts.Every API call is an observation. qb.observations is exact, and so is the other number.
  3. No secrets are collected.There is no card field, no CVC and no password field anywhere in the SDK.
  4. Accessible by default.Modals are real dialogs with a focus trap, Escape to close, focus restoration and scroll lock. Animations stop when the OS asks them to.
  5. Well-behaved.Components work when added after load, work in multiples, and clean up their observers and timers when removed.
  6. Sandbox by default.Demo keys never bill. Live keys bill when observed.

Source of this page's demos: /sdk/v1/quantum-billing.js. Also see the server SDKs and the changelog.