File German VAT (UStVA) and the Zusammenfassende Meldung
A German company reports VAT to its tax office with the monthly or quarterly advance return (Umsatzsteuer-Voranmeldung, UStVA) and reports its supplies to other EU businesses to the Federal Central Tax Office (BZSt) in the Zusammenfassende Meldung (ZM). This page computes both for August 2026 for Demo GmbH, reads the Kennzahlen, downloads the files, finalises the UStVA (which books the settlement entry) and then corrects a final return. It is meant for the accountant who prepares the returns and the second person who files them: the ERP does not let one person do both.
Before you start
- Three kinds of key take part.
$ERP_KEYis the first key of the demo organization (a human key with every scope). The system issued it, so it belongs to no person: it prepares the returns (computes and adjusts them) but can never finalise one.$ALICE_KEY(every scope) and$BOB_KEY(read,draft,post,approve) are keys of two people. Computing needs thedraftscope, readingread, amending a final returnapprove. See getting started for where the keys of people come from and conventions for who counts as who. The Swiss return of the same ERP is the subject of File a Swiss VAT return; the lifecycle (draft, final, amended) is the same. - The entity is
DE01(Demo GmbH, EUR). It files the UStVA monthly with the standard method and the ZM monthly. August 2026 holds a domestic invoice and rent bill with 19% VAT, an intra-Community supply of goods to France, services to the Netherlands, goods bought in the Netherlands and cloud services bought in Ireland. January to August are closed, September is open.
1. Read the VAT settings
curl "$ERP_URL/v1/entities/DE01/vat-settings" \
-H "Authorization: Bearer $ERP_KEY"
{
"entity": "DE01",
"country": "DE",
"vat_number": "DE811569869",
"configured": true,
"method": "standard",
"period": "month",
"recapitulative_period": "month",
"tax_number": "143/815/08150",
"tax_fx": "monthly",
"tax_fx_booking": false
}
recapitulative_period is the ZM period. tax_number is the Steuernummer printed on the forms.
tax_fx: monthly means that VAT on foreign-currency documents is converted at the monthly rate of
the Federal Ministry of Finance (the demo has none).
2. List the return periods
For a year the list holds the UStVA periods (kind: vat_return) and, because a ZM period is set,
the ZM periods (kind: recapitulative_statement). A period without a return has the status
open; this is not the state of the accounting period.
curl "$ERP_URL/v1/entities/DE01/vat-periods?year=2026" \
-H "Authorization: Bearer $ERP_KEY"
The answer has 24 rows, twelve per kind. Two of them:
[
{"period": "2026-08", "start": "2026-08-01", "end": "2026-08-31", "kind": "vat_return",
"return_id": null, "reference": null, "status": "open", "payable": null},
{"period": "2026-08", "start": "2026-08-01", "end": "2026-08-31", "kind": "recapitulative_statement",
"return_id": null, "reference": null, "status": "open", "payable": null}
]
3. Compute the UStVA
The period is 2026-08. The computation reads the posted, tax-tagged lines of the month and
routes each to a Kennzahl of the 2026 form (Vordruckmuster USt 1 A). Bases are in whole euros
(cents are dropped), the tax is computed from the base with cents. Nothing is booked.
Idempotency-Key is required.
curl -X POST "$ERP_URL/v1/vat-returns" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-de-ustva-2026-08" \
-H "Content-Type: application/json" \
-d '{"entity": "DE01", "period": "2026-08"}'
{
"id": "<uuid>",
"reference": "DE01-VAT-2026-08",
"kind": "vat_return",
"status": "draft",
"period": "2026-08",
"payable": "570.00",
"errors": 0,
"warnings": 1,
"result": {
"form": {"id": "de.bmf.ustva.2026", "currency": "EUR"},
"payable": "570.00",
"issues": [{"severity": "warning", "code": "earlier_period_unfiled"}],
"reconciliation": {
"ledger": "570.00",
"reported": "570.00",
"computation_difference": "0.00",
"unexplained": "0.00",
"settlement": [
{"account": "1400", "amount": "-684.00"},
{"account": "2200", "amount": "950.00"},
{"account": "2210", "amount": "304.00"},
{"account": "2220", "amount": "-570.00"}
]
}
}
}
The one warning, earlier_period_unfiled, names the months of the year before August that have no
final return yet (details.periods): the demo files only August. Filing out of order is allowed;
the warning is there so the earlier months are not forgotten.
result.boxes has every box of the form with its Kennzahl (code), its line on the form (line),
the amount, the tax where the form computes tax from a base, and how many ledger lines fill it
(lines). The boxes with an amount in this demo:
| Kz. | Zeile | Meaning | Base (EUR) | Tax (EUR) |
|---|---|---|---|---|
| 81 | 13 | Taxable supplies at 19% (invoice to the Hamburg customer) | 5,000 | 950.00 |
| 41 | 19 | Intra-Community supplies of goods (France) | 3,000 | |
| 89 | 25 | Intra-Community acquisitions at 19% (goods from the Netherlands) | 1,200 | 228.00 |
| 46, 47 | 30 | Services from an EU supplier, tax owed by the recipient (Ireland) | 400 | 76.00 |
| 21 | 35 | Non-taxable other services, § 18b (services to the Netherlands) | 1,500 | |
| 37 | Output VAT (lines 13 to 18 and 25 to 32) | 1,254.00 | ||
| 66 | 38 | Input tax from invoices of other businesses (rent) | 380.00 | |
| 61 | 39 | Input tax from intra-Community acquisitions | 228.00 | |
| 67 | 41 | Input tax from § 13b services | 76.00 | |
| 83 | 50 | Remaining prepayment | 570.00 |
reconciliation compares the VAT accounts with the return: ledger (what the accounts moved)
equals reported (what the return reports) and nothing is unexplained. settlement is the entry
that finalising will book, as signed debits: 2200 and 2210 are cleared, the input tax account 1400
is cleared, and 2220 (liability to the tax office) takes 570.00.
4. See the lines behind a Kennzahl
The id of a box in the path is the Kennzahl id (kz89) or, for the form's computed lines, line37.
curl "$ERP_URL/v1/vat-returns/$USTVA_ID/boxes/kz89/lines" \
-H "Authorization: Bearer $ERP_KEY"
[
{
"entry_date": "2026-08-12",
"account": "5000",
"description": "Sensoren",
"document": "<any>",
"counterparty": "Eindhoven Components B.V.",
"rule": "acquisition_19",
"amount": "1200.00"
}
]
5. Compute the Zusammenfassende Meldung
The ZM is the same endpoint with kind: recapitulative_statement. It has no amount payable and no
reconciliation; its result is rows, one per customer VAT number and kind of supply (L goods,
S services, D triangular transactions). A customer without a VAT number would show as an error
(missing_vat_number), because the ZM needs it.
curl -X POST "$ERP_URL/v1/vat-returns" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-de-zm-2026-08" \
-H "Content-Type: application/json" \
-d '{"entity": "DE01", "kind": "recapitulative_statement", "period": "2026-08"}'
{
"reference": "DE01-ZM-2026-08",
"kind": "recapitulative_statement",
"status": "draft",
"payable": null,
"errors": 0,
"result": {
"form": {"id": "de.bzst.zm"},
"rows": [
{"country_code": "FR", "vat_id": "40303265045", "party": "Paris Distribution SARL", "box": "L", "amount": "3000.00"},
{"country_code": "NL", "vat_id": "859048451B01", "party": "Rotterdam Logistics B.V.", "box": "S", "amount": "1500.00"}
],
"reconciliation": null
}
}
6. Download the files
The ZM is exported in the CSV format of the import of the BZStOnline-Portal. csv_version names
the version of the import help (the default is the current one).
curl "$ERP_URL/v1/vat-returns/$ZM_ID/file?format=csv" \
-H "Authorization: Bearer $ERP_KEY"
#v1.0
Laenderkennzeichen,USt-IdNr.,Betrag(EUR),Art der Leistung
FR,40303265045,3000,L
NL,859048451B01,1500,S
The UStVA has a printable form, html or pdf, marked as a draft until the return is final, and
a json file (the return as GET /v1/vat-returns/{id} gives it). The ELSTER transmission itself
is not part of the ERP: the printable form shows what to enter.
curl "$ERP_URL/v1/vat-returns/$USTVA_ID/file?format=html" \
-H "Authorization: Bearer $ERP_KEY"
<h1>Umsatzsteuer-Voranmeldung 2026</h1>
<p class="draft">Entwurf, nicht eingereicht</p>
<dt>Referenz</dt><dd>DE01-VAT-2026-08</dd>
7. Finalise the UStVA
Finalising recomputes the return, books the settlement entry through the approval policy and, once
the entry is posted, locks VAT postings in the period. It is done by a human (policy action
finalize_vat_return) who is a known person (not the first key) and who did not prepare the
return: whoever created it or computed it last does not finalise it (policy action
finalize_own_vat_return, denied by default). The first key prepared both returns, so Alice
files them. The Swiss page shows the refusals in detail
(step 8). The settlement is dated the last day of the period, or the first open day
after it when the period is closed. August is closed, so the entry is dated 1 September: a closed
period takes no postings.
curl -X POST "$ERP_URL/v1/vat-returns/$USTVA_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-de-ustva-finalize" \
-H "Content-Type: application/json" \
-d '{}'
{
"vat_return": {"reference": "DE01-VAT-2026-08", "status": "final", "payable": "570.00"},
"settlement": {"status": "posted", "policy": {"decision": "allow", "rule_id": "humans_allowed"}}
}
curl "$ERP_URL/v1/journal-entries/$SETTLEMENT_ID" \
-H "Authorization: Bearer $ERP_KEY"
{
"status": "posted",
"source": "vat",
"source_ref": "DE01-VAT-2026-08",
"entry_date": "2026-09-01"
}
Final is not filed: send the UStVA through ELSTER yourself (the application files nothing
electronically), then record the day and the transfer ticket with POST /v1/vat-returns/{return_id}/submitted (submitted_on, submission_reference). Only from then on
is the month filed in the periods list, and the to-do counts and the close stop reminding of it.
Once the bank debit to the Finanzamt is matched to the settlement account, link that posted entry
with POST /v1/vat-returns/{return_id}/payments: the return reads paid when the links cover the
settled amount. The Swiss page shows both steps.
8. Finalise the ZM
A ZM has nothing to settle, so it becomes final at once and settlement is null.
curl -X POST "$ERP_URL/v1/vat-returns/$ZM_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-de-zm-finalize" \
-H "Content-Type: application/json" \
-d '{}'
{"vat_return": {"reference": "DE01-ZM-2026-08", "status": "final"}, "settlement": null}
After uploading the ZM in the BZStOnline-Portal, record its submission the same way. With nothing
to pay, it stays submitted: there is no payment to link.
9. Correct a final UStVA
amend opens a correction: the original becomes amending and a new draft
(DE01-VAT-2026-08/1, its amends_id names the original) is computed from the current ledger. It
needs the approve scope and the policy action finalize_vat_return. Bob opens it; the correction
is then his draft, and Alice files it. August is closed, so the
ledger cannot change; this correction brings an amount the ledger does not hold: import VAT
(Kz. 62, Einfuhrumsatzsteuer) of 50.00 that was paid to customs. To correct the bookings themselves,
reopen the period first (see Close a period).
curl -X POST "$ERP_URL/v1/vat-returns/$USTVA_ID/amend" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-de-amend"
{
"reference": "DE01-VAT-2026-08/1",
"status": "draft",
"amends_id": "<uuid>",
"payable": "570.00"
}
The adjustment needs an offset account, the account that takes the other side of the amount in the settlement. Control accounts (bank, receivables, payables) are refused when the settlement is booked, because they are posted through their subledger. Nothing is checked about that while computing, so the 422 comes at finalising:
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/recompute" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-de-adjust-bank" \
-H "Content-Type: application/json" \
-d '{"adjustments": [{"box": "kz62", "amount": "50.00", "account": "1010", "note": "Einfuhrumsatzsteuer"}]}'
{"status": "draft", "payable": "520.00"}
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-de-correction-finalize-bank" \
-H "Content-Type: application/json" \
-d '{}'
{"error": {"code": "control_account_manual"}}
Use the account for the liability to customs instead (2300, accrued liabilities). The new adjustments replace the old ones:
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/recompute" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-de-adjust-2300" \
-H "Content-Type: application/json" \
-d '{"adjustments": [{"box": "kz62", "amount": "50.00", "account": "2300", "note": "Einfuhrumsatzsteuer"}]}'
{
"status": "draft",
"payable": "520.00",
"result": {
"adjustments": [{"box": "kz62", "amount": "50.00", "account": "2300", "note": "Einfuhrumsatzsteuer"}],
"reconciliation": {"adjustments": "-50.00", "unexplained": "0.00"}
}
}
Finalising the correction books only the difference to the original settlement and marks the
original superseded.
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-de-correction-finalize" \
-H "Content-Type: application/json" \
-d '{}'
{
"vat_return": {"reference": "DE01-VAT-2026-08/1", "status": "final", "payable": "520.00"},
"settlement": {"status": "posted"}
}
curl "$ERP_URL/v1/journal-entries/$CORRECTION_ENTRY_ID" \
-H "Authorization: Bearer $ERP_KEY"
{
"source": "vat",
"source_ref": "DE01-VAT-2026-08/1",
"entry_date": "2026-09-01",
"lines": [
{"line_no": 1, "debit": "50.00", "credit": "0.00"},
{"line_no": 2, "debit": "0.00", "credit": "50.00"}
]
}
curl "$ERP_URL/v1/vat-returns?entity=DE01&kind=vat_return" \
-H "Authorization: Bearer $ERP_KEY"
[
{"reference": "DE01-VAT-2026-08/1", "status": "final", "payable": "520.00"},
{"reference": "DE01-VAT-2026-08", "status": "superseded", "payable": "570.00"}
]
What can go wrong
The files fit the form: the eCH-0217 XML is for Swiss returns, and the ZM CSV only for a ZM.
curl "$ERP_URL/v1/vat-returns/$USTVA_ID/file?format=xml" \
-H "Authorization: Bearer $ERP_KEY"
{"error": {"code": "validation_failed"}}
curl "$ERP_URL/v1/vat-returns/$USTVA_ID/file?format=csv" \
-H "Authorization: Bearer $ERP_KEY"
{"error": {"code": "validation_failed"}}
The organization's first key prepares returns but never files them. Even for a return that is already final, the question who is asking comes first:
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/finalize" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-de-finalize-system-key" \
-H "Content-Type: application/json" \
-d '{}'
{"error": {"code": "forbidden"}}
Only a draft is finalised or recomputed. A final return is corrected with amend; a return that
is not final (a draft, or one already superseded) cannot be amended.
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-de-finalize-again" \
-H "Content-Type: application/json" \
-d '{}'
{"error": {"code": "vat_return_not_draft"}}
curl -X POST "$ERP_URL/v1/vat-returns/$USTVA_ID/amend" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-de-amend-superseded"
{"error": {"code": "vat_return_not_final"}}
A period that already has a final return cannot get a second one: compute asks for an amendment.
curl -X POST "$ERP_URL/v1/vat-returns" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-de-ustva-again" \
-H "Content-Type: application/json" \
-d '{"entity": "DE01", "period": "2026-08"}'
{"error": {"code": "vat_return_blocks"}}
Reference
GET /v1/entities/{entity}/vat-settingsGET /v1/entities/{entity}/vat-periodsPOST /v1/vat-returnsGET /v1/vat-returnsGET /v1/vat-returns/{return_id}/boxes/{box}/linesGET /v1/vat-returns/{return_id}/filePOST /v1/vat-returns/{return_id}/recomputePOST /v1/vat-returns/{return_id}/finalizePOST /v1/vat-returns/{return_id}/amendGET /v1/journal-entries/{entry_id}