CheckboxDocs

Consent receipts

For each subscriber checkout where receipts are on, a tamper-evident record of the disclosure shown next to the pay button and the consent given — a structured snapshot, hashed, chained, signed and re-rendered as an image on demand. Evidence you can produce.

When a subscriber disputes a renewal — a chargeback, a demand letter, a class-action complaint — the question is always the same: what did this person see next to the button, and did they agree? A consent receipt answers it for one checkout: "on March 3 at 14:14 UTC, this disclosure text was rendered visibly 12px above the Start subscription button, its smallest text 16px at 17:1 contrast, and the subscriber ticked this box." It records the disclosure as captured by a script on the page: what was rendered, not what the person read, and only where the script ran (see limits).

Receipts are evidence you can produce. They are not legal protection and not a legal opinion: whether a checkout complied with a law is for your lawyer (and, eventually, a court) to decide. Checkbox does not promise that any court or regulator will accept a receipt, and takes on no legal liability for your subscription program (Terms). (Pending attorney review.)

What a receipt contains

DisclosureIts visibly rendered text (personal data redacted), a sanitized copy of its HTML, and the presentation that matters legally, measured over every visible text run and reporting the worst: smallest font size, lowest text/background contrast; visibility, whether it was set off, above the fold, above the button and how far from it. Text that was in the page but not visibly rendered (collapsed, off-screen, clipped, transparent, zero-sized) is left out and counted, with the flag hidden_text
ConsentThe checkbox's checked state and label (the only form state captured)
ButtonThe label of the button that was pressed
PageURL without query string or fragment, viewport size, language, a coarse user-agent class (desktop-chrome)
TimeClient time and Checkbox's server time
SubscriberYour reference (customer_reference, set by your server), optionally an HMAC-SHA-256 of their email under your account's own key
IntegritySHA-256 of the canonical JSON receipt, its position in the site's hash-chained evidence vault, and Checkbox's Ed25519 signature. The signed copy holds a salted commitment to the subscriber identifiers, not the identifiers themselves (retention)

It is not a session replay and not a screenshot taken in the subscriber's browser. One structured snapshot is taken when the form is submitted (plus a layout-only measurement when the disclosure first renders, for proximity). GET /v1/receipts/:id/render.png re-renders "what the subscriber saw" from that snapshot on Checkbox's servers, in an isolated browser with no network and scripts disabled, and labels the image as a reconstruction.

What is never captured

  • No keystrokes, no mouse or touch movement, no scrolling history: nothing is recorded continuously.
  • No form-field contents. Every <input>, <textarea>, <select> and editable region (any contenteditable, role="textbox" and similar) — card number, name, email, address, password, promo code — becomes an empty box of the same size, and so do live regions (aria-live, role="status"/"alert", <output>). The only form state kept is whether the consent checkbox was ticked.
  • Echoes of typed values are masked. If your page displays what the subscriber typed ("Hi Jane", a shipping address preview, a card preview, a button reading "Subscribe as Jane"), that text is replaced by [masked]: at submit the script compares page text with field values in memory and never sends the values. This catches exact values and their words of four or more letters; very short values (a two-letter name) can slip through, so keep order summaries and contact details out of the element you mark. Mark anything else private with data-checkbox-ignore.
  • No cookies or storage, no iframes, images or other third-party content, no query strings. If your paths contain personal segments (/account/jane-doe/checkout), set a route template: data-receipt-path="/account/:user/checkout" on the marked element or the script tag (receipt-path on <checkbox-disclosure>).
  • The server re-validates everything: text is redacted again (email addresses, card-like numbers, phone numbers), the subtree is rebuilt from an allowlist of tags and style properties, free-form fields are restricted to known shapes, and snapshots over 64 KB are refused.

Set it up

Turn receipts on

