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:

  • zyn_test_* → sandbox endpoints (test tax authority environments)
  • zyn_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 (zyn_test_ / zyn_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