Compliance checks on every pull request
checkbox.json lists your Stripe Checkout params, disclosure, pages and lifecycle emails; checkbox ci checks them against automatic-renewal laws on every code change, offline, with annotations and the corrected params in the pull request.
A refactor that drops custom_text from your Checkout Session, a new pricing page without the renewal terms, a "friendlier" welcome email that no longer says how to cancel: each one quietly breaks an automatic-renewal law requirement. checkbox ci re-checks the files that produce your sign-up flow and emails on every pull request and fails the build when one needs a fix, with the exact change to make.
- Offline by default. The rules engine ships in the CLI: no API key, no network, nothing leaves the runner.
- Points at the code. Annotations land on the file (and line) that needs the change; the job summary has the fix and the engine-verified corrected params or text.
- Same verdicts as the API. The checks are the preflight checks (
/v1/checks/*).
Quick start
npx @checkbox-site/cli ci init --yes # checkbox.json + .github/workflows/checkbox.yml, from your project
npx @checkbox-site/cli ci # run every check locallyci init reads the project the way checkbox init does: every stripe.checkout.sessions.create({...}) call, plans from inline price_data, email templates by file name (emails/welcome.html is an acknowledgment, trial-ending.txt a trial reminder), built pricing / checkout pages and plain-text disclosures. What it cannot tell is written as a "TODO: …" value, which checkbox ci refuses (exit 3) until a person or agent fills it in. It never overwrites an existing file without --force; on a terminal it asks first, --yes writes without asking, --dry-run only shows the result. checkbox init --ci does the same.
Then commit both files. The workflow:
name: Checkbox
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
checkbox:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
# Only needed when checkbox.json uses "module" entries that import your dependencies:
# - run: npm ci
- uses: checkbox-site/checkbox@v1
# with:
# strict: true # also fail when a check needs review
# args: --online # URL pages via the API; needs the env below
# env:
# CHECKBOX_SECRET_KEY: ${{ secrets.CHECKBOX_SECRET_KEY }}checkbox.json
The file sits at the project root (.checkbox.json works too; --config points elsewhere). Paths are relative to it. Add "$schema" and editors complete and validate it from /schemas/checkbox.json.
{
"$schema": "https://checkbox.site/schemas/checkbox.json",
"merchant": { "name": "Acme", "cancel_url": "https://acme.example/account/cancel", "support_contact": "support@acme.example" },
"jurisdictions": "all",
"plans": [
{ "name": "Pro", "price": 19.99, "currency": "usd", "interval": "month", "trial_days": 14 },
{ "name": "Pro yearly", "price": 199, "currency": "usd", "interval": "year" }
],
"checks": {
"stripe_checkout": [
{ "name": "Checkout route", "source": "app/api/checkout/route.ts" },
{ "name": "Yearly checkout", "module": "lib/checkout.ts#yearlyParams", "args": [{ "customerEmail": "test@acme.example" }], "plan": "Pro yearly" }
],
"disclosures": [{ "name": "Pay-button terms", "file": "components/renewal-terms.txt", "plan": "Pro", "consent": { "checkbox": true } }],
"pages": [
{ "name": "Pricing page", "file": "out/pricing.html", "kind": "pricing", "plan": "Pro" },
{ "name": "Preview checkout", "url": "https://preview.acme.example/checkout", "kind": "checkout", "plan": "Pro" }
],
"emails": [
{ "name": "Welcome", "kind": "acknowledgment", "file": "emails/welcome.html", "plan": "Pro" },
{ "name": "Trial ending", "kind": "trial_reminder", "module": "emails/render.ts#trialEnding", "plan": "Pro" }
],
"notice_schedule": true
}
}| Key | What it is |
|---|---|
merchant | name (the brand subscribers see), cancel_url (where they cancel online), support_contact (shown in emails). What the code alone does not say. |
jurisdictions | "all" (default) or codes like ["US-CA", "US-NY"]. Federal law is always included. |
plans[] | name, price (major units) or amount (minor units), currency, interval (day, week, month, year), interval_count, trial_days; optional intro_price / intro_amount / intro_periods, cancel_timing. |
checks.stripe_checkout[] | The params your code passes to stripe.checkout.sessions.create, from one of: params (inline), file (JSON), source (a file with the call; the object literal is read statically on every run, call picks the n-th call), or module (below). plan gives the price when line items use a Price id. |
checks.disclosures[] | The renewal terms by the pay button: text or file, optional button_text, placement and consent (as in check_disclosure). |
checks.pages[] | file (built or static HTML) or url (a preview deployment; checked with --online), kind checkout or pricing. |
checks.emails[] | kind: acknowledgment, trial_reminder, renewal_reminder, price_change, annual_terms, charge_notice, cancellation_confirmation. Body from file (.html, .txt, .md; a first line Subject: … or the HTML <title> sets the subject), text / html with subject, or module. previous_price for price changes. |
checks.notice_schedule | true: for each plan, the emails the law requires (from the notice schedule) must each have a template in emails; a missing one needs review. |
Every disclosure, page and email names its plan when there are several plans. Names must be unique: they label the results. Unknown keys, a wrong kind, a missing file or a TODO value stop the run with exit 3 and a message that names the key, for example checks.emails[1].kind must be one of acknowledgment, … (got "welcome").
Code that builds the params or renders the email
"module": "path/to/file.ts#exportName" imports the file in a separate Node process and uses the export: an object, or a function called with args (sample values) that returns it — the exact Checkout Session params, or { subject, html } / { subject, text } for an email. That is how templates built in code are checked:
import { render } from '@react-email/render';
import TrialEnding from './TrialEnding';
export async function trialEnding() {
return { subject: 'Your Acme Pro trial ends in 3 days', html: await render(TrialEnding({ firstName: 'Sam', trialEnd: 'June 3' })) };
}TypeScript runs through the project's tsx when it is installed, else Node's type stripping (Node 22.6+; .tsx needs tsx). Install your dependencies first in CI (npm ci) when a module imports them. Prefer source for plain checkout.sessions.create({...}) calls: nothing is executed, and values that are expressions (a Price id from an environment variable) are reported and taken from plan.
Run it
mkdir -p stripe emails
cat > checkbox.json <<'EOF'
{
"$schema": "https://checkbox.site/schemas/checkbox.json",
"merchant": { "name": "Acme", "cancel_url": "https://acme.example/account/cancel" },
"plans": [{ "name": "Pro", "price": 19.99, "currency": "usd", "interval": "month" }],
"checks": {
"stripe_checkout": [{ "name": "Pro checkout", "file": "stripe/session-params.json" }],
"emails": [{ "name": "Welcome email", "kind": "acknowledgment", "file": "emails/welcome.txt" }],
"notice_schedule": true
}
}
EOF
cat > stripe/session-params.json <<'EOF'
{
"mode": "subscription",
"line_items": [{ "price_data": { "currency": "usd", "unit_amount": 1999, "recurring": { "interval": "month" }, "product_data": { "name": "Acme Pro" } }, "quantity": 1 }],
"success_url": "https://acme.example/welcome"
}
EOF
cat > emails/welcome.txt <<'EOF'
Subject: Welcome to Acme Pro
Thanks for subscribing to Acme Pro. Your subscription renews automatically every month at $19.99 until you cancel.
Cancel anytime online at https://acme.example/account/cancel.
EOF
checkbox ciCheckbox CI checkbox.json · offline · rules 1.4.0 · catalog 2026.11.01.1 · 3 checks
✗ Pro checkout → 4 fixes needed · 1 not checked yet
fix: Explain the cancellation policy and exactly how to cancel in the checkout terms. (stripe/session-params.json)
✓ Welcome email → Checks pass · 2 live checks pending
? Required emails → 2 required emails not in checkbox.json
fix: Add a charge notice (a receipt per charge usually serves) to checks.emails (kind "charge_notice"; 13 days
before: Renewal charge (a receipt or upcoming-charge email per charge)), or have Checkbox Notices
send it. (checkbox.json:8)
1 needs a fix · 1 needs review · 1 pass · exit 1
Corrected params / text: --verbose, --json, or the job summary in GitHub Actions.
Automated check against Checkbox's catalog of automatic-renewal laws; not legal advice.The status labels are the ones the dashboard and API use: Checks pass, N fixes needed, needs review (a question for a person, or something only a live check can confirm). On a Stripe-hosted page, legibility of the renewal terms is Stripe's styling and stays a note, so correct Stripe params pass.
| Flag | |
|---|---|
--config <path> | Another config file. |
--online | Run the checks through the API with CHECKBOX_SECRET_KEY (or CHECKBOX_API_KEY): pages given as a url (a preview deployment) are fetched and checked. Without it they are skipped. |
--strict | A check that needs review fails too (exit 2). |
--changed-only | Skip the run when no file the config reads changed against --base (in a pull request: origin/$GITHUB_BASE_REF; use actions/checkout with fetch-depth: 0). A module entry counts every code change. |
--verbose | Every fix, the warnings and the corrected params / text. |
--json | One JSON document ("object": "ci_run") with each check's status, label, fixes, corrected output and full result. |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Every check passes (needs review without --strict is reported, not failed) |
| 1 | At least one check needs a fix |
| 2 | Only needs review, with --strict |
| 3 | Config or usage error: invalid checkbox.json, a missing file, a TODO value, a module that throws |
| 4 | --online without a valid key |
| 5 | API or network error (--online) |
In the pull request
With GITHUB_ACTIONS=true (any GitHub Actions job), checkbox ci also:
- prints
::errorannotations (a fix is needed) and::warningannotations (needs review) on the file each check reads — the params JSON, the source line of thecheckout.sessions.createcall, the module, the email template — so they show in the pull request's "Files changed" tab; - appends a summary to
$GITHUB_STEP_SUMMARY: one row per check with its status and top fix, then each corrected params object or text in a collapsible block; - writes
status=pass|fail|needs_review|error|skippedto$GITHUB_OUTPUT(the Action'sstatusoutput).
The job summary for the run above:
## Checkbox compliance checks
**1 needs a fix · 1 needs review · 1 pass** — `checkbox.json` · offline · rules 1.4.0 · catalog 2026.11.01.1
| Check | Status | Fix | File |
|---|---|---|---|
| ❌ Pro checkout<br><sub>Stripe Checkout</sub> | 4 fixes needed · 1 not checked yet | Explain the cancellation policy and exactly how to cancel in the checkout terms. (+4 more) | `stripe/session-params.json` |
| ✅ Welcome email<br><sub>Email</sub> | Checks pass · 2 live checks pending | | `emails/welcome.txt` |
| ⚠️ Required emails<br><sub>Notice schedule</sub> | 2 required emails not in checkbox.json | Add a charge notice (a receipt per charge usually serves) to checks.emails (kind "charge_notice"; …), or have Checkbox Notices send it. (+1 more) | `checkbox.json:8` |
<details><summary><b>Pro checkout</b>: corrected params (verified by the rules engine)</summary>
"consent_collection": { "terms_of_service": "required" },
"custom_text": {
"submit": { "message": "$19.99 every month (plus applicable taxes). Renews automatically until you cancel. Cancel anytime online at https://acme.example/account/cancel before your next billing date to avoid the next charge." },
"terms_of_service_acceptance": { "message": "I agree that my subscription renews automatically at $19.99 every month until I cancel." }
}
</details>The Action
uses: checkbox-site/checkbox@v1 sets up Node and runs npx @checkbox-site/cli@<version> ci.
| Input | Default | |
|---|---|---|
config | checkbox.json | Path to the config, relative to working-directory. |
strict | false | true: needs review fails the job. |
working-directory | . | The project root (monorepos: the app's folder). |
args | More flags, e.g. --online, --changed-only. | |
version | the Action's release | The @checkbox-site/cli version to run. |
node-version | 22 | Node 22+ runs TypeScript modules without tsx. |
Output status: pass, fail, needs_review, error or skipped. Other CI systems run the same command: npx @checkbox-site/cli ci (exit codes above).
Offline and online
Offline (the default) runs the same rules engine and legal catalog as the API, in the job; results carry the same rules_version and catalog_version. --online sends each check to /v1/checks/* with a key: use it for preview URLs, or when you want the checks recorded against your account. Give CI a restricted key with only checks: read as the repository secret CHECKBOX_SECRET_KEY.
With an AI coding agent
After an agent fixes a checkout with Checkbox (the Claude skill, the MCP server), it offers once to add these checks to CI: it writes checkbox.json pointing at the files it just fixed and the workflow above, runs checkbox ci to confirm it passes, and leaves the commit to you.
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.