France Fiscalization Guide
France requires NF525-certified point-of-sale systems to maintain tamper-evident signature chains, immutable audit logs, and perpetual grand totals. Unlike Spain and Italy, there is no government API submission -- all compliance is enforced locally through cryptographic integrity controls.
FiscalAPI handles signing, chaining, audit logging, and receipt generation automatically.
How it works
France's NF525 flow is synchronous and local:
- You create a transaction via
POST /v1/transactions - FiscalAPI signs the transaction data with ECDSA P-256 and chains it to the previous signature
- A sequential number is assigned (gap-free per register)
- The transaction is logged in the Technical Event Log (JET)
- Grand totals (perpetual, daily, monthly, yearly) are updated
- Your transaction completes immediately with a
fiscal_idand signature extract
There is no async processing or external submission -- transactions complete in ~10ms.
Signature chain
Each transaction is digitally signed using ECDSA P-256 with SHA-256. Signatures form a tamper-evident chain where each signature incorporates the previous one:
Signature(N) = ECDSA( payload(N) + "," + Signature(N-1) )
The first transaction in a chain signs payload + "," (empty previous signature).
Signing payload
The payload is 7 comma-separated fields:
| Position | Field | Example |
|---|---|---|
| 1 | Total TTC per tax rate (pipe-separated) | 120.00 |
| 2 | Total TTC | 120.00 |
| 3 | Timestamp (YYYYMMDDHHMMSS) | 20260315143022 |
| 4 | Register ID | CAISSE-01 |
| 5 | Sequential number (zero-padded 6 digits) | 000042 |
| 6 | Transaction type | SALE |
| 7 | First transaction flag | N |
Example: 120.00,120.00,20260315143022,CAISSE-01,000042,SALE,N
Signature extract
NF525 requires a 4-character extract of the signature to be printed on receipts. FiscalAPI returns this as signature_extract in the transaction response. The extract consists of the 3rd, 7th, 13th, and 19th characters (1-indexed) of the Base64url-encoded signature.
Fiscal ID format
The fiscal_id for French transactions is {register_id}/{sequence_number} -- for example, CAISSE-01/000042.
Technical Event Log (JET)
The JET (Journal des Événements Techniques) is an append-only audit log required by NF525. Every fiscally significant operation is recorded and cannot be modified or deleted.
Event types
| Event | Description |
|---|---|
TICKET | Individual sale transaction |
DUPLICATE | Receipt reprint |
GRANDTOTAL | Grand total calculation |
ARCHIVE | Archive file creation |
CART_EMPTY | Cart emptying / line deletion |
PAYMENT_CHANGE | Payment method change |
LOGIN | User login |
LOGOUT | User logout |
CONFIG_CHANGE | Configuration modification |
FiscalAPI automatically logs TICKET events for every transaction. Each JET entry includes the register ID, operator ID, timestamp, amount, tax breakdown, and a digital signature.
Grand totals
NF525 requires perpetual counters that track cumulative transaction amounts. FiscalAPI maintains four levels of counters per register:
| Counter | Reset | Formula |
|---|---|---|
| Perpetual total | Never (lifetime of register) | sales - |returns| |
| Perpetual total (absolute) | Never | sales + |returns| |
| Daily total | Midnight | Running total for current day |
| Monthly total | Month boundary | Accumulated for current month |
| Yearly total | Year boundary | Accumulated for current year |
For refunds, the perpetual total decreases while the perpetual absolute total increases. This ensures both net and gross volumes are tracked independently.
Country config fields
The country_config object for French locations:
| Field | Type | Required | Description |
|---|---|---|---|
siret | string | Yes | 14-digit establishment identifier (validated with Luhn check) |
siren | string | No | 9-digit company identifier (derived from SIRET if not provided) |
tva_intracom | string | Yes | French VAT number: FR + 2 check digits + SIREN |
naf_code | string | Yes | Activity code: 4 digits + 1 uppercase letter (e.g., 6201Z) |
legal_name | string | Yes | Business legal name |
register_id | string | Yes | Unique POS register identifier (e.g., CAISSE-01) |
certificate_ref | string | No | NF525 certificate reference (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 empty) |
Validation rules
| Field | Rule | Example |
|---|---|---|
siret | 14 digits, Luhn check | 80365813700036 |
siren | 9 digits, Luhn check, auto-derived from SIRET[:9] | 803658137 |
tva_intracom | FR + 2 check digits + 9-digit SIREN | FR83404833048 |
naf_code | 4 digits + 1 uppercase letter (dot optional) | 6201Z |
legal_name | Required, non-empty | Boulangerie Dupont SAS |
register_id | Required, non-empty | CAISSE-01 |
SIRET numbers starting with 356000000 (La Poste) use an alternative checksum (digit sum % 5 == 0) instead of the standard Luhn algorithm.
Configuration example
curl -X POST https://api.fiscalapi.com/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer fsk_test_abc123def456..." \
-d '{
"name": "Paris Boulangerie",
"country": "FR",
"address": "12 Rue de Rivoli, 75001 Paris",
"tax_id": "80365813700036",
"country_config": {
"siret": "80365813700036",
"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
curl -X POST https://api.fiscalapi.com/v1/transactions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer fsk_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, TVA
- NAF code
- 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 (Taux, Base HT, TVA, TTC)
- 4-character signature extract
- NF525 certificate reference
- Software version
- Footer: "Ticket de caisse certifié NF525"
Concurrency and data integrity
FiscalAPI uses PostgreSQL advisory locks to serialize writes per register. This ensures:
- Gap-free sequencing: Sequential numbers are never skipped or duplicated
- Chain integrity: Each signature correctly references the previous one
- Atomic updates: Grand totals, chain state, and JET entries are updated in a single transaction
Multiple registers can process transactions concurrently without contention.
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
Use test_mode: true on your transactions to test NF525 compliance without affecting production chains. Sandbox transactions use separate chain state and sequential numbering.
curl -X POST https://api.fiscalapi.com/v1/transactions \
-H "Authorization: Bearer fsk_test_abc123def456..." \
-d '{
"location_id": "loc_abc123",
"type": "sale",
"test_mode": true,
"amount": 1200,
"currency": "EUR",
"line_items": [
{
"description": "Test item",
"quantity": 1,
"unit_price": 1000,
"vat_rate": 2000
}
]
}'