Receipts are off until you turn them on, because you must tell subscribers about them first. Add the privacy-policy paragraph (or your counsel's version), then switch them on in the dashboard (site → More → Receipts) or with updateReceiptSettings (enabled: true).

With Checkbox Elements

<checkbox-disclosure> (elements.js 1.1.0 or later) already records a consent and sends a receipt snapshot with it. Checkbox keeps it only once you have turned receipts on for your account; until then the snapshot is discarded and the consent's receipt is null. Once on, each consent object carries the receipt id (rcpt_…). To bind receipts to your checkout, add receipt-token (below).

With the snippet

Add data-receipts="1" and your publishable key to the snippet tag, and mark the element that holds your automatic-renewal disclosure:

HTML
<script async src="https://app.checkbox.site/s.js" data-key="pk_6ea05a319dc611d8ce3327fe"
        data-api="https://app.checkbox.site" data-receipts="1" data-publishable-key="pk_test_..."></script>

<form action="/subscribe" method="post">
  <div data-checkbox-receipt data-receipt-token="rct_test_...">
    <p>Your Acme subscription renews automatically every month at $19.99 until you cancel.
       Cancel any time at acme.example/account.</p>
    <label><input type="checkbox" name="agree" required> I agree to the automatic renewal terms.</label>
  </div>
  <button type="submit">Start subscription</button>
</form>

Mark the disclosure

data-checkbox-receipt (or data-checkbox-disclosure) tells Checkbox exactly which element is the disclosure (detection.method: "marked", confidence high). Without a mark, the snippet looks for the text block next to the submit button that reads like a renewal disclosure and records how sure it is (detection.method: "auto", confidence: high | medium | low; low confidence adds the flag low_confidence_detection). Forms that do not look like a checkout (sign-in, search) produce no receipt at all, and submitting a form other than the checkout (a newsletter or search form on the same page) never records the checkout's disclosure: only a marked element inside the submitted form counts, or one linked to it with data-checkbox-receipt-form="<form id>".

Checkouts without a <form> (a button in a single-page app) call window.CheckboxReceipts.capture({ submit: button }).

Bind receipts to real checkouts

Anyone can send a request with your publishable key, so a receipt from a browser is a client claim. Mint a short-lived capture token on your server for each checkout page and render it into the page; receipts that present it are binding: "bound" and carry your customer_reference. Receipts without one are still recorded, flagged unbound, and count against a separate (smaller) daily limit so they can never crowd out bound receipts.

A bound receipt proves that your server issued the token for that checkout: it is merchant-attested, not independent proof that the snapshot is authentic. For Stripe-hosted checkouts, the receipt Checkbox reconstructs from Stripe's own record is the independent corroboration.

curl https://app.checkbox.site/v1/receipt_tokens \
  -u "$CHECKBOX_SECRET_KEY:" \
  -H 'Content-Type: application/json' \
  -d '{ "site": "site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b", "customer_reference": "cus_Q2x9", "expires_in": 3600 }'

The token is only good for binding one receipt of that site (max_uses, default 1: mint one per checkout attempt) for an hour (expires_in, up to a day). The subscriber can read it in the page, so an expired or used-up token binds nothing: such receipts are recorded unbound with the flag token_used_up / token_expired and carry no checkout or customer reference. Pass email to store an HMAC of it under your account's key (never the address).

Stripe-hosted Checkout and Payment Links

Checkbox's JavaScript cannot run on checkout.stripe.com or buy.stripe.com. For those checkouts the receipt is built from two parts:

  1. Your page before the redirect. The snippet takes the snapshot when the subscriber clicks through to Stripe (phase: "pre_checkout"). For Payment Links it adds client_reference_id=ckbx_<nonce> to the link (only when you have not set one) so the two records can be matched.
  2. The Checkout Session as Stripe recorded it. When checkout.session.completed arrives, Checkbox reads the session back with your connected Stripe credentials: consent_collection, consent.terms_of_service, custom_text, price, interval, trial and timestamps. That receipt is source: "stripe_checkout" (or "stripe_payment_link"), reconstructed: true, evidentiary_weight: "reconstructed_from_stripe" — a lower evidentiary weight than a direct capture, because Checkbox could not observe the page itself. Stripe's record has no wording for its own terms-of-service checkbox or pay button, so the receipt claims none (checkbox.label and submit_label are null) and the image draws them as dashed illustrative placeholders. It links to the pre-checkout snapshot in pre_checkout_receipt: a bound snapshot of the same session or token taken before the session completed (the earliest; several are flagged multiple_pre_checkout_candidates), else the Payment Link nonce's snapshot from the two hours before the session (flagged linked_by_client_nonce, since the subscriber can see and change the nonce). A Payment Link's client_reference_id is kept as client_reference, never as the trusted customer_reference (that comes from your token, else the Stripe subscription or customer). The receipt's mode is the Stripe event's.

To link them for Checkout Sessions you create, set the token id in the session's metadata:

Node
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: 'price_joy_monthly', quantity: 1 }],
  consent_collection: { terms_of_service: 'required' },
  custom_text: { submit: { message: 'Renews monthly at $19.99 until you cancel. Cancel any time in your account.' } },
  metadata: { checkbox_receipt_token: token.id },
  success_url: 'https://acme.example/thanks',
});

Find and verify a receipt

curl -G https://app.checkbox.site/v1/receipts \
  -u "$CHECKBOX_SECRET_KEY:" \
  -d site=site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b \
  -d customer_reference=cus_Q2x9

verifyReceipt re-hashes the stored receipt, checks the signature, verifies the site's evidence chain, and checks that everything the API, dashboard and image show (binding, source, text, snapshot, flags, identifiers) matches the signed document (checks.index; identifiers linked after signing, such as the Stripe session, are listed separately). To check without trusting Checkbox's servers, download the bundle (GET /v1/receipts/:id/export, or Download bundle in the dashboard) and run its verifier with keys you fetched yourself:

