# 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](https://checkbox.site/terms)).

## Install {#install}

1. Get the pinned script tag with Subresource Integrity from the manifest (or from `createDisclosure`, below):

```curl
curl https://app.checkbox.site/v1/elements/manifest.json
```
```node
import 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);
```
```python
import os
from checkbox import Checkbox

checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])

manifest = checkbox.elements.retrieve_manifest()
print(manifest["script_tag"])
```

2. Put the script tag in `<head>` and the element inside your sign-up form, directly above the pay button:

```html
<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>
```

3. Your server receives `auto_renew_consent` (the label text) and `checkbox_consent_id` (`cons_…`) with the form.
   Store the id with the subscription — for example `metadata: { checkbox_consent: 'cons_…' }` on the Stripe Checkout
   Session — or set `customer_reference` later with `updateConsent`.

`/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](https://checkbox.site/docs/receipts#turn-on) (after updating your privacy policy), each consent also
records a [consent receipt](https://checkbox.site/docs/receipts) — 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 {#react}

```tsx
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.

```tsx
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](https://checkbox.site/docs/sdks/node#install).

## Server-issued disclosures {#server}

`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
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"
    }
  }'
```
```node
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);
```
```python
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 {#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 {#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 {#csp}

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 {#consents}

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`](https://checkbox.site/docs/api/elements#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](https://checkbox.site/docs/concepts/evidence), and `consent.created` is sent to your [webhooks](https://checkbox.site/docs/webhooks).
List them with [`listConsents`](https://checkbox.site/docs/api/elements#listConsents).
