Sandbox vs Production Routing
Decision
Sandbox/production endpoint routing is determined per-request by the API key prefix, following the Stripe model:
fsk_test_*→ sandbox endpoints (test tax authority environments)fsk_live_*→ production endpoints (real tax authority submissions)
This is the sole determinant. There is no location config field, no environment variable, and no per-deployment toggle for sandbox routing.
Architecture
API Key (fsk_test_ / fsk_live_)
↓
Auth Middleware → sets "environment" = "test" | "live" in context
↓
Transaction Handler → stores environment on the transaction row
↓ (guards: rejects live-mode if FISCALIZATION_LIVE_ENABLED=false)
↓
FiscalizerBridge → maps environment to tx.TestMode bool
↓
Country Adapter → uses tx.TestMode to pick sandbox/production endpoint
↓
RoutingClient → caches HTTP clients per endpoint for TLS reuse
Environment Guard
The FISCALIZATION_LIVE_ENABLED environment variable prevents accidental
production submissions from non-production deployments:
| Deployment | FISCALIZATION_LIVE_ENABLED | Test keys | Live keys |
|---|---|---|---|
| dev | false (default) | ✅ sandbox | ❌ rejected (403) |
| staging | false | ✅ sandbox | ❌ rejected (403) |
| production | true | ✅ sandbox | ✅ production |
Live keys in production reach real tax authorities. Test keys in production still route to sandbox — this lets merchants test their integration in the production environment without submitting real invoices.
Database
The transactions table stores environment VARCHAR(10) NOT NULL DEFAULT 'test'
with a CHECK constraint (environment IN ('test', 'live')). This is immutable
after creation — a transaction's test/live status is set at creation time and
never changes.
Cancellation transactions inherit the environment from the original transaction.
Country Adapter Contract
Every country adapter receives tx.TestMode on the adapters.Transaction
struct. The adapter MUST:
- Use
tx.TestModeto determine sandbox vs production endpoint routing - Never read a "sandbox" field from the location's
country_config - Use a
RoutingClientpattern that picks the endpoint per-request - Cache HTTP clients per endpoint for connection/TLS reuse
Spain: TicketBAI
Uses ticketbai.RoutingClient → resolves endpoint per territory + sandbox:
- Gipuzkoa:
egoitza.gipuzkoa.eus(prod) /tbai.prep.gipuzkoa.eus(sandbox) - Araba:
web.araba.eus(prod) /pruebas-ticketbai.araba.eus(sandbox) - Bizkaia: LROE batch (no real-time endpoint)
Spain: Verifactu
Uses verifactu.RoutingClient → picks AEAT SOAP endpoint:
- Production:
www1.agenciatributaria.gob.es - Sandbox:
prewww1.aeat.es
Mutual TLS configured via VERIFACTU_CERT_ID — same cert works for both endpoints.
Adding a New Country
When implementing a new country adapter:
- Accept
tx.TestMode— do NOT add sandbox config to location - Implement a
RoutingClientthat picks endpoint per-request - Define both sandbox and production endpoint constants
- Document the endpoints in this file