Shell
curl -sO https://app.checkbox.site/.well-known/checkbox-keys.json
unzip checkbox-receipt-rcpt_….zip -d receipt && node receipt/verify.mjs receipt --keys checkbox-keys.json

The bundle holds the canonical receipt JSON, receipt.sig.json, subscriber.json (the identifiers that open the signed commitment), the signed manifest with the receipt's chain links and the SHA-256 of every other file, receipt.png and render.html, a README for whoever receives it, and verify.mjs (Node.js 18+, no dependencies). Exit code 0 means every hash, link and signature checks out against the keys you supplied; a changed, missing or added file fails. The verifier prints the source, binding, evidentiary weight and flags from the signed record: rely on those, not on the README or the image.

Retention and erasure

Receipts are kept 4 years (1,461 days) by default: the limitation period of California's Unfair Competition Law (Bus. & Prof. Code § 17208), the longest commonly pleaded alongside automatic-renewal claims. (Pending attorney review: your counsel may want a different period.) Change it per account and mode with updateReceiptSettings (retention_days, 30–3653) or in the dashboard; it applies to receipts recorded afterwards. Expired receipts are deleted by a daily job. updateReceiptSettings with enabled: false stops collection (browser receipts then answer 403 receipts_disabled).

The write-once copy. Each receipt is also stored in the site's evidence vault, which is write-once (S3 Object Lock) and kept for the vault's period (7 years by default, EVIDENCE_RETENTION_DAYS), longer than the receipt itself. That copy holds the disclosure, its presentation, the page, the times and the flags, but no subscriber identifier: only a salted SHA-256 commitment to the customer / client reference, email HMAC, Stripe session, subscription, token and nonce. The identifiers and the salt live in the receipt's database row. When a receipt expires or is erased, the row and its salt are deleted, and the vault copy can no longer be tied to the subscriber, while the chain and signatures stay intact. (Pending attorney review: whether this meets a deletion request in your jurisdiction.)

Erasure requests. Delete a subscriber's receipts by your reference, email or Checkout Session:

curl https://app.checkbox.site/v1/receipts/erase \
  -u "$CHECKBOX_SECRET_KEY:" \
  -H 'Content-Type: application/json' \
  -d '{ "customer_reference": "cus_Q2x9" }'

Limits

  • Opt-in and best effort. Receipts exist only for checkouts on pages where receipts are on and the script ran. A receipt sent as the page navigates away can be dropped by the browser; a malformed snapshot is refused. A missing receipt says nothing about what the subscriber saw.
  • Rendered, not read. "Visibly rendered" means a box on the page with readable size and colour, not that the subscriber looked at it.
  • Merchant-attested binding. See bind.
  • Device clock. client_timestamp comes from the subscriber's device; receipts whose device time is more than 5 minutes from Checkbox's are flagged client_clock_skew. Checkbox's server time is authoritative.

Your obligations

Receipts are personal information about your subscribers that you collect; Checkbox processes it for you as your service provider (processor) under your agreement with Checkbox, and uses it for nothing else. Before turning receipts on:

Privacy policy

  • Say so in your privacy policy. For example (pending attorney review):

    When you subscribe, we record a snapshot of the subscription terms and consent checkbox shown to you at checkout — the text and how it was displayed, whether you ticked the box, the page address and the time — to document your consent. The contents of the form fields you fill in are not recorded; text our page displays near the terms may be. This record is kept by our service provider Checkbox (checkbox.site) on our behalf for up to four years and used only to document and defend the terms of your subscription. A copy that no longer identifies you may be kept in tamper-evident storage for up to seven years. You can ask us to delete your record.

  • Tell Checkbox only what you need to. Use your own subscriber reference (customer_reference), not names or email addresses; pass an email only as email (Checkbox stores an HMAC of it under your account's key; that hash is still personal information and is erased with the receipt).
  • Honour deletion requests with eraseReceipts (above).
  • California wiretap law (CIPA). Session-replay and chat tools have been sued under Cal. Penal Code § 631 for recording what visitors type. Receipts are designed to stay out of that pattern — one snapshot of your own disclosure at checkout submit, no keystrokes, no field contents, typed values masked where the page echoes them, no continuous recording — but your counsel should review your disclosure and setup. (Pending attorney review.)

Webhook

receipt.created fires for every receipt (browser, server and reconstructed). data.object is the receipt.

Reference

createReceiptToken, createReceipt, listReceipts, retrieveReceipt, renderReceipt, verifyReceipt, exportReceipt, eraseReceipts, retrieveReceiptSettings, updateReceiptSettings: see Receipts. MCP: list_receipts and get_receipt.

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.