Skip to main content

E-invoices (ZUGFeRD, Factur-X)

An e-invoice is the PDF your template renders and the same invoice as XML, in one file. Formfeed creates ZUGFeRD and Factur-X invoices: it converts the rendered PDF to PDF/A-3b, embeds the invoice as XML and validates the file before it is delivered. The XML is built from an _invoice block in the render data.

Formfeed is a tool that writes the format. It gives no tax advice: you issue the invoice and answer for its content and its VAT treatment, Formfeed answers for the format.

What you get​

  • One file for people and for software. The PDF looks as your template renders it. It is a PDF/A-3b and carries the invoice as an attachment named factur-x.xml, in the Cross Industry Invoice syntax (CII) that ZUGFeRD and Factur-X use.
  • ZUGFeRD and Factur-X. The two are one specification under two names, and the file is the same. Formfeed writes the release Factur-X 1.09, which is ZUGFeRD 2.5, in the profiles en16931 and basic.
  • The XML comes from your data. It is built from data._invoice and never read out of the PDF. Formfeed computes nothing: a total that does not add up is refused, never rounded.
  • Validated before delivery. The XML is held to its schema and to the rules of the profile, which include the business rules of EN 16931, and the file to PDF/A-3b. A file that does not pass is not delivered, and no plain PDF is delivered in its place: the render fails and says why.
  • A report with every invoice. The render's einvoice field says what was made, what the validator found, and whether the PDF shows the values the XML states.

It works for the PDF output of HTML templates, of Word and PowerPoint templates, of ad-hoc html renders and of url renders.

E-invoice coverage lists which business terms of EN 16931 the file carries and which validator and rules it passes. E-invoices by country says which countries ask for what, and where Formfeed's part ends.

Quick start​

  1. Get a key. A test key (ff_test_…) makes e-invoices on every plan, each with the test mark, up to a number a day; a live key (ff_live_…) makes them on the Starter plan and higher. See Authentication and Plans and units.
  2. Make an invoice template. The quickest way is a copy of the example: Templates → Examples → E-invoice: ZUGFeRD and Factur-X from one template. It declares the e-invoice and prints the values of its _invoice block. For a template of your own, switch the e-invoice on in Page setup (Switching it on) and print the same values. Publish the version.
  3. Send the invoice data. POST /v1/renders with the template's slug and the _invoice block in data, as in the request below.
  4. Read the report. A render that succeeded made a valid e-invoice. Its einvoice.validation.messages lists the validator's warnings, and einvoice.display.missing the values of the XML the PDF does not show (What comes back).

Switching it on​

An e-invoice can be declared in three places. They merge field by field, the later one winning:

  1. The template's settings (settings.einvoice). Every PDF rendered from the template is then an e-invoice, and a request need not say anything.
  2. The request's settings.einvoice.
  3. The request's post.einvoice: an object with the options, {} for the defaults, or false.

Each place takes the same object of options, or false. false drops what the places before it declared, so post.einvoice: false switches a template's declaration off for one request. A value that is neither an object of these options nor false answers 400 invalid_request, naming the field.

In the template​

In the editor, open Page setup, go to the E-invoice section and switch on Embed the invoice as XML in every PDF. The section then offers Format, Profile, When the PDF does not show a value of the XML and Store the XML as a file of its own, which are the options below.

Under them, the card Invoice data says what the data set on screen has to say about its _invoice block: Complete with the invoice number, the lines and the total, the number of findings with the field and the rule each one breaks, or No _invoice block, with Add the _invoice block to insert one with placeholders. The status bar names the kind of e-invoice the template makes. Save and publish the version, as with every other setting.

On a plan without e-invoices the switch cannot be turned on, unless the plan's test renders make them (Free does, see Plans and units). A template that already declares one, such as a copy of the example, shows the switch on, and you can turn it off to render plain PDFs.

Image templates have no such section, because an e-invoice is a PDF. Word and PowerPoint templates have none on their page either: declare the e-invoice in their settings through the Templates API or the project's settings.json, or ask for it per request.

In a project, the declaration is the einvoice key of settings.json:

{
"einvoice": { "profile": "en16931", "flavour": "zugferd" }
}

In the request​

curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Idempotency-Key: invoice-RE-2026-0001" \
-H "Content-Type: application/json" \
-d '{
"template": "invoice",
"data": {
"_invoice": {
"number": "RE-2026-0001",
"issue_date": "2026-01-15",
"type_code": "380",
"currency": "EUR",
"seller": {
"name": "Fennlor Studio GmbH",
"vat_id": "DE000000000",
"address": { "street": "Musterstraße 1", "postcode": "12345", "city": "Musterstadt", "country": "DE" },
"contact": { "name": "Erika Mustermann", "email": "rechnung@fennlor.example", "phone": "+49 30 0000000" }
},
"buyer": {
"name": "Olvarest GmbH",
"address": { "street": "Beispielweg 2", "postcode": "54321", "city": "Beispielstadt", "country": "DE" }
},
"delivery": { "date": "2026-01-15" },
"lines": [
{ "id": "1", "name": "Consulting", "quantity": 8, "unit_code": "HUR", "net_price": 120, "net_amount": 960, "tax": { "category": "S", "rate": 19 } }
],
"tax": { "breakdown": [{ "category": "S", "rate": 19, "basis": 960, "amount": 182.4 }] },
"totals": { "line_net": 960, "tax_basis": 960, "tax_total": 182.4, "grand": 1142.4, "due": 1142.4 },
"payment": { "means_code": "58", "iban": "DE36000000000000000000", "reference": "RE-2026-0001", "terms": "Zahlbar innerhalb von 14 Tagen ohne Abzug", "due_date": "2026-01-29" }
}
},
"post": { "einvoice": { "profile": "en16931", "flavour": "zugferd" } }
}'

