Going to Production
This guide covers the steps to move from sandbox testing to live fiscalization for both the Cloud API and Embedded Local SDKs.
Pre-launch checklist
1. Swap test keys for live keys
Replace your zyn_test_ API key with a zyn_live_ key. The key prefix determines whether transactions are routed to sandbox or production tax authority endpoints.
| Key prefix | Routes to |
|---|---|
zyn_test_* | Sandbox tax authority endpoints |
zyn_live_* | Production tax authority endpoints |
Cloud API: Change the key in your Authorization header:
# Before (sandbox)
curl -H "Authorization: Bearer zyn_test_abc123..."
# After (production)
curl -H "Authorization: Bearer zyn_live_abc123..."
Embedded SDK: Update your configuration to use the live key and set the environment to "production":
# fiscalization.yaml
environment: production
api_key: zyn_live_abc123
live_mode: true
Or override via environment variables without changing the config file:
export ENVIRONMENT=production
export FISCALIZATION_LIVE_ENABLED=true
export ZYNTEM_API_KEY=zyn_live_abc123
The SDK validates that the key prefix matches the environment at init time. A zyn_test_ key with "production" environment (or vice versa) is rejected with an init error.
2. Enable live mode on the server (Cloud API)
The Cloud API requires FISCALIZATION_LIVE_ENABLED=true on the server for live keys to work. Without it, requests with zyn_live_ keys return 403 Forbidden:
{
"error": "live mode is not enabled"
}
The live_mode field in the configuration defaults to false. Set it explicitly:
# fiscalization.yaml (server config)
live_mode: true
Or via environment variable:
export FISCALIZATION_LIVE_ENABLED=true
3. Verify token provisioning (Embedded SDK)
The Embedded SDK exchanges your API key for a signed token during init(). This is the only network call required at startup. Verify that token provisioning succeeds in your production environment:
- Ensure the device can reach
api.zyntem.comover HTTPS on first boot - After the first successful init, the token is cached locally -- subsequent restarts reuse it without a network call
- If provisioning fails, the SDK returns an error and is not usable until init succeeds
Common provisioning errors:
| Error | Cause | Action |
|---|---|---|
HTTP 401 | Invalid or revoked API key | Check key is correct and active |
HTTP 403 | Key lacks permission for this country | Verify key permissions |
connection refused | Can't reach api.zyntem.com | Check network/firewall rules |
4. Upload signing certificates
Several countries require signing certificates for transaction submission. Upload them before going live.
Spain (ES): PKCS#12 certificates for Verifactu, SII, and/or LROE submission. Upload via the certificates API and reference by cert_id in your config:
spain:
verifactu:
cert_id: my-verifactu-cert
sii:
cert_id: my-sii-cert
lroe:
cert_id: my-lroe-cert
Spain's Verifactu and LROE clients use mutual TLS (mTLS) with PEM-encoded certificates when submitting to production endpoints. Sandbox endpoints do not require mTLS.
Portugal (PT): Portugal uses local document signing (ATCUD + QR codes) rather than per-transaction online submission. Ensure your software_certificate_number is set in the location's country_config, and optionally set at_certificate_id if you have an AT-issued certificate:
{
"country_config": {
"software_certificate_number": "1234",
"at_certificate_id": "my-at-cert",
"series": {
"FT": { "prefix": "FT 2026/", "atcud_code": "ABCD1234" }
}
}
}
Italy (IT): Ensure the italy.sandbox config is set to false (or removed) for production. The default is true (sandbox mode):
italy:
sandbox: false
France (FR): France uses NF525-compliant local signing. Ensure your signing key is configured in the certificates directory (certs.dir). No per-transaction submission is required.
5. Configure certificate storage
Certificates can be stored on the filesystem or in GCP Secret Manager:
certs:
backend: filesystem # or "secretmanager"
dir: /data/certs # for filesystem backend
password: "" # PKCS#12 password (if encrypted)
secretmanager:
project: my-gcp-project # for secretmanager backend
Set the password via environment variable to avoid storing it in config files:
export FISCALIZATION_CERTS_PASSWORD=your-cert-password
Country-specific submission windows
Each country has a different deadline for submitting transactions to the tax authority. The Embedded SDK queues transactions locally and submits them asynchronously within these windows:
| Country | Submission window | Notes |
|---|---|---|
| Spain (ES) | 4 days | TicketBAI/Verifactu and SII must submit within 4 days |
| Italy (IT) | 12 days | SDI submission within 12 days |
| Portugal (PT) | None (monthly SAF-T) | No per-transaction submission; monthly SAF-T batch file instead |
| France (FR) | None | No submission required; NF525 compliance is purely local signing |
For countries with a submission window (Spain, Italy), the Embedded SDK's local queue tracks deadlines per entry and issues alerts as deadlines approach.
Monitoring the submission queue (Embedded SDK)
The Embedded SDK stores transactions in a local SQLite-backed queue and submits them asynchronously via the forward_worker. Monitor queue health using fiscal_queue_status():
{
"pending": 3,
"processing": 1,
"completed": 142,
"failed": 0,
"dead": 0,
"total": 146,
"alerts": []
}
Queue entry lifecycle
pending → processing → completed
↘ failed (retry with exponential backoff)
↘ dead (after 20 attempts)
Key details from the implementation:
- Max attempts: 20 per entry (configurable via
submission.retry_maxfor the engine, ormax_attemptsper entry in the Cloud API retry queue which caps at 50) - Backoff: Exponential, starting at 2 seconds and doubling each attempt, capped at 6 hours (21,600 seconds)
- Dead entries: After exhausting all retry attempts, an entry moves to
deadstatus and stops retrying
Deadline alerts
The queue monitors submission deadlines and raises alerts at three thresholds:
| Alert level | Threshold | Meaning |
|---|---|---|
warning_75pct | 75% of window elapsed | Submission deadline approaching |
critical_90pct | 90% of window elapsed | Deadline imminent |
breached_100pct | 100% of window elapsed | Deadline passed -- regulatory risk |
Alerts appear in the alerts array of the queue_status response. Each alert includes the transaction_id, country, deadline, and pct_elapsed.
What to watch for
deadcount increasing: Entries that exhausted all retries. Investigate thelast_erroron each dead entry -- common causes are certificate expiry, network issues, or tax authority outages.pendingcount growing: The queue is not draining. Ensure the forward worker is running (it must be called periodically, e.g., on a timer or after network connectivity is restored on mobile).- Deadline alerts: Act on
critical_90pctandbreached_100pctalerts immediately.
Setting up webhooks for dead letter alerts
Configure a webhook to receive notifications when a transaction permanently fails (moves to dead status):
curl -X POST https://api.zyntem.dev/v1/webhooks \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_live_abc123..." \
-d '{
"url": "https://example.com/webhooks/zyntem",
"events": ["fiscalization.completed", "fiscalization.dead_letter"],
"description": "Production alerts"
}'
The fiscalization.dead_letter event fires when a transaction exceeds the maximum retry attempts. The payload includes the transaction_id, last_error, and attempt count.
Verify webhook signatures using the whsec_* secret returned on creation. See the Webhooks guide for signature verification details and retry policy.
Set up dead letter alerts before going live. A dead entry means a transaction was signed locally but never reached the tax authority -- this is a compliance risk that requires manual intervention.
Production configuration reference
Key environment variables for production deployments:
| Variable | Purpose | Example |
|---|---|---|
ENVIRONMENT | Sets environment name | production |
FISCALIZATION_LIVE_ENABLED | Enables live mode (required for zyn_live_ keys) | true |
FISCALIZATION_CERTS_DIR | Certificate directory path | /data/certs |
FISCALIZATION_CERTS_PASSWORD | PKCS#12 certificate password | (secret) |
FISCALIZATION_STORAGE_BACKEND | Storage backend (sqlite or postgres) | postgres |
DATABASE_URL | PostgreSQL connection URL (Cloud API) | postgres://... |
FISCALIZATION_LOG_LEVEL | Log verbosity | info |
FISCALIZATION_SUBMISSION_AUTO | Auto-submit prepared records | true |
FISCALIZATION_SUBMISSION_RETRY_MAX | Max retry attempts (engine-level) | 3 |
FISCALIZATION_SUBMISSION_RETRY_DELAY | Seconds between retries (engine-level) | 60 |
Embedded SDK config (YAML)
environment: production
live_mode: true
storage:
backend: sqlite
sqlite_path: /data/fiscalization.db
certs:
dir: /data/certs
submission:
auto_submit: true
retry_max: 3
retry_delay_secs: 60
logging:
level: info
Cloud API server config
mode: hosted
environment: production
live_mode: true
server:
port: "8080"
storage:
backend: postgres
postgres:
url: postgres://fiscalization:password@db:5432/fiscalization
receipts:
backend: gcs
gcs:
bucket: my-receipts-bucket
certs:
backend: secretmanager
secretmanager:
project: my-gcp-project
Go-live verification
After deploying with live configuration:
- Submit a test live transaction and verify it reaches the tax authority (check the
environment: "live"field in the response) - Check queue status (Embedded): confirm
pendingcount decreases andcompletedcount increases - Verify webhook delivery: confirm your webhook endpoint receives the
fiscalization.completedevent - Monitor logs for certificate or submission errors in the first few hours
- Check deadline alerts: ensure no entries are approaching their submission window deadline
If anything fails, you can switch back to sandbox by reverting to a zyn_test_ key -- no server-side changes needed.