Checkbox Elements
A drop-in web component (and React wrapper) that shows a locked automatic-renewal disclosure, checked by the rules engine, above your pay button with an unchecked consent checkbox, and records each consent as evidence.
<checkbox-disclosure> renders the disclosure for your terms — price, interval, trial, that it renews until cancelled, how to cancel — as one semibold line in your page's own font, size and colours (no box, no banner; since 1.3.0, or a bordered box with appearance="box"), directly above your pay button, with an unchecked, required consent checkbox. It keeps the pay button disabled until the shopper ticks the box, and records each consent with Checkbox, so you get a cons_… id to store with the subscription. The wording is the same locked text the rules engine verifies everywhere else.
Elements is a tool: you choose the terms it shows and remain responsible for your checkout and your subscription program. It is not legal advice, and Checkbox takes on no legal liability for your program (Terms).
Install
- Get the pinned script tag with Subresource Integrity from the manifest (or from
createDisclosure, below):
curl https://app.checkbox.site/v1/elements/manifest.jsonimport Checkbox from '@checkbox-site/node';
const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);
const manifest = await checkbox.elements.retrieveManifest();
console.log(manifest.script_tag);import os
from checkbox import Checkbox
checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])
manifest = checkbox.elements.retrieve_manifest()
print(manifest["script_tag"])- Put the script tag in
<head>and the element inside your sign-up form, directly above the pay button:
<script src="https://app.checkbox.site/v1/elements/1.2.0/elements.js"
integrity="sha384-…" crossorigin="anonymous" defer></script>
<form action="/subscribe" method="post">
<checkbox-disclosure
publishable-key="pk_test_..."
merchant-name="Acme" amount="1999" currency="usd" interval="month" trial-days="14"
cancel-url="https://acme.example/account/cancel"
customer-reference="cs_test_a1b2c3">
</checkbox-disclosure>
<button type="submit">Start free trial</button>
</form>- Your server receives
auto_renew_consent(the label text) andcheckbox_consent_id(cons_…) with the form. Store the id with the subscription — for examplemetadata: { checkbox_consent: 'cons_…' }on the Stripe Checkout Session — or setcustomer_referencelater withupdateConsent.
/v1/elements.js always serves the current version (cached for 5 minutes), handy while prototyping. In production pin /v1/elements/<version>/elements.js with its integrity; released versions never change.
Once you turn consent receipts on (after updating your privacy policy), each consent also records a consent receipt — the disclosure as rendered next to the button, hashed and signed. Add receipt-token="rct_…" (minted per checkout with createReceiptToken) to bind it to the checkout, and receipt-path="/checkout" if your page paths contain personal segments.
React
import { CheckboxDisclosure } from '@subcomply/elements/react';
export function Consent({ sessionId, onConsent }: { sessionId: string; onConsent: (id: string) => void }) {
return (
<CheckboxDisclosure
publishableKey={process.env.NEXT_PUBLIC_CHECKBOX_PUBLISHABLE_KEY!}
terms={{ merchant_name: 'Acme', amount: 1999, currency: 'usd', interval: 'month', trial_days: 14,
cancel_url: 'https://acme.example/account/cancel' }}
customerReference={sessionId}
onConsent={(consent) => onConsent(consent.id)}
/>
);
}In single-page apps that submit with fetch, use recordConsent="manual" and call ref.current.recordConsent() before creating the subscription. In manual mode recordConsent() rejects when Checkbox cannot be reached (code timeout after 6 seconds, or network_error), so wrap it: a Checkbox outage must not block the sale.
let consentId = null;
try {
consentId = (await ref.current.recordConsent({ customerReference: email })).id;
} catch (err) {
// Outage or timeout: create the subscription anyway, and record the consent later (or rely on your own record).
console.warn('Checkbox consent not recorded', err.code);
}
await createSubscription({ consentId });The React entry is published with the SDK packages; see the pre-release note.
Server-issued disclosures
createDisclosure derives the terms from the site's billing connection (or the terms you pass), generates the locked wording, runs the rules engine on your checkout with the element installed, and returns the markup to paste:
curl https://app.checkbox.site/v1/disclosures \
-u "$CHECKBOX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{
"site": "site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b",
"terms": {
"merchant_name": "Acme",
"amount": 1999,
"currency": "usd",
"interval": "month",
"trial_days": 14,
"cancel_url": "https://acme.example/account/cancel"
}
}'import Checkbox from '@checkbox-site/node';
const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);
const disclosure = await checkbox.disclosures.create({
site: 'site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b',
terms: { merchant_name: 'Acme', amount: 1999, currency: 'usd', interval: 'month', trial_days: 14, cancel_url: 'https://acme.example/account/cancel' },
});
console.log(disclosure.verification.passes);
console.log(disclosure.element_html);import os
from checkbox import Checkbox
checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])
disclosure = checkbox.disclosures.create(
site="site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b",
terms={"merchant_name": "Acme", "amount": 1999, "currency": "usd", "interval": "month",
"trial_days": 14, "cancel_url": "https://acme.example/account/cancel"},
)
print(disclosure["verification"]["passes"])
print(disclosure["element_html"])With disclosure="disc_…" on the element, it loads its text from Checkbox (publishable key, Origin-checked). Keep the terms attributes as well: they are the fallback if Checkbox cannot be reached. Without either, the element shows "Subscription terms unavailable" and keeps the pay button disabled.
Attributes
| Attribute | |
|---|---|
publishable-key | pk_test_… / pk_live_…. Without it the element still works but records nothing |
merchant-name, amount (minor units), currency, interval (day, week, month, year), interval-count, trial-days, intro-amount (first-period price, minor units; 1.4.0), intro-periods, cancel-url (https), cancel-timing (period_end or immediately) | Your terms |
disclosure | disc_… from createDisclosure |
site | site_…; optional (resolved from the page's domain), needed on localhost when you have several sites |
submit | CSS selector of the pay button(s) to gate; default: the form's submit buttons |
form | id of the form when the element is outside it |
customer-reference | Your id for the subscriber (a Stripe session or customer id). Emails are refused |
record-consent | submit (default), change (when ticked) or manual |
placement | auto moves the element directly above the pay button |
appearance | line (default): one semibold line in your page's font and colours; box: a bordered box |
name, consent-id-name | Form field names (default auto_renew_consent, checkbox_consent_id) |
api-base | Defaults to the origin the script was loaded from |
Events (they bubble): checkbox:ready, checkbox:change { consented }, checkbox:consent { id, consent }, checkbox:guardrail { violations, messages, fallbackApplied }, checkbox:error { code, message }. Methods: isConsented(), recordConsent({ customerReference?, metadata? }), consentId, checkGuardRails(), refresh().
With record-consent="submit" (the default) a Checkbox outage never blocks a sale: if recording fails or takes over 6 seconds, the form submits without the id and checkbox:error fires. With record-consent="manual" you call recordConsent() yourself and it rejects on an outage; catch it (see above) so the sale goes through.
Guard rails
Theme it with CSS custom properties (--checkbox-font-size, --checkbox-text-color, --checkbox-border-color, --checkbox-background, --checkbox-accent-color and others). After rendering, and on resize, the element measures what the shopper actually sees with the rules engine's own thresholds — font size at least the page's body size, WCAG AA contrast, visible, set off (semibold for line, a border for box), above the pay button and within 250px of it. If your theme or page CSS breaks size, contrast or the border, it restyles itself with safe inline styles, logs a warning and fires checkbox:guardrail. A checkbox ticked by a script is not consent: the element unticks it and reports prechecked.
Content-Security-Policy
The element uses no eval, no inline styles or <style> elements and no innerHTML. Allow script-src https://app.checkbox.site and connect-src https://app.checkbox.site.
Consent records
Each consent stores the disclosure text and label shown and their hashes, whether they match the locked wording, the terms, the checkbox state (checked, initially checked, required), server time, the page URL, Origin, a hash of the User-Agent, the element version and its on-screen measurement.
The publishable key is public, so a browser consent is a client claim: the customer-reference the page sends is kept as client_reference (unverified), never as customer_reference. Bind the consent to the real subscription from your server: the form carries checkbox_consent_id; call updateConsent with a secret key and the subscription id (or record the consent from your server with createConsent and a secret key). Browser consents are limited per site and day; corroborating page captures are stored for the first 50 a day. Origins match the site's own host and its subdomains; on shared hosts (*.vercel.app, *.netlify.app, *.onrender.com, *.github.io, …) each customer's subdomain is its own site. The full record goes into the site's evidence chain, and consent.created is sent to your webhooks. List them with listConsents.
Checkbox is compliance software, not a law firm; nothing in these docs is legal advice (details). You remain solely responsible for your subscription program, and Checkbox takes on no legal liability for it (Terms). Checkbox is a System1 company; Trust center · Status. For AI agents: every page is available as Markdown at the same URL plus .md; the index is /llms.txt.