Sandbox Modes
Test mode (zyn_test_* keys) supports two routing modes for fiscalization. Emulator is the default — Zyntem synthesises a stubbed authority response so you can exercise the full request shape without uploading a certificate or making a network call to the regulator. Authority routes to the real country sandbox endpoint and requires a sandbox cert on file.
You select the mode per request with the X-Zyntem-Sandbox header. Both modes are test-key only — live keys reject either header with 400 Bad Request.
Emulator vs authority
| Mode | Header | Cert required | Network round-trip | Use it for |
|---|---|---|---|---|
| Emulator (default) | X-Zyntem-Sandbox: emulator (or omit) | No | No — fully synthetic | Wiring up requests, integration tests, error-path coverage |
| Authority | X-Zyntem-Sandbox: authority | Yes — POST /v1/certificates first | Yes — real sandbox endpoint | End-to-end conformance against the regulator's sandbox |
The resolved mode is echoed back to you in two places:
- The
X-Zyntem-Sandboxresponse header on every test-key request. - The
sandbox_modefield on the transaction response body ("emulator","authority", ornullfor live keys).
This is a one-bit difference at request time but a large one operationally — emulator mode lets ISVs onboard, run their CI suite, and drive every error path without ever talking to the tax authority. Switch to authority mode only when you specifically need to verify the regulator's own sandbox accepts your payloads.
Choosing a mode
Default to emulator. It's the right choice for ~90% of test-mode traffic:
- New integrations: get a
201 Createdfor a transaction before you've even opened the cert-onboarding portal. - CI / automated tests: deterministic, network-free, no shared sandbox state across builds.
- Error-path testing: pair with
X-Zyntem-Test-Scenarioto drive every fault class on demand (see below). - Demos and onboarding flows: show end-to-end behaviour without provisioning a regulator account.
Opt into authority when you need ground-truth from the regulator:
- Pre-launch conformance checks before flipping a country to
zyn_live_. - Investigating a discrepancy between Zyntem's emulator and the regulator's actual response shape.
- Reproducing an issue that was only seen in production (the authority sandbox is the closest available proxy).
Authority mode requires that the location has a sandbox certificate on file. If it doesn't, the 400 response includes a discovery hint pointing back at emulator mode plus POST /v1/certificates:
Location "Madrid HQ" requires certificate to reach the ES sandbox. To skip
the round-trip with a stubbed response, omit the X-Zyntem-Sandbox header
(defaults to emulator) or set it to emulator. To upload a sandbox cert,
POST /v1/certificates.
Triggering specific errors with X-Zyntem-Test-Scenario
The emulator's default outcome is success. To exercise an error path, send X-Zyntem-Test-Scenario with a value from the catalog below. Each scenario maps to a generic error envelope that the country adapter translates into its native shape (TBAI fault codes, SDI error tuples, KSeF processingCode, etc.) — your code sees the same stable scenario name across every country.
| Scenario | HTTP | Retry-After | What it represents |
|---|---|---|---|
success (default) | 200 | — | Happy path. Synthetic authority acceptance. |
cert_revoked | 400 | — | Signing certificate revoked by the issuing CA. |
cert_expired | 400 | — | Signing certificate past its notAfter. |
throttled | 429 | 60s | Authority returned 429 Too Many Requests. |
malformed_response | 502 | — | Authority returned a payload that failed to parse. |
timeout | 504 | 5s | Authority did not respond before the runtime timeout. |
invalid_tax_id | 400 | — | Tax id failed authority-side format / registry validation. |
network_down | 502 | 30s | DNS / TCP connection to the authority could not be established. |
authority_unavailable | 503 | 120s | Authority is in maintenance / outage. |
duplicate_transaction | 409 | — | Indistinguishable transaction already accepted by the authority. |
Examples
Verify your client retries on a 429:
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-H "Content-Type: application/json" \
-H "X-Zyntem-Test-Scenario: throttled" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{ ... }'
The response carries Retry-After: 60 and an X-Zyntem-Sandbox: emulator echo header; the underlying error envelope arrives in the country adapter's native shape.
Drive a cert-expiry alert:
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-H "X-Zyntem-Test-Scenario: cert_expired" \
-d '{ ... }'
Confirm idempotent replay handling:
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-H "X-Zyntem-Test-Scenario: duplicate_transaction" \
-d '{ ... }'
Combine with authority mode — the scenario header still picks the emulator's outcome shape, but the regulator round-trip (and cert validation) happens first:
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-H "X-Zyntem-Sandbox: authority" \
-H "X-Zyntem-Test-Scenario: timeout" \
-d '{ ... }'
Unknown scenario values are rejected with 400 Bad Request and the response body lists every accepted value, so you can self-correct without consulting docs.
What's deterministic vs synthetic
Emulator responses are deterministic and idempotent: the same Idempotency-Key plus the same scenario header always yields the same envelope. Fiscal IDs and timestamps are synthesised but stable for the lifetime of the transaction record, so you can assert against them in tests.
What you do not get from the emulator: a real regulator-signed receipt, a working QR code that resolves on the regulator's verifier portal, or coverage of regulator-specific quirks that aren't in the catalog. For any of those, switch to authority mode.
Going to live mode
When you're ready to flip a country to production, treat the test → live transition as a discrete promotion step rather than a key swap.
Pre-promotion checklist:
- Acceptance in emulator — every transaction shape your application emits (sales, refunds, multi-line, multi-tax) returns
201 Createdwithsandbox_mode: "emulator". Run this against your CI suite. - Error-path coverage — your client gracefully handles every catalog scenario above. At minimum, verify
throttled(retries),authority_unavailable(back-off),cert_expired(alerts your ops team), andduplicate_transaction(does not double-charge). - Sandbox cert uploaded —
POST /v1/certificatessucceeded for each location that will go live. The cert must be the regulator's sandbox cert, not the production one yet. - Authority-mode dry run — repeat step 1 with
X-Zyntem-Sandbox: authority. Compare the transaction body against the emulator response — fiscal IDs and timestamps will differ, but the shape and status should match. - Production cert uploaded — repeat the cert upload with the live cert. The cert is bound to the location, not the API key, so this is a per-location action.
- Switch the API key — replace
zyn_test_*withzyn_live_*in your production deployment. Do not sendX-Zyntem-SandboxorX-Zyntem-Test-Scenarioon live keys; both will return400 Bad Request. - Watch the
environmentfield — the first live transactions should report"environment": "live"and"sandbox_mode": null. Alert on any drift.
See the Authentication guide for key management and Sandbox routing for how the test/live key split interacts with server-side FISCALIZATION_LIVE_ENABLED.
Live keys: no test-mode headers
Live keys (zyn_live_*) reject either header with 400 Bad Request:
{
"error": "Test-mode headers only valid on test API keys"
}
The sandbox_mode field on the transaction response is null and the X-Zyntem-Sandbox response header is absent for any live-key request — there's nothing to echo because there's no mode to resolve.
This is enforced server-side; you cannot accidentally route live traffic through the emulator by adding the header.