---
name: bt-reactors
description: Break out to Reactors when Proxy cannot express plaintext logic.
license: MIT
metadata:
  tier: standard
  last_verified: '2026-09-25'
---

# Reactors: code in the boundary

## When a reactor is the right primitive

Fact: Reactors run your code inside Basis Theory's environment with access to detokenized data, invoked synchronously or asynchronously (https://developers.basistheory.com/docs/concepts/what-are-reactors, https://developers.basistheory.com/docs/api/reactors/).

Implementation pattern — the decision rubric (full version in `bt-architecture`):

- **Proxy default** when the job is secure synchronous forwarding, detokenization, request/response transforms, or preserving an existing HTTP call shape. Use it in as many cases as Proxy/transforms can safely express.
- **Reactor breakout** only when Proxy/transforms cannot safely express programmable plaintext logic: nontrivial branching, multi-step orchestration, side effects, async workflows, or formats no transform should carry.
- **Neither** when the logic never needs plaintext — run it in your own app.

Smell test for checkout paths: if the design depends on a webhook + reactor + polling chain becoming "fast enough" for a synchronous checkout, the problem is primitive choice, not tuning. That makes it an architecture question before it is a reactor question — open `bt-architecture` for the full rubric rather than answering from this skill alone, then move the critical path to a synchronous primitive: the proxy when the job is forwarding, a synchronous reactor invocation when plaintext logic is genuinely required.

## Mechanics

