Never call a third-party API directly from a controller or a model. Every external integration in
One Shot, billing, email, bot checking, App Store and Play Store receipt verification, is wrapped
in the same seam: a real client when credentials are present, and a deterministic fake otherwise.
This is the single mechanism that lets a fresh clone of the kit run its entire feature set with
zero external accounts, and it's the pattern to copy when you add a new one. The fastest way is the
**`add-integration`** skill in Claude Code.

## The reference implementations

`app/adapters/billing.rb` is the full example (with `billing/local.rb` and `billing/stripe.rb`
alongside it). `app/adapters/bot_check.rb` is the simpler one, worth reading first if you want the
shape without the extra complexity billing has.

## Building a new adapter

1. **Create `app/adapters/<service>.rb`** as a module with an entry point, typically `.for(...)` or
   `.client`, that returns the real client when the service's credential environment variable is
   present, and a fake otherwise. Resolve credentials `ENV` first, then
   `Rails.application.credentials.dig(...)` as a fallback.
2. **Put the real client and the fake in `app/adapters/<service>/`.** The real client talks to the
   actual API, using `Net::HTTP` or the vendor's gem. The fake returns sensible canned values with
   no network call at all, and it has to be complete enough that the whole feature works against
   it, not just enough to avoid raising.
3. **Honor a force-fake override**, following the pattern in `Billing#stripe?`, so demos and CI can
   run entirely on fakes even when real credentials happen to be configured.
4. **Fail sensibly, on purpose.** Rescue and report errors through `ErrorReporter.report(e,
   context: {...})`, and decide deliberately whether this integration should fail open or fail
   closed. `BotCheck#verify` fails closed: if the check can't run, it treats the request as
   suspicious rather than waving it through.
5. **Document the credential** in [Bring Your Own Keys](/docs/bring-your-own-keys/optional-integrations)
   and in `.env.example`, so the next person setting this up knows what to provide.

## If the integration sends the user off-site

A hosted checkout, an OAuth consent screen, a hosted KYC flow: anything that redirects the browser
to another domain needs two more changes, and both fail silently if you miss them.

**Content Security Policy.** The CSP is enforced, and `form-action` covers the entire navigation a
form starts, including the redirect a POST triggers. Add the provider's host to `policy.form_action`
in `config/initializers/content_security_policy.rb`, and pin it in
`spec/requests/content_security_policy_spec.rb`. Miss this and the button does nothing: no
exception, no flash, nothing in the network tab, just a link that appears dead. See
[SEO and Structured Data](/docs/content-seo/seo-and-structured-data) for the CSP's other
implications on this site.

**`data-turbo="false"`.** A form whose action redirects off-site needs this attribute, on the form
or the submit button (Turbo checks both). Without it, Turbo submits the form via `fetch` and tries
to follow the cross-origin redirect as an XHR, which `connect-src` blocks: the same dead-looking
button, a different cause. `spec/requests/cross_origin_forms_spec.rb` is where this is pinned.

## Testing

Test both paths: the fake path, which should work with no credentials configured at all, and the
enabled-but-invalid path, where credentials are present but the service rejects them.
`spec/adapters/bot_check_spec.rb` is the reference spec.

## Verify

`bin/check` should be green.

## Next

If the new integration involves money, read
[How Billing Works](/docs/billing-entitlements/how-billing-works) for how the billing adapter
specifically handles the real-versus-fake split.
