Skip to main content

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 prefixRoutes 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
caution

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.com over 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:

ErrorCauseAction
HTTP 401Invalid or revoked API keyCheck key is correct and active
HTTP 403Key lacks permission for this countryVerify key permissions
connection refusedCan't reach api.zyntem.comCheck 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:

CountrySubmission windowNotes
Spain (ES)4 daysTicketBAI/Verifactu and SII must submit within 4 days
Italy (IT)12 daysSDI submission within 12 days
Portugal (PT)None (monthly SAF-T)No per-transaction submission; monthly SAF-T batch file instead
France (FR)NoneNo 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_max for the engine, or max_attempts per 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 dead status and stops retrying

Deadline alerts

The queue monitors submission deadlines and raises alerts at three thresholds:

Alert levelThresholdMeaning
warning_75pct75% of window elapsedSubmission deadline approaching
critical_90pct90% of window elapsedDeadline imminent
breached_100pct100% of window elapsedDeadline 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

  • dead count increasing: Entries that exhausted all retries. Investigate the last_error on each dead entry -- common causes are certificate expiry, network issues, or tax authority outages.
  • pending count 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_90pct and breached_100pct alerts 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.

tip

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:

VariablePurposeExample
ENVIRONMENTSets environment nameproduction
FISCALIZATION_LIVE_ENABLEDEnables live mode (required for zyn_live_ keys)true
FISCALIZATION_CERTS_DIRCertificate directory path/data/certs
FISCALIZATION_CERTS_PASSWORDPKCS#12 certificate password(secret)
FISCALIZATION_STORAGE_BACKENDStorage backend (sqlite or postgres)postgres
DATABASE_URLPostgreSQL connection URL (Cloud API)postgres://...
FISCALIZATION_LOG_LEVELLog verbosityinfo
FISCALIZATION_SUBMISSION_AUTOAuto-submit prepared recordstrue
FISCALIZATION_SUBMISSION_RETRY_MAXMax retry attempts (engine-level)3
FISCALIZATION_SUBMISSION_RETRY_DELAYSeconds 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:

  1. Submit a test live transaction and verify it reaches the tax authority (check the environment: "live" field in the response)
  2. Check queue status (Embedded): confirm pending count decreases and completed count increases
  3. Verify webhook delivery: confirm your webhook endpoint receives the fiscalization.completed event
  4. Monitor logs for certificate or submission errors in the first few hours
  5. 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.