DynamicDocumentAPI

E-invoicing

Factur-X and ZUGFeRD PDFs with embedded EN 16931 XML, XRechnung export in CII or UBL, the invoice model, computed totals and validation reports.

View as Markdown

Dynamic Document API turns one invoice model into e-invoices that pass the official validators. Two outputs are available:

  • Hybrid PDFs (Factur-X, ZUGFeRD): your usual invoice PDF, converted to PDF/A-3b, with the invoice XML embedded.
  • Standalone XML (XRechnung, EN 16931): the XML on its own, in CII or UBL syntax.

Every file is validated before it is delivered, and the validation report comes back with the render.

OutputRequestFormat
Factur-X / ZUGFeRD PDFoutput.einvoice on any PDF renderPDF/A-3b with embedded UN/CEFACT CII XML (Factur-X 1.09, identical to ZUGFeRD 2.x)
XRechnung PDFoutput.einvoice with the profile XRECHNUNGPDF/A-3b with an embedded XRechnung 3.0 CII file, xrechnung.xml
XRechnung or EN 16931 XMLPOST /v1/einvoicesCII (UN/CEFACT D16B) or UBL 2.1

The XML follows the European semantic model EN 16931-1. It is checked with the KoSIT validator, which applies the XRechnung, EN 16931 and Factur-X rules. PDFs are checked with veraPDF. Neither validator is modified.

E-invoicing is included in the Growth, Pro, Scale and Enterprise plans. On Free and Starter, requests with output.einvoice and calls to POST /v1/einvoices fail with 402 plan_feature_unavailable, and a template's default e-invoice is skipped with a warning. See Plans and limits.

What we guarantee

Dynamic Document API guarantees technical format conformance, validated against the official schemas and schematron rules. You remain responsible for the content of your invoices, their tax correctness and their transmission, for example through a certified platform (PDP/PA) in France or the e-invoicing portals of German public authorities.

Profiles

A profile decides which information the XML carries and which rules apply. Pick the one your recipient asks for. EN16931 is the right default for B2B invoices in the EU, and XRECHNUNG is the standard for German public-sector buyers.

ProfileGuideline ID (BT-24)Invoice linesEmbedded fileAFRelationshipSyntaxes
MINIMUMurn:factur-x.eu:1p0:minimumnot transmittedfactur-x.xmlDataCII
BASIC_WLurn:factur-x.eu:1p0:basicwlnot transmittedfactur-x.xmlDataCII
BASICurn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basicbasic subsetfactur-x.xmlAlternativeCII
EN16931urn:cen.eu:en16931:2017completefactur-x.xmlAlternativeCII, UBL
EXTENDEDurn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extendedcompletefactur-x.xmlAlternativeCII
XRECHNUNGurn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0completexrechnung.xmlAlternativeCII, UBL
  • MINIMUM and BASIC_WL carry header data and totals only. They don't satisfy EN 16931, so German B2B rules don't accept them as e-invoices. Use them only where a booking aid is enough.
  • The PDF's XMP metadata names the profile in fx:ConformanceLevel, as the Factur-X specification requires (for example EN 16931 or BASIC WL).
  • UBL is available for standalone XML only. Hybrid PDFs always embed CII.

Create a Factur-X or ZUGFeRD PDF

Add output.einvoice to any PDF render: template, HTML, URL or Markdown. The page renders as usual. Then the engine:

  1. generates the XML from the invoice model and validates it
  2. converts the PDF to PDF/A-3b and embeds the XML as an attachment
  3. writes the Factur-X metadata and validates the finished PDF with veraPDF

The invoice model can sit in your template data. invoice_path points at it, so the template and the XML use the same numbers:

curl https://api-eu.dynamicdocumentapi.com/v1/renders \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: invoice-R-2026-0042" \
  -H "Content-Type: application/json" \
  -d '{
    "input": { "type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C" },
    "data": {
      "invoice": {
        "number": "R-2026-0042",
        "issue_date": "2026-09-23",
        "due_date": "2026-10-23",
        "currency": "EUR",
        "buyer_reference": "PO-4711",
        "seller": {
          "name": "Example GmbH",
          "vat_id": "DE123456789",
          "electronic_address": { "id": "invoices@example.com", "scheme": "EM" },
          "address": { "line1": "Beispielstraße 1", "city": "Berlin", "postcode": "10115", "country": "DE" }
        },
        "buyer": {
          "name": "Customer AG",
          "vat_id": "DE987654321",
          "electronic_address": { "id": "ap@customer.example", "scheme": "EM" },
          "address": { "line1": "Marktplatz 5", "city": "München", "postcode": "80331", "country": "DE" }
        },
        "payment": { "means_code": "58", "credit_transfers": [{ "account_id": "DE02120300000000202051" }] },
        "lines": [
          { "quantity": "10", "unit": "HUR", "price": { "net": "100.00" }, "tax": { "category": "S", "rate": "19" }, "item": { "name": "Consulting" } }
        ]
      }
    },
    "output": {
      "format": "pdf",
      "filename": "invoice-{{ data.invoice.number }}.pdf",
      "einvoice": { "profile": "EN16931", "invoice_path": "invoice" }
    }
  }'

The E-Rechnung template in the gallery is built for this. It reads the invoice model from invoice, prints the computed amounts and works with every profile.

FieldDescription
output.einvoice.profileRequired. One of the profiles
output.einvoice.invoiceThe invoice model itself
output.einvoice.invoice_pathA dotted path into data that holds the invoice model, such as "invoice" or "order.invoice". Send exactly one of invoice and invoice_path.
output.einvoice.syntaxOmit it, or send "cii". Hybrid PDFs always embed CII.

