Skip to main content

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.

You don't need to worry about this

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:

  1. Your POS creates a transaction via POST /v1/transactions
  2. FiscalAPI digitally signs the transaction data and chains it to the previous transaction's signature
  3. A gap-free sequential number is assigned (no numbers can ever be skipped)
  4. The transaction is logged in an append-only audit log (called the JET)
  5. Running totals (daily, monthly, yearly, and lifetime) are updated
  6. Your transaction completes immediately with a fiscal_id and 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.

You don't need to worry about this

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):

PositionFieldExample
1Total amount per tax rate120.00
2Grand total (including tax)120.00
3Timestamp20260315143022
4Register IDCAISSE-01
5Sequential number000042
6Transaction typeSALE
7First transaction flagN (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?

EventWhat it records
TICKETEvery sale or return transaction
DUPLICATEEvery time a receipt is reprinted
GRANDTOTALGrand total calculations (e.g., end-of-day reports)
ARCHIVEWhen archive files are created
CART_EMPTYWhen items are removed from a cart before checkout
PAYMENT_CHANGEWhen a payment method is changed mid-transaction
LOGIN / LOGOUTOperator sign-in and sign-out
CONFIG_CHANGEAny configuration modification
You don't need to worry about this

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:

CounterHow often it resetsWhat it tracks
Perpetual totalNever (lifetime of register)Net sales (sales minus returns)
Perpetual total (absolute)NeverGross volume (sales plus absolute value of returns)
Daily totalMidnightToday's running total
Monthly totalMonth boundaryThis month's accumulated total
Yearly totalYear boundaryThis 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.

You don't need to worry about this

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:

FieldTypeRequiredDescription
sirenstringYes9-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_id field, not in country_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

FieldRuleExample
tax_id (SIRET)14 digits, passes Luhn checksum (set on location, not in country_config)80365813700036
siren9 digits, passes Luhn checksum, must match first 9 digits of tax_id803658137
tva_intracomFR + 2 check digits + 9-digit SIRENFR83404833048
naf_code4 digits + 1 uppercase letter (dot is optional)6201Z
legal_nameRequired, cannot be emptyBoulangerie Dupont SAS
register_idRequired, cannot be emptyCAISSE-01
La Poste exception

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

You don't need to worry about this

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
}
]
}'