Skip to main content

Migration Guide

This page documents breaking and behavioral changes that affect existing integrations. Apply the fixes that match the API surface you use.

Versioned FFI Envelope (Embedded SDKs, C ABI)

Who is affected: Direct consumers of the C shared library (libfiscalization.so/.dylib/.dll) or the WASM module's low-level export.

The shared library now exposes exactly two C symbolsfiscal_call and fiscal_free — plus a single JNI entry point (FiscalEngineJni.call) and a single WASM export (fiscalCall). The previous nine ad-hoc exports (fiscal_init, fiscal_process_transaction, fiscal_queue_init, fiscal_queue_status, etc.) have been removed.

Both request and response speak a JSON envelope with a version field:

Request:  {"version":1,"method":"<name>","params":{...}}
Response: {"version":1,"result":{...}}
or {"version":1,"error":{"code":"...","message":"..."}}

Methods routed internally: init, process_transaction, queue_init, queue_status.

Before

char *out = fiscal_process_transaction("{...tx json...}");

After

char *out = fiscal_call(
"{\"version\":1,\"method\":\"process_transaction\",\"params\":{...tx json...}}"
);

SDK users are unaffected

The public API of every official SDK is unchanged. Python zyntem_fiscal.process_transaction(...), Kotlin FiscalEngine.processTransaction(...), .NET FiscalEngine.ProcessTransaction(...), Swift FiscalEngine.processTransaction(...), and the JavaScript/WASM processTransaction(...) wrapper all still work exactly as before — they now build and unwrap the envelope internally.

If you wrote your own FFI binding or forked the C header, update your dispatch path to use fiscal_call with the envelope. The version field lets future schema evolution happen without another rename cycle.

Location Registration Validates country_config

Who is affected: Anyone calling POST /v1/locations or PATCH /v1/locations/{id}.

country_config is now parsed and validated against the country-specific schema at registration time. Invalid config returns 400 Bad Request immediately; previously, some invalid shapes were only caught at transaction time (or silently accepted and defaulted to {}).

The accepted value is sealed at registration — adapters load the typed config from the sealed envelope on every transaction, so there is no "try again at transaction time" fallback for malformed config.

Per-country 400 errors are documented in the API reference. A few examples:

CountryExample new 400
Spain (ES)country_config.system is required for Spain locations (must be "ticketbai" or "verifactu")
Italy (IT)country_config.codice_fiscale must be 11-16 alphanumeric characters
France (FR)country_config.legal_name is required for France locations; country_config.register_id is required for France locations
Portugal (PT)Portugal country_config.series is required (map of document type to series config); Portugal country_config.series[FT].atcud_code is required

Action required

If you have existing locations that were accepted with partial or empty country_config, they have been backfilled to {} and will now fail per-country adapter checks on the next transaction submission. Call PATCH /v1/locations/{id} with a complete country_config to resolve — the PATCH payload will be re-validated and returns 400 if still invalid.

Asynchronous Error Translation

Who is affected: Anyone reading error_message on a failed transaction.

When a tax-authority submission fails, the transaction is persisted as failed immediately and the response is returned to the client without waiting on translation. The raw authority message (often in Spanish, Italian, French, or Portuguese) is translated to English in the background via the Claude API and filled in asynchronously.

Before

error_message was always populated synchronously with the raw authority message.

After

On a freshly failed transaction:

  • error_message may be null initially when AI translation is enabled. Poll or wait for the translation to fill it in.
  • The raw authority error code and message are always present in response_payload.submit_error.error_code / response_payload.submit_error.raw_message — use these if you need an immediate, code-stable value.

See Error Handling → Asynchronous error translation for the polling pattern.

Action required

Treat error_message as nullable. If your integration surfaces the error to end users or operators, prefer response_payload.submit_error.raw_message if you need to act synchronously, and fall back to polling for the translated error_message when the UX allows a few seconds of latency.

Management Plane Database Consolidation

Who is affected: Self-hosters and on-premise operators running their own core API + management plane binaries.

The fiscalization core API and the management plane now share a single PostgreSQL database. The management plane no longer owns its own migrations directory and no longer opens a separate DB connection string.

Removed:

  • FISCALIZATION_MANAGEMENT_* environment variables
  • Separate management_plane_dev / management_plane_* database
  • make mp-migrate-up / make mp-migrate-down Makefile targets
  • The rust/management-plane/migrations/ directory (management-plane tables now live in core API migrations)

Action required for self-hosters

  1. Stop running the management plane against its own database — point it at the same DATABASE_URL as the core API.
  2. Remove any FISCALIZATION_MANAGEMENT_* env vars from your deployment config.
  3. On first startup against a single DB, the core API migrations create the merged schema (including former management-plane tables: agents, heartbeats, binary_versions, license_keys, alerts, fleet_webhooks, fleet_webhook_deliveries, merchant_limit_violations). The management-plane binary trusts that this migration has already run.
  4. Note the table rename: the management-plane webhooks / webhook_deliveries tables are now fleet_webhooks / fleet_webhook_deliveries so they do not collide with the account-scoped webhook subscriptions owned by the core API.

At runtime there are no HTTP calls between the core API and the management plane — the management plane reads location data directly from the shared DB. If you had custom monitoring on the HTTP call volume between them, you can retire it.