Skip to main content

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:

DeploymentFISCALIZATION_LIVE_ENABLEDTest keysLive keys
devfalse (default)✅ sandbox❌ rejected (403)
stagingfalse✅ sandbox❌ rejected (403)
productiontrue✅ 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:

  1. Use tx.TestMode to determine sandbox vs production endpoint routing
  2. Never read a "sandbox" field from the location's country_config
  3. Use a RoutingClient pattern that picks the endpoint per-request
  4. 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:

  1. Accept tx.TestMode — do NOT add sandbox config to location
  2. Implement a RoutingClient that picks endpoint per-request
  3. Define both sandbox and production endpoint constants
  4. Document the endpoints in this file