The answer is the render, with the report in einvoice. For a template that declares an e-invoice itself, leave post out: the _invoice block in data is all the request needs.

  • Async renders and batches take the same fields. A batch queues nothing unless every item that makes an e-invoice can make one; the problem names the first item that cannot in index.

  • post.einvoice: false switches a template's declaration off for one request, for example for a plain PDF that carries a password:

    { "template": "invoice", "data": { … }, "post": { "einvoice": false, "password": { "user": "open-sesame" } } }
  • Ad-hoc html takes the invoice from data._invoice too. Without an engine the HTML is printed as it is, and data holds nothing but the block:

    { "html": "<h1>Rechnung RE-2026-0001</h1> …", "data": { "_invoice": { … } }, "post": { "einvoice": {} } }
  • A url render works the same way: the page at the address is printed as it is, the invoice comes from data._invoice, and the display check reads the text of that page:

    { "url": "https://invoices.example/RE-2026-0001", "data": { "_invoice": { … } }, "post": { "einvoice": {} } }

Options​

The same four options are the fields of post.einvoice, of settings.einvoice and of the E-invoice section in the editor. Every one has a default, so {} asks for a Factur-X invoice in the EN 16931 profile.

OptionValuesDefaultMeaning
profileen16931, basicen16931en16931 carries every field of the European standard. basic is the smallest profile that is an invoice on its own; France does not accept it for an invoice between two French businesses.
flavourfactur-x, zugferdfactur-xThe name the invoice goes by. ZUGFeRD and Factur-X are one specification under two names, and the file is the same: the embedded XML is factur-x.xml either way. The choice decides the name and the release the render reports, Factur-X 1.09 or ZUGFeRD 2.5.
xmlembedded, bothembeddedboth also stores the XML as a file of its own beside the PDF.
display_checkwarn, strictwarnWhat happens when the PDF does not show a value of the XML: warn reports it, strict fails the render. See The display check.
  • Upper and lower case and surrounding spaces make no difference in profile and flavour. A profile or a flavour outside these values answers 422 einvoice_profile_unsupported; that includes minimum, basic-wl, extended and xrechnung.
  • An option that does not exist, or a value of xml or display_check outside its list, answers 400 invalid_request.
  • In basic, the XML leaves out what that profile has no place for: the contact of seller and buyer, seller.legal_info, a line's description, seller_item_id, buyer_item_id, order_line_reference and buyer_accounting_reference, and payment.bic and payment.account_name. A totals.rounding other than 0 is refused in basic. Left out in the profile basic lists them with their business terms.
  • An invoice between two French businesses is refused in basic with 422 einvoice_data_invalid, because France's reform takes the profiles EN 16931 and EXTENDED-CTC-FR only. See French domestic invoices.
  • The layers merge field by field. A template that declares { "flavour": "zugferd", "display_check": "strict" } and a request with "post": { "einvoice": { "xml": "both" } } make a ZUGFeRD invoice in the EN 16931 profile, with the strict display check and the XML stored beside the PDF.

The _invoice block​

