France Fiscalization Guide
What is French fiscalization?
France requires the point-of-sale software itself to be tamper-proof, rather than sending each invoice to a government server. The law (known as "Loi Anti-Fraude" -- Anti-Fraud Law) mandates that POS systems must be NF525-certified, meaning they maintain cryptographic proof that no transaction has been deleted, modified, or reordered.
In plain English: The French government doesn't watch your transactions in real-time. Instead, they require your POS software to create an unbreakable mathematical chain where every transaction is linked to the previous one. If a tax auditor shows up, they can verify that the chain is intact and no sales were hidden or deleted.
FiscalAPI is NF525-certified and handles all the cryptographic signing, chaining, audit logging, and receipt generation automatically. From your perspective, you just create transactions -- they complete in about 10 milliseconds with no external API calls.
How it works
France's NF525 flow is synchronous and local -- there's no government server involved:
- Your POS creates a transaction via
POST /v1/transactions - FiscalAPI digitally signs the transaction data and chains it to the previous transaction's signature
- A gap-free sequential number is assigned (no numbers can ever be skipped)
- The transaction is logged in an append-only audit log (called the JET)
- Running totals (daily, monthly, yearly, and lifetime) are updated
- Your transaction completes immediately with a
fiscal_idand a 4-character signature extract
There is no async processing or external submission. Transactions complete in ~10ms.
Example: When your POS submits a EUR 12 sale (two baguettes and three croissants) at a bakery in Paris, FiscalAPI signs the transaction, chains it to the previous one, logs it in the audit journal, and returns the result -- all within milliseconds. The response includes a 4-character signature extract (cgmp) that must appear on the printed receipt.
The signature chain -- why it matters
What is it?
Each transaction is digitally signed using ECDSA P-256 (an industry-standard cryptographic algorithm). The key innovation is that each signature incorporates the previous transaction's signature, creating a chain:
Signature(N) = ECDSA( payload(N) + "," + Signature(N-1) )
Why does France require this?
If someone tries to delete a transaction (to hide revenue from tax authorities), the chain breaks. The next transaction's signature was computed using the deleted one's signature -- so the math no longer checks out. An auditor can detect tampering by verifying the chain.
FiscalAPI manages the entire signature chain automatically. You never need to compute signatures, track chain state, or worry about chain integrity. The adapter handles all of it.
What's in the signing payload?
The signature covers 7 fields (you don't need to construct this -- it's documented for transparency):
| Position | Field | Example |
|---|---|---|
| 1 | Total amount per tax rate | 120.00 |
| 2 | Grand total (including tax) | 120.00 |
| 3 | Timestamp | 20260315143022 |
| 4 | Register ID | CAISSE-01 |
| 5 | Sequential number | 000042 |
| 6 | Transaction type | SALE |
| 7 | First transaction flag | N (no) or O (yes, first in chain) |
Signature extract on receipts
French law requires a 4-character extract of the digital signature to be printed on every receipt. FiscalAPI returns this as signature_extract in the transaction response. Your receipt printer just needs to display these 4 characters.
Fiscal ID format
The fiscal_id for French transactions is {register_id}/{sequence_number} -- for example, CAISSE-01/000042.
Technical Event Log (JET)
What is it?
The JET (Journal des Événements Techniques -- "Technical Event Log") is an append-only audit log required by NF525. Every fiscally significant event is recorded and can never be modified or deleted. Think of it as a permanent, tamper-proof activity log for the register.
What gets logged?
| Event | What it records |
|---|---|
TICKET | Every sale or return transaction |
DUPLICATE | Every time a receipt is reprinted |
GRANDTOTAL | Grand total calculations (e.g., end-of-day reports) |
ARCHIVE | When archive files are created |
CART_EMPTY | When items are removed from a cart before checkout |
PAYMENT_CHANGE | When a payment method is changed mid-transaction |
LOGIN / LOGOUT | Operator sign-in and sign-out |
CONFIG_CHANGE | Any configuration modification |
FiscalAPI automatically logs all required events. You don't need to manage the JET -- it's maintained internally and available for export during audits.
Grand totals
What are they?
NF525 requires the POS to maintain running counters that track cumulative transaction amounts. These counters prove that the total revenue reported matches the sum of all individual transactions. FiscalAPI maintains four levels of counters per register:
| Counter | How often it resets | What it tracks |
|---|---|---|
| Perpetual total | Never (lifetime of register) | Net sales (sales minus returns) |
| Perpetual total (absolute) | Never | Gross volume (sales plus absolute value of returns) |
| Daily total | Midnight | Today's running total |
| Monthly total | Month boundary | This month's accumulated total |
| Yearly total | Year boundary | This year's accumulated total |
Why two perpetual totals? The net total shows actual revenue. The absolute total shows total transaction volume (including returns). If a business processes EUR 100,000 in sales and EUR 5,000 in returns, the net perpetual total is EUR 95,000 and the absolute is EUR 105,000.
FiscalAPI updates all grand totals automatically with every transaction. You don't need to track or calculate these values.
Country config fields
The country_config object for French locations:
| Field | Type | Required | Description |
|---|---|---|---|
siren | string | Yes | 9-digit company identifier (France's equivalent of a federal business registration number). Must match the first 9 digits of the location's tax_id. |
Note: The SIRET (14-digit establishment identifier -- SIREN + a 5-digit establishment code) goes in the location's
tax_idfield, not incountry_config. It identifies the specific business location (like a branch or store).
| tva_intracom | string | Yes | French VAT number: FR + 2 check digits + 9-digit SIREN. This is the merchant's EU VAT identification number. |
| naf_code | string | Yes | Activity classification code (4 digits + 1 letter, e.g., 6201Z for software development). Similar to a US NAICS code. |
| legal_name | string | Yes | Business legal name |
| register_id | string | Yes | Unique identifier for this specific POS register (e.g., CAISSE-01). Each register maintains its own signature chain. |
| certificate_ref | string | No | NF525 certificate reference number (e.g., B 525/0498-5) |
| software_version | string | No | Certified software version |
| signing_key_id | string | No | Reference to signing key (auto-generated if you don't provide one) |
Validation rules
| Field | Rule | Example |
|---|---|---|
tax_id (SIRET) | 14 digits, passes Luhn checksum (set on location, not in country_config) | 80365813700036 |
siren | 9 digits, passes Luhn checksum, must match first 9 digits of tax_id | 803658137 |
tva_intracom | FR + 2 check digits + 9-digit SIREN | FR83404833048 |
naf_code | 4 digits + 1 uppercase letter (dot is optional) | 6201Z |
legal_name | Required, cannot be empty | Boulangerie Dupont SAS |
register_id | Required, cannot be empty | CAISSE-01 |
SIRET numbers starting with 356000000 (France's postal service, La Poste) use a different checksum algorithm (digit sum % 5 == 0) instead of the standard Luhn algorithm. FiscalAPI handles this automatically.
Configuration example
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Paris Boulangerie",
"country": "FR",
"address": "12 Rue de Rivoli, 75001 Paris",
"tax_id": "80365813700036",
"country_config": {
"siren": "803658137",
"tva_intracom": "FR83803658137",
"naf_code": "1071C",
"legal_name": "Boulangerie Dupont SAS",
"register_id": "CAISSE-01",
"certificate_ref": "B 525/0498-5",
"software_version": "1.0.0"
}
}'
Creating a transaction
Example: A customer at a Paris bakery buys 2 baguettes (EUR 1.30 each, 5.5% VAT), 3 croissants (EUR 1.50 each, 5.5% VAT), and 1 apple tart (EUR 28.00, 20% VAT).
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"location_id": "loc_abc123",
"type": "sale",
"amount": 12000,
"currency": "EUR",
"line_items": [
{
"description": "Baguette tradition",
"quantity": 2,
"unit_price": 130,
"vat_rate": 550
},
{
"description": "Croissant au beurre",
"quantity": 3,
"unit_price": 150,
"vat_rate": 550
},
{
"description": "Tarte aux pommes",
"quantity": 1,
"unit_price": 2800,
"vat_rate": 2000
}
]
}'
Response
{
"id": "txn_abc123",
"status": "success",
"fiscal_id": "CAISSE-01/000042",
"signature_extract": "cgmp",
"created_at": "2026-03-15T14:30:22Z"
}
The signature_extract (4 characters) must appear on the printed receipt for NF525 compliance.
NF525-compliant receipts
FiscalAPI generates PDF receipts that include all mandatory NF525 fields:
- Store name, address, SIRET, VAT number
- NAF code (business activity classification)
- Date and time (DD/MM/YYYY HH:MM:SS)
- Register ID ("Caisse")
- Sequential number (zero-padded 6 digits)
- Transaction type (SALE / RETURN)
- Itemized lines with quantity, unit price, tax rate, and totals
- Tax breakdown per rate (showing base amount, VAT, and total per rate)
- 4-character signature extract
- NF525 certificate reference
- Software version
- Footer: "Ticket de caisse certifié NF525"
Concurrency and data integrity
FiscalAPI uses database-level locks to ensure that sequential numbers are never skipped or duplicated, and that the signature chain is always correct -- even when multiple transactions hit the same register simultaneously. Multiple registers can process transactions concurrently without interfering with each other.
Error examples
Missing SIRET:
{
"error": "siret is required"
}
Invalid SIRET (Luhn check failure):
{
"error": "siret failed Luhn check"
}
Invalid NAF code:
{
"error": "naf_code must be 4 digits followed by 1 uppercase letter"
}
Sandbox testing
Sandbox routing is automatic when using a test API key (zyn_test_...). Test transactions hit sandbox endpoints without affecting production data.
curl -X POST https://api.zyntem.dev/v1/transactions \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"location_id": "loc_abc123",
"type": "sale",
"amount": 1200,
"currency": "EUR",
"line_items": [
{
"description": "Test item",
"quantity": 1,
"unit_price": 1000,
"vat_rate": 2000
}
]
}'