# Preflight checks

> Stateless "check before you ship" calls for AI agents and CI — disclosure text, Stripe params, lifecycle emails, checkout HTML and notice timing — with verdicts per legal requirement and the exact fix.

Preflight checks judge what your code will produce, before anything is deployed. Nothing is stored, no site is needed,
and test keys work. Send the text, params, email or HTML; get a verdict per check and per legal requirement, with
citations, and a fix the engine has already verified.

| Check | Endpoint | CLI | Offline |
|---|---|---|---|
| Disclosure text, placement, consent checkbox | [`POST /v1/checks/disclosure`](https://checkbox.site/docs/api/checks#checkDisclosure) | `checkbox check disclosure` | yes |
| Stripe Checkout Session params | [`POST /v1/checks/stripe_checkout_session`](https://checkbox.site/docs/api/checks#checkStripeCheckoutSession) | `checkbox check stripe-session` | yes |
| Stripe Payment Link params | [`POST /v1/checks/stripe_payment_link`](https://checkbox.site/docs/api/checks#checkStripePaymentLink) | `checkbox check payment-link` | yes |
| A lifecycle email | [`POST /v1/checks/email`](https://checkbox.site/docs/api/checks#checkEmail) | `checkbox check email` | yes |
| Checkout HTML (or a live URL) | [`POST /v1/checks/page`](https://checkbox.site/docs/api/checks#checkPage) | `checkbox check page` | `--file` only |
| When each notice must be sent | [`POST /v1/checks/notice_schedule`](https://checkbox.site/docs/api/checks#checkNoticeSchedule) | `checkbox check notice-schedule` | no |

`--offline` runs the same rules engine locally: no key, no network, the same JSON plus `"mode": "offline"`.

## Example {#example}

```bash
checkbox check disclosure \
  --text "Renews monthly at \$19.99 until you cancel." \
  --merchant-name Acme --price 19.99 --interval month \
  --consent-checkbox --offline
```

The same call over HTTP returns JSON like this (trimmed):

```json
{
  "object": "check_result",
  "kind": "disclosure",
  "verdict": "fail",
  "checks": [
    {
      "check_id": "signup.disclosure.cancellation_policy",
      "status": "fail",
      "severity": "critical",
      "fix": "Explain the cancellation policy and exactly how to cancel in the checkout terms.",
      "requirements": [
        { "id": "US-FED.8403.1", "jurisdiction": "US-FED", "citation": "15 U.S.C. § 8403(1)" },
        { "id": "US-CA.17602.a.1", "jurisdiction": "US-CA", "citation": "Cal. Bus. & Prof. Code § 17602(a)(1); § 17601(a)(2)-(3)" }
      ]
    }
  ],
  "replacement": {
    "disclosure_text": "Your Acme subscription automatically renews every month at $19.99 (plus applicable taxes) until you cancel. You can cancel anytime online at {{cancel_url}}; cancellation takes effect at the end of your current billing period. To avoid the next charge, cancel before your next billing date (every month).",
    "placeholders": ["{{cancel_url}}"],
    "verified": true
  },
  "rules_version": "1.4.0",
  "catalog_version": "2026.11.01.1",
  "disclaimer": "Automated compliance check against Checkbox’s catalog of automatic-renewal laws. It is not legal advice…",
  "next_action": {
    "type": "replace_text",
    "message": "Failing: signup.disclosure.cancellation_policy (fail), signup.disclosure.conspicuous (needs_review). Replace the disclosure with replacement.disclosure_text, show it directly above the purchase button… then call this check again."
  }
}
```

`next_action.type` is one of `none`, `replace_text`, `apply_patch`, `replace_template`, `fix_and_recheck`,
`human_review`, `provide_input`, `schedule_notices`. Loop until the verdict is `pass`.

`replacement` carries the same verified markup twice: `html`, and `jsx` for React / Next.js components (`className`,
style objects, self-closed inputs). For emails, keep the template's lines and add your own greeting, sign-off and merge
tags around them; date variables (`{{endDate}}`, `${trialEndDate}`) are accepted as written. When `next_action.type` is
`fix_and_recheck` because Checkbox cannot see what one of your merge tags holds, keep the tag in your code and check the
email again with a sample value in its place.

## In CI {#ci}

The `checkbox` CLI is not on npm yet, so CI calls the HTTP API directly (round-3 pilot: the previous `npx` step could
not run). The step fails the build when the verdict is `fail` (add `needs_review` to the `case` to make it strict):

```yaml title=".github/workflows/compliance.yml (excerpt)"
- name: Checkbox preflight
  env:
    CHECKBOX_API_KEY: ${{ secrets.CHECKBOX_API_KEY }}
  run: |
    jq -n --slurpfile p checkout/session-params.json \
      '{params: $p[0], merchant: {merchant_name: "Acme", cancel_url: "https://acme.example/account/cancel"}}' \
      | curl -sS -X POST https://app.checkbox.site/v1/checks/stripe_checkout_session \
          -H "Authorization: Bearer $CHECKBOX_API_KEY" -H "Content-Type: application/json" --data @- > preflight.json
    verdict=$(jq -r .verdict preflight.json); echo "Checkbox verdict: $verdict"
    case "$verdict" in fail) jq '.checks[] | select(.status != "pass")' preflight.json; exit 1;; esac
```

Give CI a restricted key with only `checks: read`. Once the CLI is published, `npx @checkbox-site/cli check stripe-session …` does
the same with stable exit codes (`0` pass, `1` fail, `2` needs review with `--strict`).

To check every file on each pull request, list them once in `checkbox.json` and run `checkbox ci` (the GitHub Action
`checkbox-site/checkbox@v1`): offline, no key, annotations on the files that need a fix and the corrected params in
the job summary. See [Compliance checks on every pull request](https://checkbox.site/docs/guides/ci).

## Determinism and limits {#limits}

- Every result carries `rules_version`, `catalog_version`, `catalog_hash`, `as_of` and a disclaimer. The same input and
  `as_of` date give the same verdicts.
- On any Stripe-hosted page, *clear and conspicuous* (`signup.disclosure.conspicuous`) stays `needs_review`: Stripe
  controls the styling.
- The page check reads static HTML; buttons rendered by JavaScript are not found, so send the rendered HTML. Layout is
  estimated, and a passing estimate is reported as `needs_review`, never `pass`; a scan confirms it.
- Disclosure and Stripe checks model a desktop page; mobile layouts are judged by scans.
- Email checks judge content; real send timing is judged by monitoring.
- Rate limit: 120 checks per minute per account (`page` with `url`: 20 per minute).
