StabisoDocs

REST API

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_KEY is 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 the draft scope, reading read, amending a final return approve. 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

Stabiso, the Enterprise System of Record · generated from the code and its guides · release 6f03a5e846b3