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.
FiscalAPI 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 - FiscalAPI 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, FiscalAPI 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), FiscalAPI 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.
FiscalAPI 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. FiscalAPI manages this process. You provide the validation codes in your location's country_config.
FiscalAPI 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 |
FiscalAPI 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), FiscalAPI 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% |
FiscalAPI 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.
FiscalAPI 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" }
}
}
}'
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 FiscalAPI 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
}
]
}'