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 by invoking the queue_status method on the FFI envelope (fiscal_call with {"version":1,"method":"queue_status","params":{}}):

{
"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.