_invoice sits at the top level of data, beside whatever else the template uses. This is the block of the request above, which is also what Add the _invoice block inserts in the editor (there with today's date as issue_date and the due date 14 days later):

{
"_invoice": {
"number": "RE-2026-0001",
"issue_date": "2026-01-15",
"type_code": "380",
"currency": "EUR",
"seller": {
"name": "Fennlor Studio GmbH",
"vat_id": "DE000000000",
"address": {
"street": "Musterstraße 1",
"postcode": "12345",
"city": "Musterstadt",
"country": "DE"
},
"contact": {
"name": "Erika Mustermann",
"email": "rechnung@fennlor.example",
"phone": "+49 30 0000000"
}
},
"buyer": {
"name": "Olvarest GmbH",
"address": {
"street": "Beispielweg 2",
"postcode": "54321",
"city": "Beispielstadt",
"country": "DE"
}
},
"delivery": { "date": "2026-01-15" },
"lines": [
{
"id": "1",
"name": "Consulting",
"quantity": 8,
"unit_code": "HUR",
"net_price": 120,
"net_amount": 960,
"tax": { "category": "S", "rate": 19 }
}
],
"tax": {
"breakdown": [
{ "category": "S", "rate": 19, "basis": 960, "amount": 182.4 }
]
},
"totals": {
"line_net": 960,
"tax_basis": 960,
"tax_total": 182.4,
"grand": 1142.4,
"due": 1142.4
},
"payment": {
"means_code": "58",
"iban": "DE36000000000000000000",
"reference": "RE-2026-0001",
"terms": "Zahlbar innerhalb von 14 Tagen ohne Abzug",
"due_date": "2026-01-29"
}
}
}

What holds for every field:

  • Numbers are JSON numbers or the same as text (1142.4 or "1142.40"). An amount of money has at most 2 decimals, a quantity or a unit price at most 6, a rate or a percentage at most 4. More decimals are a finding; nothing is rounded for you. A JSON number is read at fifteen significant digits, so 0.30000000000000004 counts as 0.3; a number sent as text is taken as written.
  • Text fields must not be empty or only spaces. Leave a field out instead.
  • Dates are YYYY-MM-DD and must exist in the calendar.
  • Codes (currency, address.country, unit_code, payment.means_code, type_code) must be in their code list, in the release the validator uses; upper or lower case makes no difference.
  • Identifiers (id, legal_registration, electronic_address, standard_item_id) are a string, or an object { "value": "…", "scheme": "…" } that names the scheme the value belongs to.
  • Unknown fields are dropped, not refused. The template reads _invoice for display too, so data only the page needs may sit in the block.

The tables name the business term (BT) or group (BG) of EN 16931 a field carries. E-invoice coverage lists all terms the other way round, and those the block does not carry.

How the block is checked​

Before anything is rendered or counted, the block is held to the rules in the tables below, in three stages:

  1. The shape: every required field is there, with the right type.
  2. The values: decimals, dates, codes, the check digits of an IBAN, the country prefix of a VAT ID.
  3. The rules of EN 16931: the totals add up, the VAT breakdown matches the lines, each VAT category has what it asks for, the payment details are complete, and, for an invoice between two French businesses, what France asks for.

A stage is checked only when the one before it passed, so a block that is missing a field is not yet told that its totals do not add up. A block that fails answers 422 einvoice_data_invalid. errors lists every finding of the stage, each with the field (path, from data._invoice, list items counted from 0), its message and, where one applies, the rule; detail repeats the first:

{
"type": "https://docs.formfeed.dev/errors/einvoice-data-invalid",
"title": "E-invoice data invalid",
"status": 422,
"code": "einvoice_data_invalid",
"detail": "_invoice.totals.due is 1000.00, but grand less prepaid plus rounding is 1142.40 (BR-CO-16)",
"errors": [
{
"path": "data._invoice.totals.due",
"message": "_invoice.totals.due is 1000.00, but grand less prepaid plus rounding is 1142.40 (BR-CO-16)",
"rule": "BR-CO-16"
}
]
}

The editor's Invoice data card, formfeed validate and formfeed einvoice run the same check offline. After rendering, the validator holds the finished file to the full rule set of the profile, which can find more; see What comes back.

Document​

FieldTermRequiredMeaning
numberBT-1yesThe invoice number.
issue_dateBT-2yesThe date of issue.
type_codeBT-3noThe kind of document, a code of UNTDID 1001, as text or as a number: 380 invoice (the default), 381 credit note, 384 corrected invoice, 389 self-billed invoice.
currencyBT-5yesThe currency of the invoice, ISO 4217 (EUR).
accounting_currencyBT-6noThe currency the VAT is accounted in, where the invoice is in another. It goes with totals.tax_total_accounting: state both or neither (BR-53).
buyer_referenceBT-10noThe buyer's routing reference. A German authority's Leitweg-ID goes here.
order_referenceBT-13noThe number of the buyer's order.
contract_referenceBT-12noThe number of the contract.
noteBT-22noA note on the invoice, as free text. It becomes the first note, without a subject code.
notes[]BT-21, BT-22dependsFurther notes, each with text and an optional subject_code (UNTDID 4451). Three are required for French domestic invoices.
periodBG-14 (BT-73, BT-74)noThe invoicing period: start, end or both. It must not end before it starts (BR-29).
preceding_invoices[]BG-3noThe invoices a credit note or a correction refers to: number (BT-25, required in each entry) and issue_date (BT-26).
operation, operation_codeBT-23dependsRequired for French domestic invoices.

Seller and buyer​

seller and buyer take the same fields; where two terms are named, the first is the seller's.

FieldTermRequiredMeaning
nameBT-27, BT-44yesThe name of the business.
trading_nameBT-28, BT-45noThe name the business trades under, where it differs.
idBT-29, BT-46noAn identifier of the party.
legal_registrationBT-30, BT-47noThe register entry: a company number, a SIREN.
vat_idBT-31, BT-48dependsThe VAT identification number. It starts with its country code, as in DE000000000 (BR-CO-09; EL for Greece); spaces are removed. Whether it is required depends on the VAT categories.
address.countryBT-40, BT-55yesThe country, ISO 3166-1 with two letters (DE).
address.street, .street2, .street3BT-35, BT-36, BT-162; BT-50, BT-51, BT-163noThe lines of the street address.
address.postcode, address.cityBT-38, BT-37; BT-53, BT-52noThe postcode and the city.
address.subdivisionBT-39, BT-54noThe region or province.
contact.name, .phone, .emailBG-6, BG-9noWhom to ask about the invoice: one name, a person's or a department's. Left out in basic.
electronic_addressBT-34, BT-49dependsWhere the party receives electronic invoices. An e-mail address as a string (it gets the scheme EM), or an object whose scheme is a code of the EAS list, as in { "value": "000000000", "scheme": "0225" }. Any other string is refused for want of its scheme. Required for both parties of a French domestic invoice.

Only the seller has:

FieldTermRequiredMeaning
seller.tax_numberBT-32dependsThe seller's tax number, where there is no VAT ID. Whether it is required depends on the VAT categories.
seller.legal_infoBT-33noFurther legal information the invoice must state: register court, capital. Left out in basic.
seller.vat_on_debitsBT-8dependsRequired for French domestic invoices.

Only the buyer has buyer.siren (BT-47), see French domestic invoices.

The seller needs at least one of vat_id, legal_registration and id (BR-CO-26). Which tax identifiers the check asks for beyond that depends on the VAT categories the invoice uses.

Delivery​

FieldTermRequiredMeaning
delivery.dateBT-72noThe date of the delivery.
delivery.nameBT-70noThe name of the party delivered to.
delivery.addressBG-15noThe address delivered to, with the fields of a party's address (BT-75 to BT-80, BT-165). country (BT-80) is required once there is an address.

An invoice that states no delivery is valid, and its report carries one warning, PEPPOL-EN16931-R008 (an empty element): the syntax requires the delivery element, and it is empty when the block has none.

Lines​

lines[] (BG-25) has at least one entry.

FieldTermRequiredMeaning
idBT-126yesThe identifier of the line, as text or as a number.
nameBT-153yesThe name of the item.
descriptionBT-154noA longer description of the item. Left out in basic.
quantityBT-129yesThe quantity invoiced, at most 6 decimals. A credit is a negative quantity.
unit_codeBT-130, BT-150noThe unit, a code of UN/ECE Recommendation 20 or 21: C62 a piece (the default), HUR an hour, DAY a day, KGM a kilogram. A word such as hours is refused (BR-CL-23); for the common ones the finding names the code meant.
net_priceBT-146yesThe net price of one unit, at most 6 decimals. It must not be negative (BR-27).
price_base_quantityBT-149noThe quantity the price is for, where it is not one unit.
net_amountBT-131yesThe net amount of the line: quantity times price (divided by price_base_quantity where there is one), less the line's allowances, plus its charges. The check before rendering does not recompute it; the totals are added up from it.
tax.categoryBT-151yesThe VAT category: S, Z, E, AE, K, G or O.
tax.rateBT-152dependsThe VAT rate in percent, 19 for 19 %. Required for every category but O.
periodBT-134, BT-135noThe period the line covers: start, end or both (BR-30).
allowances[], charges[]BG-27, BG-28noAllowances and charges on the line: amount (required in each entry), reason, reason_code, base_amount, percent, as on the document.
buyer_accounting_referenceBT-133noThe buyer's accounting reference for the line. Left out in basic.
noteBT-127noA note on the line.
seller_item_idBT-155noThe seller's article number. Left out in basic.
buyer_item_idBT-156noThe buyer's article number. Left out in basic.
standard_item_idBT-157noA standard identifier of the item, as an object with its scheme (BR-64): { "value": "…", "scheme": "0160" } for a GTIN.
order_line_referenceBT-132noThe line of the buyer's order this line answers. Left out in basic.

Allowances and charges on the document​

allowances[] and charges[] at the top of the block (BG-20, BG-21) apply to the whole invoice. A line's own allowances and charges take the same fields without tax.

FieldTermRequiredMeaning
amountBT-92, BT-99yesThe amount of the allowance or the charge, without VAT.
tax.categoryBT-95, BT-102yesThe VAT category the allowance or the charge falls under, as on a line. Not on a line's own allowances and charges, which fall under the line's.
tax.rateBT-96, BT-103dependsThe VAT rate, as on a line: required for every category but O.
reasonBT-97, BT-104dependsThe reason, as text. EN 16931 asks for reason or reason_code on every allowance and charge; the check before rendering does not, the validator does.
reason_codeBT-98, BT-105dependsThe reason as a code: UNTDID 5189 for an allowance (95 a discount), UNTDID 7161 for a charge (FC freight).
base_amount, percentBT-93, BT-94; BT-100, BT-101noThe amount the allowance or charge is a percentage of, and that percentage.

How the amounts fit together, with a line allowance, a document allowance and a document charge, all at 19 %:

{
"lines": [
{
"id": "1",
"name": "Etiketten 100 × 150 mm",
"quantity": 10,
"unit_code": "XRO",
"net_price": 18,
"net_amount": 162,
"tax": { "category": "S", "rate": 19 },
"allowances": [
{
"amount": 18,
"base_amount": 180,
"percent": 10,
"reason": "Mengenrabatt",
"reason_code": "95"
}
]
},
{
"id": "2",
"name": "Thermotransferband",
"quantity": 4,
"net_price": 12.5,
"net_amount": 50,
"tax": { "category": "S", "rate": 19 }
}
],
"allowances": [
{
"amount": 10.6,
"base_amount": 212,
"percent": 5,
"reason": "Treuerabatt",
"reason_code": "95",
"tax": { "category": "S", "rate": 19 }
}
],
"charges": [
{
"amount": 9.9,
"reason": "Versand",
"reason_code": "FC",
"tax": { "category": "S", "rate": 19 }
}
],
"tax": {
"breakdown": [
{ "category": "S", "rate": 19, "basis": 211.3, "amount": 40.15 }
]
},
"totals": {
"line_net": 212,
"allowances": 10.6,
"charges": 9.9,
"tax_basis": 211.3,
"tax_total": 40.15,
"grand": 251.45,
"prepaid": 50,
"due": 201.45
}
}

The line allowance is inside the first line's net_amount (10 × 18 − 18 = 162). The document's allowance and charge are not: they enter the breakdown's basis and the totals (212 − 10.60 + 9.90 = 211.30).

VAT breakdown​

tax.breakdown[] (BG-23) has one entry for each combination of category and rate the lines, allowances and charges use, and no entry twice.

FieldTermRequiredMeaning
categoryBT-118yesThe VAT category.
rateBT-119dependsThe VAT rate in percent, as on a line.
basisBT-116yesThe amount the VAT is calculated on: the net_amount of the lines of that category and rate, plus the document's charges and less the document's allowances of that category and rate.
amountBT-117yesThe VAT amount. Where it differs from rate times basis by more than VAT rounded line by line can explain (half a cent for each line, allowance and charge of that rate), the block is refused (BR-CO-17).
exemption_reasonBT-120dependsWhy no VAT is charged, as text. One of the two reason fields is required for every category but S and Z, which take neither.
exemption_reason_codeBT-121dependsThe same as a VATEX code, such as VATEX-EU-AE.

VAT categories​

The categories are the codes of UNCL 5305 as EN 16931 uses them. Which category and rate apply to a sale is a tax question. Formfeed does not answer it and takes the values you send. It checks only that the block is consistent with the category you state:

CategoryrateIn the breakdownSeller and buyer
Sabove 0no exemption reasonseller: vat_id or tax_number
Z0amount 0, no exemption reasonseller: vat_id or tax_number
E0amount 0, exemption_reason or exemption_reason_codeseller: vat_id or tax_number
AE0amount 0, exemption_reason or exemption_reason_codeseller: vat_id or tax_number; buyer: vat_id or legal_registration
K0amount 0, exemption_reason or exemption_reason_codeseller: vat_id; buyer: vat_id. The invoice also states delivery.date or period, and delivery.address.
G0amount 0, exemption_reason or exemption_reason_codeseller: vat_id
Oleft outamount 0, exemption_reason or exemption_reason_code; no other category in the invoiceneither seller nor buyer states a vat_id

The rules a finding names follow the category: BR-S-…, BR-Z-…, BR-E-…, BR-AE-…, BR-IC-… for K, BR-G-… and BR-O-….

Totals​

totals (BG-22). Each total has to equal what the standard defines it as, to the cent. allowances, charges, prepaid and rounding count as 0 when they are left out, and are checked all the same: an invoice with document allowances has to state totals.allowances.

FieldTermRequiredMeaning
line_netBT-106yesThe sum of the lines' net_amount (BR-CO-10).
allowancesBT-107noThe sum of the document's allowances (BR-CO-11).
chargesBT-108noThe sum of the document's charges (BR-CO-12).
tax_basisBT-109yesThe net total: line_net less allowances plus charges (BR-CO-13).
tax_totalBT-110yesThe VAT total: the sum of the breakdown's amounts (BR-CO-14).
tax_total_accountingBT-111noThe VAT total in the accounting_currency (BR-53).
grandBT-112yesThe total with VAT: tax_basis plus tax_total (BR-CO-15).
prepaidBT-113noWhat has been paid already.
roundingBT-114noA rounding amount. Refused in basic unless it is 0.
dueBT-115yesThe amount due: grand less prepaid plus rounding (BR-CO-16).

Payment​

payment is optional as a whole, but an invoice with an amount due above 0 states due_date or terms (BR-CO-25).

FieldTermRequiredMeaning
means_codeBT-81dependsHow the invoice is paid, a code of UNTDID 4461, as text or as a number: 58 SEPA credit transfer, 59 SEPA direct debit, 30 credit transfer, 48 card. Required once iban, bic, account_name, mandate_id or debited_iban is stated (BR-49).
ibanBT-84dependsThe account to pay to. Required for the codes 30 and 58 (BR-61). Its check digits are verified.
bicBT-86noThe BIC of that account. Left out in basic.
account_nameBT-85noThe name of the account. Left out in basic.
referenceBT-83noWhat the payer quotes with the payment.
termsBT-20dependsThe payment terms, as text. One of terms and due_date is required when totals.due is above 0.
due_dateBT-9dependsThe date the payment is due.
mandate_idBT-89noDirect debit: the mandate.
creditor_idBT-90noDirect debit: the creditor's identifier.
debited_ibanBT-91noDirect debit: the account that is debited. Its check digits are verified.

The XML is the invoice, so the PDF has to show what it says. The reliable way is to print the values of _invoice in the template, rather than values from other fields or totals the template adds up itself:

<h1>Invoice {{ _invoice.number }}</h1>
<p>{{ _invoice.buyer.name }}</p>

<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>Net</th>
</tr>
</thead>
<tbody>
{% for line in _invoice.lines %}
<tr>
<td>{{ line.name }}</td>
<td>{{ line.quantity }}</td>
<td>{{ line.net_price | money }}</td>
<td>{{ line.net_amount | money }}</td>
</tr>
{% endfor %}
</tbody>
</table>

<p>Net {{ _invoice.totals.tax_basis | money }}</p>
<p>VAT {{ _invoice.totals.tax_total | money }}</p>
<p>Total {{ _invoice.totals.grand | money }}</p>
<p>Due {{ _invoice.totals.due | money }}</p>

The helpers format the values for the reader; the display check reads amounts in either notation. The XML does not depend on the locale or on translations: a German and an English PDF of the same data embed the same XML.

The example E-invoice: ZUGFeRD and Factur-X from one template is a complete invoice template built this way. Templates → Examples in the app puts a copy of it into your workspace.

The display check​

In an e-invoice the XML is the invoice. Nothing forces a template to print the values of _invoice: one that adds up its own totals, or shows another field's number, renders without complaint, and a PDF that shows other invoice details can count as a second invoice. So Formfeed reads the text of the finished PDF, headers and footers included, and looks for what the XML states:

TermValueLooked for
BT-1numberas text; spaces, line breaks, upper and lower case and the kind of dash make no difference
BT-109totals.tax_basisas a number
BT-110totals.tax_totalas a number
BT-112totals.grandas a number
BT-115totals.dueas a number, where it differs from totals.grand
BT-117each tax.breakdown[].amountas a number, where the breakdown has more than one entry
  • A number counts as shown in either notation, 1.234,56 or 1,234.56. Thousands may also be grouped with a space or an apostrophe, and the sign is ignored, because a credit note may print as negative what its XML states as positive.
  • Zero amounts are not looked for: a template need not print 0,00. Dates are not compared.
  • The result is einvoice.display: checked lists the terms that were looked for, missing those the text does not show.

What a missing value does is the option display_check:

  • warn, the default: the render succeeds, and display.missing names the terms.
  • strict: the render fails with 422 einvoice_display_mismatch, and no file is delivered. The problem carries the report in einvoice.

The check finds a value that is missing, not a wrong one printed beside the right one. It is a net, not a proof: a number counts as shown wherever in the text it stands, and a value that is part of a picture is not found, because the check reads text.

The editor runs the same check before anything is rendered. For a template that declares an e-invoice, the Flow and Paged previews count the invoice values the document shows, and the Problems list names each one that is missing. formfeed render prints the result of the render's own check.

French domestic invoices​

When the seller's and the buyer's address.country are both FR, the check asks for what France's rules call mandatory on top of EN 16931:

FieldRuleMeaning
seller.legal_registrationBR-FR-10The seller's SIREN (BT-30), nine digits.
buyer.sirenBR-FR-11The buyer's SIREN, nine digits. It is written as the buyer's legal registration (BT-47) with the scheme 0002. A SIREN given as buyer.legal_registration counts too; if both are stated, they have to be the same.
operationBR-FR-08What was sold: goods, services or mixed. It becomes the billing framework B1, S1 or M1 in the business process (BT-23). For another framework, send its code as operation_code, which wins over operation: B1, S1, M1, B2, S2, M2, S3, B4, S4, M4, S5, S6, B7, S7, B8, S8 or M8.
seller.vat_on_debitstrue when the seller owes the VAT when the invoice is issued, false when it is owed on payment. true writes the code for "date of the invoice" (BT-8) into each VAT breakdown; false writes nothing.
notes[]BR-FR-05The statutory mentions, one note each with its subject_code (BT-21): PMT for the fixed compensation of €40 for recovery costs, PMD for the penalties for late payment and AAB for the discount for early payment, or that none is given.
seller.electronic_address, buyer.electronic_addressBR-FR-13, BR-FR-12The electronic addresses of seller (BT-34) and buyer (BT-49), each with its scheme.

These are the fields to add to, or replace in, the block above. With them, and with French addresses for both parties, the block passes the check in en16931:

{
"operation": "services",
"notes": [
{
"subject_code": "PMT",
"text": "En cas de retard de paiement, une indemnité forfaitaire pour frais de recouvrement de 40 € sera exigée."
},
{
"subject_code": "PMD",
"text": "En cas de retard de paiement, des pénalités seront appliquées au taux de trois fois le taux d'intérêt légal."
},
{
"subject_code": "AAB",
"text": "Aucun escompte n'est accordé pour paiement anticipé."
}
],
"seller": {
"name": "Fennlor Studio SAS",
"legal_registration": { "value": "000000000", "scheme": "0002" },
"vat_id": "FR00000000000",
"vat_on_debits": true,
"address": {
"street": "1 rue du Modèle",
"postcode": "99000",
"city": "Villemodèle",
"country": "FR"
},
"electronic_address": { "value": "000000000", "scheme": "0225" }
},
"buyer": {
"name": "Olvarest SARL",
"siren": "000000018",
"address": {
"street": "2 avenue de l'Exemple",
"postcode": "99100",
"city": "Exempleville",
"country": "FR"
},
"electronic_address": { "value": "000000018", "scheme": "0225" }
}
}
  • Write such an invoice in the profile en16931. France's reform takes the profiles EN 16931 and EXTENDED-CTC-FR only, so basic is refused for it with 422 einvoice_data_invalid.
  • The validator then holds the file to France's own business rules as a whole, the BR-FR rules of the standard AFNOR XP Z12-012, in the version Mustang 2.26.0 carries (1.3.0, 16 February 2026). It reports what breaks them as warnings in einvoice.validation.messages and still delivers the file, which is why the check asks for the mandatory ones above before rendering. Look there for any other BR-FR finding before the invoice goes to your platform.
  • The delivery address, where it differs from the buyer's, goes into delivery.address, with delivery.name and delivery.date. The check does not ask for it.
  • None of this is asked for when only one of the two is in France.

Formfeed creates the file. Sending it through a plateforme agréée is yours or your platform's to do, see What Formfeed does not do.

What comes back​

A render that made an e-invoice carries the report in einvoice. For every other render the field is null. The example shows the fields that matter here; the render object has the rest.

{
"id": "rnd_7da09ab6cd9946b798c187c2a9350852",
"status": "succeeded",
"output": "pdf",
"page_count": 1,
"units": 2,
"einvoice": {
"profile": "en16931",
"flavour": "factur-x",
"spec_version": "1.09",
"xml_url": null,
"validation": {
"valid": true,
"schematron": "Factur-X 1.09.2 (Mustang 2.26.0)",
"pdfa": "PDF/A-3b",
"messages": [],
"id": "6f2c7e9a-1d4b-4c3e-8a5f-2b7d9e0c1a34",
"versions": {
"mustang": "2.26.0",
"verapdf": "1.30.2",
"pdfbox": "3.0.8"
},
"xml_sha256": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
"pdf_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
},
"display": {
"checked": ["BT-1", "BT-109", "BT-110", "BT-112"],
"missing": []
}
}
}
FieldMeaning
profile, flavourWhat was made, with every default filled in.
spec_versionThe release of the specification the file was written and validated for, under the name of flavour: 1.09 for Factur-X, 2.5 for ZUGFeRD.
xml_urlA link to the XML, when the render asked for xml: both and the file is still stored; null otherwise. It is made like download_url: signed, or the public link with access: "public". The XML is inside the PDF either way.
validationWhat the validator said. null only when the render failed before the validator ran.
validation.validtrue when no finding is an error. A render succeeds only with a valid file.
validation.schematronThe rule set the XML was held to, as the validator names it.
validation.pdfaPDF/A-3b once the file was checked against it; null when validation ended at the XML.
validation.messages[]The findings, at most 50, errors first. Each has part (xml or pdf), severity (error or warning), rule (a rule of EN 16931 such as BR-CO-25, a rule of the syntax such as CII-SR-465, a code of the XML schema such as cvc-…, or a clause of PDF/A such as 6.2.11.4.1-1; null where the validator names none), message and location (an XPath into the XML, a line and column for a finding of the XML schema, or the object of the PDF).
validation.truncatedPresent and true when there were more findings than messages holds.
validation.idThe validator's own id for this report. Quote it when you ask about a render.
validation.versionsThe versions of the validator's parts that judged the file: Mustang, veraPDF and PDFBox. Validator and rules lists the versions in use.
validation.xml_sha256, validation.pdf_sha256SHA-256 of the XML and of the PDF that were judged. For a render that succeeded, the PDF's is the delivered file, the same as the render's output_sha256, and the XML's is the XML inside it, so the report names the very file it is about and can be kept beside the invoice as the record of the check. pdf_sha256 is null where the validator refused the XML before there was a converted file.
displayThe display check: checked lists the business terms that were looked for in the text of the PDF, missing those it does not show. null when the render failed before the check ran.
  • GET /v1/renders/{id}, the list of renders and the render.completed and render.failed webhook events carry the same object.
  • Reports of renders before 9 October 2026 have no id, versions or hashes.
  • With ?output=binary the answer is the PDF itself. Read the report from GET /v1/renders/{id}, with the id in the X-Render-Id header.
  • A failed render keeps its report. When the validator refuses the file, the render is failed, its einvoice.validation says why, and display is null, because the display check runs after the validator. The problem of the request carries the same einvoice.
  • In the app, the render's sheet in the render log has an E-invoice section with the verdict, the findings and what the PDF shows. Report (JSON) downloads the report as a file. A True render in the editor shows the same section under its result: it is a real e-invoice, validated like an API render, free like every preview, and a test render with the test mark (see Plans and units).

Storing the XML​

With "xml": "both" (Store the XML as a file of its own in the editor), the XML is stored as a second file beside the PDF. It is the same XML that is embedded in the PDF.

  • The file has the PDF's name with .xml: invoice-RE-2026-0001.pdf gets invoice-RE-2026-0001.xml.
  • einvoice.xml_url is the link to it. download_url stays the link to the PDF.
  • It is kept and deleted with the PDF: same retention, same expires_in, same early deletion.
  • With bring your own storage, the XML is copied into your bucket like the PDF. Where the render names its own object with storage.key, the XML gets that key with its extension replaced by .xml.

With embedded, the default, xml_url is null and the XML exists only inside the PDF. Previews in the editor store nothing.

What it cannot be combined with​

  • A password. post.password together with an e-invoice answers 400 invalid_request with field: "post.password", wherever the e-invoice was declared: PDF/A forbids encryption. For an encrypted plain PDF from an invoice template, send post.einvoice: false.
  • Any output but PDF. A request that asks for an e-invoice on an image, a docx or a pptx output answers 400 invalid_request with field: "post.einvoice". A template's own declaration is simply not applied there: an image of an invoice template stays an image.
  • Deduplication. An e-invoice is never served from the deduplication cache; every request renders.
  • The PDF tools afterwards. The PDF tools merge, watermark and protect take the PDF of an e-invoice like any other PDF, and what they return is a new file that was never validated: a merged file carries no XML, a protected one is encrypted, and a watermarked one was saved again. The e-invoice is the render's own output; send and archive that one.

post.merge_after and a watermark work, as text or as an image. A text watermark covers Latin, Greek and Cyrillic letters, because its font is embedded in the file; use an image for other scripts. The e-invoice is the last step, after the merge, the watermark and the test mark, so what is converted and validated is the document as it will be delivered. PDFs you append have to meet PDF/A too: one whose fonts are not embedded fails the render with pdfa_conversion_failed.

As a PDF/UA-1 file​

An e-invoice can be a PDF/UA-1 document too, the format a screen reader reads a PDF by. Set pdf.ua beside the e-invoice, in the template's settings or in the request, for an HTML template or for HTML you send. In a template's settings.json:

settings.json
{
"pdf": {
"ua": true,
"metadata": { "title": "Rechnung {{ _invoice.number }}" }
},
"einvoice": { "profile": "en16931", "flavour": "zugferd" }
}

The PDF/A-3b keeps the tags of the page, and the finished e-invoice is held to PDF/UA-1 as well as to PDF/A-3b, in the same request. The render then carries accessibility beside einvoice. A file that fails PDF/UA-1 fails the render with 422 pdfua_validation_failed, whose problem carries both reports, or with { "check": "report" } is delivered as the valid e-invoice it is, without the PDF/UA identifier. The check costs no unit of its own.

Two things do not go with it: post.merge_after, because a merge drops the tags of what it appends, and Word and PowerPoint templates, whose PDF comes from another converter. An e-invoice from a Word template is an e-invoice without pdf.ua.

Plans and units​

Live e-invoices need the Starter plan or higher. Test renders make e-invoices on every plan, up to a number a day, each a valid file with the test mark on its pages:

PlanLive rendersTest renders, per day
Freerefused with 403 forbidden20
Starteryes100
Proyes500
Businessyes2,000
Enterpriseyesnot counted

On the paid plans the day's number is a thirtieth of the plan's monthly units.

  • Test renders are renders with a test key, Send test request in the editor's API tab and the editor's True render. Each test e-invoice carries the test mark, on every plan. Live renders are neither counted nor marked.
  • The count belongs to the organisation and starts again at midnight UTC. It is taken after every other check, so a request refused for another reason takes nothing; a render that fails afterwards has used its count. A batch takes one for each item that makes an e-invoice, all or nothing. A test render once the day is used up answers 403 forbidden with "feature": "einvoice", limit, used and resets_at.
  • On a plan without e-invoices, a live render that makes one answers 403 forbidden with "feature": "einvoice", for a template that declares an e-invoice too. A render never comes back as a plain PDF instead. A template that declares an e-invoice, such as a copy of the example, can be switched off in Page setup, or per request with post.einvoice: false, to render plain PDFs.
  • Units. An e-invoice costs 1 unit on top of the render's own units. A two-page invoice costs 2 units: 1 for the PDF, 1 for the e-invoice. Test renders, previews and true renders in the editor cost nothing, and neither does a failed render. See units.

Errors​

Before anything is rendered or counted, a request is checked in this order: the profile and the flavour, the output, the plan, a password, the _invoice block. The day's count of test e-invoices is taken last.

StatusCodeWhen
422einvoice_data_invalidThe _invoice block is missing or breaks a rule, answered before anything is rendered with every finding in errors (see How the block is checked); or the validator refused the finished file, and the problem carries the report in einvoice.
422einvoice_profile_unsupportedA profile or flavour Formfeed does not create: minimum, basic-wl, extended, xrechnung or an unknown value.
422einvoice_display_mismatchWith display_check: "strict", the PDF does not show a value of the XML.
500pdfa_conversion_failedThe PDF could not be made a valid PDF/A-3b. The findings are in einvoice, or the converter's message in reason. The known causes: a font that is not embedded, which an appended PDF can bring, and a font whose embedded program disagrees with the widths the PDF states, which the office converter writes for Chinese, Japanese and Korean text of a Word or PowerPoint template. A retry does not help, and async renders are not retried for it.
503conformance_unavailableThe service that converts and validates is busy or away, or e-invoices are not available in the region. Retry after Retry-After; async renders and batches retry it by themselves.
400invalid_requestAn e-invoice together with a password, on an output other than PDF, or with an option or a value that does not exist.
403forbiddenWith "feature": "einvoice". The plan has no e-invoices and the render is a live one, or the day's test e-invoices of the plan are used up.

The conversion can also end with two general codes: 422 output_too_large when the PDF is too large to be made an e-invoice, and 504 render_timeout when the conversion does not finish in time. An e-invoice that is a PDF/UA-1 file too can fail with pdfua_validation_failed as well.

Command line​

The CLI checks and writes e-invoices offline, with the code the API runs.

formfeed validate holds the _invoice block of every data set to the rules, for each template whose settings.json declares an e-invoice. A finding names the file, the line and the column, the field and the rule, and the command exits with 1:

error invoice/data/storno.json:63:7 _invoice.totals.due is 1000.00, but grand less prepaid plus rounding is 1142.40 (BR-CO-16) [einvoice-sum]
1 error(s)

A data set without the block and a profile that is not offered are errors too. On an image template the declaration is a warning, because such a template never makes an e-invoice.

formfeed einvoice <slug> writes the XML a render would embed, without an API key:

formfeed einvoice invoice --out out/rechnung.xml
invoice: ZUGFeRD 2.5, EN 16931, RE-2026-0001, 1 line(s), 1142.40 EUR -> out/rechnung.xml
checked offline against the rules the API applies before a render; the official validator runs with the render

--data takes the name of a data set or a JSON file, --out the file to write (<slug>.xml without it), and --profile wins over the template's settings. A template that declares nothing gets the defaults. A block that breaks a rule writes no file and lists what is wrong. The XML is checked by the rules the API applies before a render; the validator itself runs with the render, not here.

formfeed render <slug> sends the local template with what settings.json declares and the _invoice block of the data set, and prints the report of the render:

e-invoice ZUGFeRD, EN 16931, specification 2.5
valid rules of the XML (Factur-X 1.09.2 (Mustang 2.26.0)), PDF/A-3b
shown invoice number, net total, total
warn not in the text of the PDF: VAT total
rnd_7da09ab6cd9946b798c187c2a9350852: 1 page(s), 0 unit(s) -> invoice.pdf

A local Word or PowerPoint file renders as a plain PDF, with a warning: its e-invoice is made when the published template renders, which formfeed render --remote does.

What Formfeed does not do​

Formfeed creates and validates the file. It does not:

  • Send it. Formfeed is neither a Peppol access point nor a French plateforme agréée. You, your platform or your access point deliver the invoice.
  • Archive it for the period the law sets. See below.
  • Book it. Bookkeeping stays in your accounting system.
  • Decide the VAT. Which category, rate and exemption reason apply is yours, or your tax adviser's, to decide. Formfeed gives no tax advice.
  • Sign it. The file carries no electronic signature.
  • Create XRechnung or UBL. Neither is available. The profiles minimum, basic-wl and extended are not offered either.

You issue the invoice and answer for its content and its VAT treatment. Formfeed answers for the format.

Archiving and the report​

Formfeed is not an archive. Outputs are kept only for the retention period of your plan and workspace (1 day on Free, 30 days on Starter, 365 days on Pro; Business sets no limit, but a workspace setting or a deletion still ends it). Nothing in Formfeed keeps an invoice for the years the law asks of you, and nothing guards it against being deleted. Archive every invoice yourself:

  • Archive the delivered file itself, or the XML output of xml: both. The XML in the file is what counts. Never archive a print, which has no XML, or a PDF that another program saved again, which is no longer the file that was validated.
  • Keep the validation report beside it. It is the render's einvoice object, from the response, from GET /v1/renders/{id} or from Report (JSON) in the render log. It records which rules the file was held to and that it passed, on the day the invoice was issued.
  • Know what the report proves. Validation proves the format and the business rules, not the content. An invoice with a wrong VAT rate passes.

Bring your own storage copies every PDF, and the XML of xml: both, into your own bucket as it is rendered.