- Create/manage reactors: https://developers.basistheory.com/docs/api/reactors/
- Invoke synchronously: https://developers.basistheory.com/docs/api/reactors/invoke-reactors
- Invoke async: https://developers.basistheory.com/docs/api/reactors/invoke-async-reactors
- **Async completion**: do not send legacy `callback_url` on either invocation path; `/reactors/:id/react` now returns `400` for it. Use `/reactors/:id/react-async`, subscribe to Reactor webhook events, then retrieve the result by request ID (https://developers.basistheory.com/docs/api/deprecations#async-reactor-callback_url-deprecation; https://developers.basistheory.com/docs/api/reactors/invoke-async-reactors).
- Error semantics and error types: https://developers.basistheory.com/docs/api/reactors/reactor-errors, https://developers.basistheory.com/docs/concepts/reactors/error-handling
- Runtimes: https://developers.basistheory.com/docs/concepts/runtimes/ — Node.js images `node22` and `node24` are current (https://developers.basistheory.com/docs/concepts/runtimes/nodejs); `node-bt` is deprecated and, from the date on https://developers.basistheory.com/docs/api/deprecations, cannot be selected for new Reactors or Proxy code transforms (https://developers.basistheory.com/docs/concepts/runtimes/node-bt, migration: https://developers.basistheory.com/docs/concepts/runtimes/migrate-from-node-bt). Pick a Node.js image explicitly for new work. Dependency vulnerability scanning applies (https://developers.basistheory.com/docs/concepts/runtimes/vulnerability-scanning).
- Runtime permissions and reveal: Node.js images take `runtime.permissions` (`token:create`, `token:read`, and `token:read` + `token:reveal` for plaintext); `token:reveal` is only grantable to runtimes, not to Applications (https://developers.basistheory.com/docs/concepts/runtimes/nodejs#runtime-permissions). A `node-bt` reactor's root-container reveal rules can migrate to runtime permissions only if the complete rule set permits tenant-wide plaintext; container-scoped rules must retain a Private Application key in encrypted `configuration` instead (https://developers.basistheory.com/docs/concepts/runtimes/migrate-from-node-bt).
- Runtime logs (Node.js images): set `runtime.logs.enabled: true` with `level` `debug` | `info` | `warn` | `error` on a Reactor or on each Proxy code transform. Write records with `event.logger.*`, not `console.*`, which is not captured. Records are sanitized (`[REDACTED]`), may truncate large strings with `…[truncated; original_bytes=N]` or omit oversized attribute keys, and are fail-open; do not infer completeness from a log record. Delivery uses `reactor.log` and `proxy.log` webhook events; changing settings reprovisions asynchronously (https://developers.basistheory.com/docs/concepts/runtimes/runtime-logs, event shapes: https://developers.basistheory.com/docs/api/webhooks/eventdata#runtime-logs). The CLI's live log stream is a separate, ad hoc surface (https://developers.basistheory.com/docs/sdks/cli/reactors#reactor-logs).
- Merchant context: the reactor request exposes only allowlisted inbound headers (`bt-merchant-id`, `bt-trace-id`) under `headers`; they are never auto-forwarded on Basis Theory calls the reactor makes, so pass `BT-MERCHANT-ID` explicitly or the call runs at tenant level (https://developers.basistheory.com/docs/concepts/reactors/#forwarding-bt-merchant-id).
- Concepts index: https://developers.basistheory.com/docs/concepts/reactors/

## Existing-code mode

1. Prove Proxy is insufficient before writing any reactor code. If the work is forwarding plus transforms, use Proxy.
2. Keep reactor code small and single-purpose; heavy business logic belongs in the customer's app, with the reactor doing only the part that requires plaintext.
3. Pin a current Node.js image deliberately and treat dependencies as attack/maintenance surface — the runtime scans for vulnerabilities; fewer deps, fewer rejections. Do not scaffold new `node-bt` code.
4. Handle every error class the docs define: reactor-thrown errors, invocation errors, timeouts. Surface reactor failures to the caller with enough context to debug without Basis Theory internals. Enable runtime logs at `info` or `warn` and log through `event.logger` with bounded, non-sensitive fields; the platform's `execution.started` / `execution.completed` lifecycle records carry the outcome (`success`, `timeout`, `out_of_memory`, `application_error`, `platform_error`).
5. Test with sandbox tokens in a test tenant; assert both the success payload and each mapped failure class.
6. If the reactor makes outbound calls, note the egress requirements explicitly and move the design back to Proxy if forwarding turns out to be the whole job.

Observability caveat (implementation pattern, worth stating to users): reactors are customer code — when they fail, the debugging surface is smaller than the proxy's request/response boundary. Runtime logs narrow that gap but are opt-in, webhook-delivered, and dropped rather than blocking when capture fails, so reactor-heavy architectures still shift operational burden onto the customer's own log pipeline. Design for that: structured `event.logger` records, deterministic errors, minimal state, and a webhook consumer that dedupes by `event.id` and orders by `invocation_id` + `sequence`.

## Basis Theory implementation guidance

When the answer needs implementation judgment beyond public docs, load only the relevant slices below from [../\_shared/references/implementation-guidance/README.md](../_shared/references/implementation-guidance/README.md). Treat these as implementation patterns, not canonical product behavior; keep public docs as the source of truth and label the guidance as **Implementation pattern** or **Assumption** unless docs prove it.

- `../_shared/references/implementation-guidance/reactors/`
- `../_shared/references/implementation-guidance/debugging-patterns.md`

<!-- bt-conventions:start -->

## Working conventions (shared by all Basis Theory skills)

- Public Basis Theory docs are the source of truth for product behavior. Link the specific docs page behind any product claim; if a skill and the docs disagree, follow the docs.
- Keep customer ownership clear: customers own PSP accounts, credentials, compliance decisions, production rollout, and application code. Skills can guide plans, bounded changes, and examples; they do not certify production readiness.
- Never request, echo, store, or commit real credentials. Use placeholders such as `<BT_API_KEY>` and prefer sandbox or synthetic data for examples.
- If a live-looking credential, live cardholder data, or production secret appears, stop mutation work, tell the user exactly where it appeared, and require rotation/revocation before continuing.
- Default to test tenants and PSP sandboxes. Do not run or recommend production charges, refunds, card reveals, credential migrations, or data moves unless the user explicitly asks and the plan names safeguards, rollback, and customer approval.
- Treat vague planning, audit, review, and “should we” prompts as read-only unless the user explicitly asks for a bounded implementation change.
- Before changing product code, map the existing checkout/data flow, credential-bearing paths, persistence, retries, refunds, webhooks, logs, and rollback expectations.
- End with an honest final report: done, assumed, remaining, proof run, unverified live behavior, rollback, and go-live items.
<!-- bt-conventions:end -->