Rules checked before anything renders. Each is a 400 validation_error with a JSON Pointer to the field:

  • output.format must be pdf. An e-invoice PDF is always PDF/A-3b, so pdf.pdfa must be omitted or "3b".
  • pdf.protect can't be combined with output.einvoice, because PDF/A forbids encryption.
  • invoice_path must resolve to an object in data. It can't be combined with data_url, because the data isn't available when the request is checked; send invoice instead. (A template's default e-invoice works with data_url.)
  • The invoice is validated against the model's JSON Schema, for example /output/einvoice/invoice/lines/0/tax/category.
  • Editor previews reject output.einvoice. Test renders accept it; see test mode.

In batches, a literal invoice applies to every item. invoice_path is resolved in each item's own data, so every row can carry its own invoice.

When a render embeds an e-invoice, or its template has a default e-invoice, the engine computes the invoice before the template runs and passes the results to the template as render.invoice. Print these values instead of calculating totals in the template, and the visible invoice always matches the XML to the cent:

{% set e = render.invoice %}
<table>
  {% for line in invoice.lines %}
  <tr>
    <td>{{ line.item.name }}</td>
    <td>{{ line.quantity }}</td>
    <td>{{ e.lines[loop.index0].net_amount | format_currency(e.currency, "de-DE") }}</td>
  </tr>
  {% endfor %}
</table>
{% for vat in e.vat_breakdown %}
<p>VAT {{ vat.rate }} % on {{ vat.basis | format_currency(e.currency, "de-DE") }}: {{ vat.tax | format_currency(e.currency, "de-DE") }}</p>
{% endfor %}
<p><strong>Total due: {{ e.totals.due_payable | format_currency(e.currency, "de-DE") }}</strong></p>
FieldDescription
number, currencyFrom the invoice
lines[].id, lines[].net_amountLine ID and line net amount (BT-131), in the order of invoice.lines
vat_breakdown[]One entry per VAT category and rate: category, rate, basis, tax, exemption_reason, exemption_reason_code
totalsline_total, allowance_total, charge_total, tax_basis_total, tax_total, grand_total, prepaid, rounding, due_payable

Amounts are strings with two decimals, such as "1190.00", which format_currency accepts.

render.invoice is there in every render that embeds an e-invoice (output.einvoice, or the template's default), and in every render of a template with a default e-invoice that doesn't embed it: the opt-out below, plans without e-invoicing, image output, editor and dashboard previews. The computation is free, so the template needs no fallback and no arithmetic of its own.

render.einvoice holds the same values plus profile, but only when the XML is embedded. Use it to tell the two apart, for example for a note on the page:

{% if render.einvoice %}<p>This PDF carries an embedded {{ render.einvoice.profile }} e-invoice.</p>{% endif %}

An invoice that can't be computed fails an e-invoice render with einvoice_invalid before the template runs. Where nothing is embedded, it never fails the render: render.invoice is left out and the render carries the warning invoice_totals_unavailable, whose message names the first problem.

Make a template an e-invoice by default

A PDF template (code or Markdown) can store the e-invoice in its default options. Every render of it is then a Factur-X / ZUGFeRD PDF, and callers only send the data:

{
  "input": { "type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C" },
  "data": { "invoice": { "number": "R-2026-0042", "issue_date": "2026-09-23", "currency": "EUR", "…": "…" } }
}

Turn it on in the dashboard under the template's Options → E-invoice (ZUGFeRD / Factur-X): pick the profile (EN16931 by default) and the data path (invoice by default). Through the API, it is einvoice in the template's options, next to output_format and pdf:

"options": {
  "output_format": "pdf",
  "pdf": { "paper": { "size": "A4" } },
  "einvoice": { "profile": "EN16931", "invoice_path": "invoice" }
}

A template default holds a profile and a data path, never an invoice. It is checked when the template is saved: profile must be one of the profiles, invoice_path a dotted data path, output_format must be pdf, and pdf.protect isn't allowed with it. A default pdf.pdfa of "2b" is raised to "3b" at render time. Canvas templates can't store an e-invoice; send output.einvoice with the request instead. Every invoice template in the gallery ships with {"profile": "EN16931", "invoice_path": "invoice"}, and "Use this template" keeps it.

What a request sends decides:

RequestResult
No output.einvoiceThe template's default applies, with the same checks as output.einvoice
output.einvoice with an objectReplaces the default completely, for example another profile or path
"output": { "einvoice": null }Turns the default off: a plain PDF ("einvoice": null in the convenience endpoints)

The same applies to batches (POST /v1/batches): the default's invoice_path is resolved in each item's data, an item without an invoice object there rejects the batch with 400 validation_error at /items/<i>/data/<path> (/csv/row/<n>/data/<path> for CSV rows, whose text cells can't hold the invoice's lines, so send JSON items), and "output": {"einvoice": null} turns the default off for every item. In the dashboard's batch wizard, that is the Embed the e-invoice checkbox on the review step. Other differences from a request-level output.einvoice:

  • Missing invoice. If the path doesn't lead to an object in the request's data, the request fails with 400 validation_error at /data, and the message names the opt-out. Model errors point into data, for example /data/invoice/seller/name.
  • data_url works. The data isn't available when the request is checked, so the renderer resolves the path. If it finds no invoice there, the render fails with einvoice_invalid.
  • Plans without e-invoicing. On Free and Starter the default is skipped: the render is a normal PDF and carries the warning einvoice_skipped_plan. A request that sends output.einvoice itself still fails with 402 plan_feature_unavailable.
  • Other formats. The default only applies to PDF output: rendering such a template as PNG, JPG or WebP gives a plain image without an e-invoice.
  • Previews. Editor previews and the publish test render ignore the default and stay plain PDFs.

In all of these cases without an embedded e-invoice, the template still gets the computed amounts as render.invoice, at no cost.

Export XRechnung or EN 16931 XML

POST /v1/einvoices returns the XML on its own, without a PDF. It creates a regular render with one application/xml file, and accepts the usual mode, delivery, webhook, reference, metadata and test fields and the Idempotency-Key header:

curl https://api-eu.dynamicdocumentapi.com/v1/einvoices \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: xrechnung-XR-2026-0007" \
  -H "Content-Type: application/json" \
  -d @xrechnung-request.json \
  -o XR-2026-0007.xml

xrechnung-request.json holds the profile, the syntax and the invoice, for example the one in The invoice model (shortened here):

{
  "profile": "XRECHNUNG",
  "syntax": "ubl",
  "filename": "XR-2026-0007.xml",
  "invoice": { "number": "XR-2026-0007", "issue_date": "2026-09-23", "currency": "EUR", "seller": {}, "buyer": {}, "lines": [] },
  "delivery": { "type": "binary" }
}
FieldDescription
profileRequired. One of the profiles
syntaxcii (default) or ubl. UBL is available for EN16931 and XRECHNUNG only.
invoiceRequired. The invoice model
filenameDefaults to the invoice number, cleaned up for use as a file name, plus .xml

With delivery.type: "binary" the response body is the XML file itself. Otherwise you get the render object: its input_type is einvoice, its output_format is xml, and it carries the validation report in result. In UBL, credit notes (type code 381) use the CreditNote root element and everything else uses Invoice.

The invoice model

The invoice model is JSON for the EN 16931 semantic model, with snake_case names. Every field corresponds to one business term (BT) or business group (BG) of the standard, and the API reference lists all of them. This complete example is a valid XRechnung for a German federal authority:

{
  "number": "XR-2026-0007",
  "issue_date": "2026-09-23",
  "due_date": "2026-10-23",
  "currency": "EUR",
  "buyer_reference": "04011000-12345-34",
  "payment_terms": "Zahlbar innerhalb von 30 Tagen ohne Abzug.",
  "invoice_period": { "start": "2026-09-01", "end": "2026-09-30" },
  "seller": {
    "name": "Größenwahn GmbH",
    "vat_id": "DE123456789",
    "tax_number": "30/123/45678",
    "legal_registration": { "id": "HRB 12345" },
    "electronic_address": { "id": "rechnung@groessenwahn.example", "scheme": "EM" },
    "address": { "line1": "Beispielstraße 1", "city": "Berlin", "postcode": "10115", "country": "DE" },
    "contact": { "name": "Erika Muster", "phone": "+49 30 1234567", "email": "erika.muster@groessenwahn.example" }
  },
  "buyer": {
    "name": "Bundesamt für Beispiele",
    "electronic_address": { "id": "04011000-12345-34", "scheme": "0204" },
    "address": { "line1": "Amtsweg 2", "city": "Bonn", "postcode": "53113", "country": "DE" }
  },
  "payment": {
    "means_code": "58",
    "remittance_information": "XR-2026-0007",
    "credit_transfers": [{ "account_id": "DE02120300000000202051", "account_name": "Größenwahn GmbH" }]
  },
  "lines": [
    {
      "quantity": "8",
      "unit": "HUR",
      "price": { "net": "95.00" },
      "tax": { "category": "S", "rate": "19" },
      "item": { "name": "Softwarewartung", "description": "Wartung Fachverfahren, September 2026" }
    },
    {
      "quantity": "1",
      "unit": "C62",
      "price": { "net": "250.00" },
      "tax": { "category": "S", "rate": "19" },
      "item": { "name": "Lizenz Modul Statistik" },
      "allowances": [{ "amount": "25.00", "reason": "Rabatt" }]
    }
  ]
}

Conventions

TopicConvention
AmountsDecimal strings such as "95.00", with at most two decimals. JSON numbers are accepted, but strings avoid floating-point surprises.
Quantities and pricesUp to six decimals, for example "0.125"
DatesYYYY-MM-DD
CodesCurrencies in ISO 4217 (EUR), countries in ISO 3166-1 alpha-2 (DE), units from UN/ECE Recommendation 20 (C62 one, H87 piece, HUR hour, DAY day, KGM kilogram). unit defaults to C62.
Electronic addressesid plus an EAS scheme, such as EM for an e-mail address, 0204 for a German Leitweg-ID or 9930 for a German VAT ID
Document typetype_code (BT-3) defaults to 380, a commercial invoice. Others include 381 credit note, 384 corrected invoice, 389 self-billed invoice, 326 partial invoice and 386 prepayment invoice.

Required fields

Every invoice needs number, issue_date, currency, seller (with name and address), buyer (with name and address) and at least one line. Each line needs quantity, price.net, tax.category and item.name, and a tax.rate unless its category is O. MINIMUM and BASIC_WL don't transmit lines, but they still need them to compute the totals.

The profiles add their own rules. For XRECHNUNG:

  • buyer_reference (BT-10), the Leitweg-ID for German public buyers
  • seller.contact with name, phone and email
  • electronic_address for both the seller and the buyer
  • payment with a payment means code, for example 58 for a SEPA credit transfer, and the account

process_id (BT-23) defaults to urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 for XRECHNUNG.

VAT categories

CategoryMeaningRateAlso required
SStandard ratethe rate, such as 19—
ZZero rated goods0—
EExempt from VAT0vat_exemptions.E
AEReverse charge0vat_exemptions.AE, seller and buyer VAT IDs
KIntra-community supply0vat_exemptions.K, seller and buyer VAT IDs, delivery information
GExport outside the EU0vat_exemptions.G
ONot subject to VATnonevat_exemptions.O
LCanary Islands IGICthe IGIC rate—
MCeuta and Melilla IPSIthe IPSI rate—

vat_exemptions gives the exemption reason (BT-120) and code (BT-121) for each category. This is a reverse-charge supply to a customer in another EU country:

{
  "vat_exemptions": { "AE": { "reason": "Reverse charge", "reason_code": "vatex-eu-ae" } },
  "charges": [{ "amount": "15.00", "tax_category": "AE", "tax_rate": "0", "reason": "Versand", "reason_code": "FC" }],
  "lines": [
    { "quantity": "12", "unit": "H87", "price": { "net": "49.50" }, "tax": { "category": "AE", "rate": "0" }, "item": { "name": "Druckkopf X2" } }
  ]
}

Document-level allowances and charges need their own tax_category and tax_rate, because they change the VAT basis of that category.

Credit notes

Set type_code to "381" and reference the original invoice in preceding_invoices. Quantities and prices stay positive, because the type code is what makes the document a credit:

{
  "number": "GS-2026-0003",
  "type_code": "381",
  "issue_date": "2026-09-23",
  "preceding_invoices": [{ "number": "R-2026-0042", "issue_date": "2026-09-23" }]
}

Computed fields

Don't send line totals, the VAT breakdown or document totals. The engine computes them from the lines, allowances and charges, using the EN 16931 rules:

ValueBusiness termsRule
Line net amountBT-131quantity × net price ÷ base quantity + line charges − line allowances
VAT per category and rateBT-116, BT-117basis = the lines, allowances and charges of that category and rate; tax = basis × rate ÷ 100, rounded to two decimals
Sum of line net amountsBT-106sum of the line net amounts
Total without VATBT-109lines − document allowances + document charges
Total VATBT-110sum of the VAT amounts per category
Total with VATBT-112total without VAT + total VAT
Amount dueBT-115total with VAT − prepaid_amount + rounding_amount

For the example above: 8 × 95.00 = 760.00 and 250.00 − 25.00 = 225.00, so the lines total 985.00. VAT at 19 % on 985.00 is 187.15, and 1,172.15 is due.

A line may carry its own net_amount. It isn't needed, but when present the engine checks it against the computed value and rejects the invoice if they differ.

Validation report

PDF/A and e-invoice renders carry a conformance report in result.conformance. Succeeded renders have one, and so do renders that failed validation:

{
  "result": {
    "conformance": {
      "pdfa": {
        "level": "3b",
        "status": "passed",
        "validator": "veraPDF 1.30.2",
        "profile": "PDF/A-3B validation profile",
        "errors": [],
        "warnings": []
      },
      "einvoice": {
        "profile": "XRECHNUNG",
        "syntax": "cii",
        "status": "passed",
        "validator": "KoSIT Validator 1.6.3",
        "scenario": "EN16931 XRechnung (CII)",
        "errors": [],
        "warnings": []
      }
    }
  }
}

A render that failed because the invoice lacks a field the profile requires carries the same object with status: "failed", and its findings point at the invoice:

{
  "rule": "BR-DE-2",
  "message": "The seller contact (BG-6) is required for XRECHNUNG.",
  "location": "invoice.seller.contact",
  "count": 1
}
FieldDescription
statuspassed, failed or unavailable. unavailable means the validator couldn't be reached in time and the file was delivered unvalidated.
levelPDF/A level that was checked, 2b or 3b
profile, syntaxThe e-invoice profile and syntax that were generated
validator, scenarioThe validator, its version and the rule set it applied
errors, warningsFindings, each with rule (such as BR-DE-15 or a PDF/A clause like 6.2.4.3-2), message, location and count

Each list holds at most 100 findings. Findings with the same rule and location are merged, and count says how often they occurred. For problems found in the invoice model, location is a JSON path such as invoice.seller.contact.email. For problems in the generated file, it is the validator's XPath or veraPDF's object path. The dashboard shows the same report on the render's page.

Errors and warnings

A failed e-invoice render isn't billed. Synchronous requests answer 422 with the error, and the report is in the render's result:

CodeWhenFix
einvoice_invalidThe invoice breaks a rule of the profile before any XML is written, for example a missing BT the profile requires, a VAT category without a rate or a net_amount that doesn't add upresult.conformance.einvoice.errors lists every problem with a JSON path into the invoice
einvoice_validation_failedThe validator rejected the generated XMLRead the rules in result.conformance.einvoice.errors; they are usually about content, such as a missing Leitweg-ID or an invalid code
pdfa_conversion_failedThe PDF couldn't be made PDF/A conformant, or veraPDF found it non-compliantSee PDF/A; fonts that aren't embedded are the usual cause

Warnings don't fail the render:

CodeMeaning
einvoice_validation_warningThe validator accepted the XML with warnings; the message names the rules
einvoice_validation_unavailableThe e-invoice validator didn't answer in time, so the XML is unvalidated
pdfa_validation_unavailableveraPDF didn't answer in time, so the PDF/A file is unvalidated
einvoice_skipped_planThe template embeds an e-invoice by default, but the plan doesn't include e-invoicing, so the PDF has none
invoice_totals_unavailableThe render embeds no e-invoice and its invoice couldn't be computed, so render.invoice is missing; the message names the first problem, such as a missing invoice in data

See Errors for every other code.

Test mode

Test renders are free and validated like live renders, so you can build and check your integration with a test key. Test PDFs carry the TEST watermark. It is drawn as vector outlines and stamped before the PDF/A conversion, so a test PDF is conformant too and the report describes the file you receive.

An XML file can't carry a watermark, so test XML carries a note instead. This applies to POST /v1/einvoices and to the XML embedded in a test PDF, the part that systems receiving the invoice read. The note is the first invoice note (BT-22):

TESTRECHNUNG – keine gültige Rechnung. TEST INVOICE – not a valid invoice (created with a test API key).
  • Every profile gets the note except MINIMUM, which has no field for it. A MINIMUM XML isn't an e-invoice on its own.
  • EXTENDED also sets TestIndicator to true, the test flag that only its schema defines.
  • Apart from the marker, the XML is the one a live key produces, and the validation report is the same.

Use a live key for invoices you send.

Billed renders

OperationBilled renders
E-invoice layer on a PDF renderincluded (+0), only the PDF itself is billed
XML export with POST /v1/einvoices1
Test renders and failed renders0

A two-page Factur-X invoice is 1 render. E-invoicing needs the Growth plan or higher. See Plans and limits.

  • PDF options for PDF/A without an e-invoice, and for converting existing PDFs
  • Renders for the render object and its result
  • Template language for format_currency and the other filters

On this page