Portugal Fiscalization Guide
What is Portuguese fiscalization?
Portugal requires all invoicing software to do three things:
- Generate a unique code (ATCUD) on every invoice -- a tamper-evident identifier that proves the document is authentic
- Include a QR code on every invoice -- scannable by tax inspectors to verify the document
- Submit a monthly summary file (SAF-T) to the tax authority -- a structured XML file containing all invoices issued that month
Portugal does not require real-time reporting of each transaction to a government server. Instead, compliance is enforced through local document integrity (each invoice gets an ATCUD code and QR code) and monthly batch reporting (the SAF-T file).
In plain English: Every invoice your merchant issues gets a unique code and a QR code baked into it. At the end of each month, a summary file of all invoices is sent to Portugal's tax authority (the AT -- Autoridade Tributária e Aduaneira). The tax authority can verify individual invoices by scanning the QR code, or review the full month's activity via the SAF-T file.
Zyntem handles ATCUD generation, QR code content, document sequencing, and SAF-T file preparation automatically.
How it works
Portugal's flow is synchronous and local per transaction, with monthly batch submission:
- Your app creates a transaction via
POST /v1/transactions - Zyntem assigns a gap-free sequential number for the document series
- An ATCUD code is generated using the AT-assigned validation code and sequence number
- A QR code content string is built per the government specification
- A 4-character document hash is computed
- Your transaction completes immediately with a
fiscal_id, ATCUD, and QR content - At month end, Zyntem generates and submits the SAF-T PT billing file to AT
There is no per-transaction government API call. Transactions complete in ~10ms.
Example: When your POS submits a EUR 50 sale at a store in Lisbon (6 pastéis de nata at EUR 1.50 each + a bottle of vinho verde at EUR 12.00), Zyntem assigns the next sequential number in the series, generates the ATCUD code (e.g., ABCD1234-42), builds the QR code content string, and returns the transaction immediately. Your receipt printer renders the QR code from the content string.
Zyntem handles ATCUD code generation, QR code content, sequential numbering, document hashing, and monthly SAF-T file generation and submission. You just create transactions and print receipts with the QR code.
ATCUD codes
What is an ATCUD?
ATCUD stands for Código Único de Documento -- "Unique Document Code." Every invoice issued in Portugal must carry one. It uniquely identifies the document and proves it was issued by authorized software using an AT-registered document series.
An ATCUD has two parts:
ATCUD: {ValidationCode}-{SequentialNumber}
- ValidationCode: An alphanumeric code (8+ characters) that you get from the AT when you register a document series. Think of it as a "license plate" for a series of documents.
- SequentialNumber: A gap-free counter within that series (1, 2, 3, ...).
Example: ABCD1234-42 means validation code ABCD1234, document number 42 in that series.
Series registration
Before a merchant can issue invoices, each document series must be registered with the AT to obtain a validation code. Zyntem manages this process. You provide the validation codes in your location's country_config.
Zyntem tracks sequential numbering per series automatically. You never need to maintain counters or worry about gaps.
QR code
What goes in the QR code?
Every Portuguese invoice must include a QR code containing structured data that tax inspectors can scan to verify the document. The QR content is a text string with fields separated by *, each prefixed with an identifier:
| Field | ID | What it contains | Example |
|---|---|---|---|
| Issuer NIF | A | Seller's tax number | 123456789 |
| Buyer NIF | B | Buyer's tax number (use 999999990 for walk-in consumers) | 999999990 |
| Buyer country | C | Buyer's country code | PT |
| Document type | D | FT, FS, FR, NC, or ND (see document types below) | FT |
| Document status | E | N = normal, A = cancelled | N |
| Document date | F | Date in YYYYMMDD format | 20260315 |
| Document ID | G | Series prefix + number | FT A/1 |
| ATCUD | H | Full ATCUD code | ABCD1234-1 |
| Tax breakdown | I1-I8 | Country, base amounts, and tax amounts per rate | |
| Tax total | N | Total tax amount | 23.00 |
| Gross total | O | Total including tax | 123.00 |
| Hash | Q | First 4 characters of the document hash | a1b2 |
| Certificate | R | Software certificate number | 1234 |
Zyntem generates the entire QR content string automatically and returns it in the qr_code_url field. You just need to render it as a QR image on your receipts using any standard QR code library.
The qr_code_url field contains the QR code content string, not an image URL. Use any QR code library to render the string as a scannable QR image on your receipts.
Document types
Portugal requires different document types depending on the transaction. The adapter selects the correct type automatically:
| Code | Name | When Used | Description |
|---|---|---|---|
| FT | Fatura (Invoice) | B2B sales, or B2C sales over EUR 100 | Standard full tax invoice. Required when the buyer provides a tax ID (NIF) or the sale exceeds EUR 100. |
| FS | Fatura Simplificada (Simplified invoice) | B2C sales under EUR 100 | Simplified invoice for small retail sales to walk-in consumers. No buyer identification needed. |
| FR | Fatura-Recibo (Invoice-receipt) | Immediate payment + receipt | Combined invoice and payment receipt. Used when payment happens at point of sale and the customer needs both documents in one. |
| NC | Nota de Crédito (Credit note) | Refunds, returns, corrections | Issued when refunding a customer or correcting a previous invoice (full or partial). |
| ND | Nota de Débito (Debit note) | Adjustments, surcharges | Issued when adjusting an amount upward (e.g., correcting an undercharge on a previous transaction). |
You don't need to specify the document type -- the adapter determines it from your transaction:
- Submit a
sale→ FT or FS (based on amount and buyer info) - Submit a
refund→ NC automatically - Submit an
adjustment→ ND automatically - Submit a
void→ cancels the original document (status A / Anulado)
Example: When your POS submits a EUR 50 sale to a walk-in consumer (no buyer tax ID), Zyntem automatically uses the FS (simplified invoice) type. If the same sale is EUR 150, it uses FT (full invoice) instead.
VAT rates
Portugal has three VAT rate tiers, with reduced rates for the autonomous regions (Azores and Madeira):
| Rate tier | Mainland | Azores | Madeira |
|---|---|---|---|
| Reduced | 6% | 4% | 5% |
| Intermediate | 13% | 9% | 12% |
| Normal (standard) | 23% | 16% | 22% |
Zyntem automatically groups line items by rate tier for the QR code tax breakdown and SAF-T generation. You just provide the VAT rate on each line item.
SAF-T PT monthly submission
What is SAF-T?
SAF-T (Standard Audit File for Tax Purposes) is an XML file format used across Europe for tax reporting. Portugal's version (SAF-T PT, version 1.04_01) is submitted monthly to the AT and contains:
- Header: Company identification, fiscal year, software certification
- Master files: Customer list, product list, tax rate table
- Source documents: Every invoice issued in the reporting period, with full line-item detail
In plain English: At the end of each month, a comprehensive XML file summarizing all invoices is sent to Portugal's tax authority. It's like a detailed monthly sales report in a standardized format.
Zyntem generates the SAF-T file automatically from your transactions and submits it to the AT via their web service. You can also trigger manual SAF-T generation via the batch endpoint.
Country config fields
The country_config object for Portuguese locations uses a series map. Portugal's tax authority (AT) tracks document sequences per series, and each series has its own ATCUD validation code issued by AT when you register the series. A single location typically processes multiple document types (invoices, simplified invoices, credit notes), so the config maps each document type to its own series prefix and ATCUD code.
The adapter automatically determines which document type to use for each transaction (see Document types above) and looks up the corresponding series config.
| Field | Type | Required | Description |
|---|---|---|---|
series | object | Yes | Map of document type codes (FT, FS, FR, NC, ND) to series config |
series[TYPE].prefix | string | Yes | Series prefix including year (e.g., "FT 2026/") |
series[TYPE].atcud_code | string | Yes | AT-assigned ATCUD validation code for this series (8+ alphanumeric characters, obtained when registering the series with AT) |
Note: The Portuguese NIF (Número de Identificação Fiscal -- a 9-digit tax number, similar to a US TIN) goes in the location's
tax_idfield, not incountry_config.
You only need to configure series for the document types your location will issue. At minimum, most locations need FT (standard invoices). Add FS if you process small B2C sales, and NC if you handle refunds.
Validation rules
| Field | Rule | Example |
|---|---|---|
tax_id (NIF) | 9 digits, mod-11 check digit, cannot start with 0 or 4 (set on location) | 501442600 |
| Series keys | Must be one of: FT, FS, FR, NC, ND | "FT" |
prefix | Non-empty string | "FT 2026/" |
atcud_code | Non-empty alphanumeric string from AT (8+ chars) | "ABCD1234" |
Portuguese NIFs use a weighted mod-11 checksum. The last digit is the check digit, calculated from the first 8 digits with weights 9, 8, 7, 6, 5, 4, 3, 2. Valid first digits: 1-3 (individual), 5 (company), 6 (public entity), 7 (other entity), 8 (sole trader), 9 (irregular/temporary).
Configuration example
curl -X POST https://api.zyntem.dev/v1/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer zyn_test_abc123def456..." \
-d '{
"name": "Lisbon Store",
"country": "PT",
"address": "Rua Augusta 100, 1100-053 Lisboa",
"tax_id": "501442600",
"country_config": {
"series": {
"FT": { "prefix": "FT 2026/", "atcud_code": "ABCD1234" },
"FS": { "prefix": "FS 2026/", "atcud_code": "EFGH5678" },
"NC": { "prefix": "NC 2026/", "atcud_code": "IJKL9012" }
}
}
}'
Embedded SDK configuration
When using the Embedded Local SDK, you pass Portugal's country_config fields inside your initWithConfig() JSON. Portugal's per-transaction flow is entirely local (no government API calls), making it well suited for the Embedded SDK. The monthly SAF-T submission is queued and submitted automatically when connectivity is available.
zyntem_fiscal.init_with_config({
"environment": "sandbox",
"api_key": "zyn_test_abc123",
"country": "PT",
"tax_id": "501442600",
"country_config": {
"legal_name": "Loja Lisboa Lda",
"software_certificate_number": "1234",
"at_certificate_id": "pt-cert-001",
"series": {
"FT": { "prefix": "FT 2026/", "atcud_code": "ABCD1234" },
"FS": { "prefix": "FS 2026/", "atcud_code": "EFGH5678" },
"NC": { "prefix": "NC 2026/", "atcud_code": "IJKL9012" }
}
},
"storage": {
"sqlite_path": "/app/data/fiscalization.db"
},
"certs": {
"dir": "/app/certs"
}
})
Key notes for Embedded mode:
- The
software_certificate_numberis the numeric certificate issued by AT when your software is certified. It appears in the QR codeRfield. - The
at_certificate_idreferences the AT communication certificate stored incerts.dir, used for SAF-T submission. - The
seriesmap must include an entry for each document type your location will issue. Eachatcud_codeis obtained from AT when you register the series. - Valid series keys:
FT(invoice),FS(simplified invoice),FR(invoice-receipt),NC(credit note),ND(debit note). - Sequential numbering per series is tracked automatically in the local SQLite database.
See the Configuration Reference for the full list of engine-level fields (storage, certs, submission, etc.).
Creating a transaction
Example: A store in Lisbon sells 6 pastéis de nata (EUR 1.50 each, 6% VAT) and a bottle of vinho verde (EUR 12.00, 23% 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": 12300,
"currency": "EUR",
"line_items": [
{
"description": "Pastel de nata",
"quantity": 6,
"unit_price": 150,
"vat_rate": 600
},
{
"description": "Vinho verde",
"quantity": 1,
"unit_price": 1200,
"vat_rate": 2300
}
]
}'
Specifying a buyer (B2B transactions)
For B2B transactions, include the buyer's NIF and country. This also causes Zyntem to use the FT (full invoice) document type instead of FS (simplified).
{
"metadata": {
"buyer_nif": "500100144",
"buyer_country": "PT"
}
}
If omitted, the buyer defaults to a final consumer (NIF 999999990, country PT).
Response
{
"id": "txn_abc123",
"status": "success",
"fiscal_id": "PT-A 2026/-1",
"qr_code_url": "A:501442600*B:999999990*C:PT*D:FT*E:N*F:20260315*G:FT A 2026//1*H:ABCD1234-1*I1:PT*I3:7.36*I4:0.54*I5:9.76*I6:2.76*N:3.30*O:123.00*Q:a1b2",
"created_at": "2026-03-15T10:30:00Z"
}
The qr_code_url contains the full QR content string. Render it as a QR image on the receipt. The fiscal_id format is PT-{Series}-{SequenceNumber}.
Error examples
Missing NIF:
{
"error": "nif is required"
}
Invalid NIF (mod-11 check digit failure):
{
"error": "nif is invalid: \"123456780\""
}
Missing series map:
{
"error": "Portugal country_config.series is required (map of document type to series config)"
}
Missing ATCUD code for a series:
{
"error": "Portugal country_config.series[FT].atcud_code is required"
}
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": 1500,
"currency": "EUR",
"line_items": [
{
"description": "Test item",
"quantity": 1,
"unit_price": 1220,
"vat_rate": 2300
}
]
}'
Receipt presentation
render_hints on the transaction response tells your POS exactly what to
print and where. You do not need to read Portaria 195/2020 — the
hints are the contract.
key | Placement | Mandatory | Kind |
|---|---|---|---|
atcud | header (or footer per merchant choice) | Yes (Portaria 195/2020 §6) | text |
qr | footer_right | Yes | qr_string (PT delimited format, min 30 mm) |
The qr.value is the regulator-mandated delimited string (key=value pairs
separated by *), not a URL. Encode it verbatim in the QR — do not
URL-escape. meta.format is AT-delimited.
Example printed receipt
ATCUD: CSDF7T5H-1
══════════════════════════════════════
Empresa Teste Lda
NIPC: 500123456
══════════════════════════════════════
Espresso 1,23 EUR
──────────────────────────────────────
TOTAL 1,23 EUR
══════════════════════════════════════
┌──────────┐
│ ▓░▓░░▓░ │
│ ░▓░▓▓░▓ │
│ ▓▓░░▓░▓ │
└──────────┘
(≥ 30 mm)
meta schema (Portugal)
| Key | Item | Description |
|---|---|---|
min_size_mm | qr | Regulator-mandated minimum module size. |
format | qr | "AT-delimited" — the value is a *-delimited key=value string per Annex of Portaria 195/2020, not a URL. |