File a Swiss VAT return (quarter)
A Swiss company declares VAT to the Federal Tax Administration (ESTV) once a quarter. This page computes the third-quarter return of Demo AG from the ledger, checks it against the VAT accounts, adjusts a box by hand, downloads the eCH-0217 file for the ESTV portal, finalises the return (which books the settlement entry and locks the period), records its submission and payment, and then corrects a final return. It is meant for the person who prepares the return and for the second person who files it: 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 may prepare a return (compute, adjust, recompute) but can never finalise one.$ALICE_KEY(every scope) and$BOB_KEY(read,draft,post,approve) are keys of two people, users who signed in and created a key for themselves. The page also creates an agent key to show what an agent may not do. Getting started explains where such keys come from and conventions who counts as who. - Computing a return needs the
draftscope, reading needsread, amending a final return needsapprove. Who may finalise is a question of people, not only of scopes: see step 8. - The entity is
CH01(Demo AG, CHF), which files by the effective method per quarter. Its invoices and bills of August 2026 are posted; January to August are closed, September is open. The third quarter (July to September) is not over, so the return works on the ledger as it is today: compute it again before you file. - Foreign-currency documents are converted at the official VAT exchange rate (the monthly average the BAZG publishes), not at the ECB rate the ledger uses. The demo data has the rates for the quarter.
1. Read the VAT settings
The settings say how the entity files: the method, the period and the settlement account. A Swiss
entity that never saved settings gets the defaults (effective method, quarter); configured tells
which case applies.
curl "$ERP_URL/v1/entities/CH01/vat-settings" \
-H "Authorization: Bearer $ERP_KEY"
{
"entity": "CH01",
"country": "CH",
"vat_number": "CHE-123.456.788 MWST",
"configured": true,
"method": "effective",
"period": "quarter",
"settlement_account": null,
"tax_fx": "monthly",
"tax_fx_booking": false
}
tax_fx: monthly is the exchange rate method: the ESTV's monthly average. settlement_account
is null, so the settlement uses the account with the role vat_settlement (2202).
2. List the return periods
The periods of a year with the status of their returns. open here means that no return has been
computed yet; it has nothing to do with whether the accounting period is open or closed.
curl "$ERP_URL/v1/entities/CH01/vat-periods?year=2026" \
-H "Authorization: Bearer $ERP_KEY"
[
{"period": "2026-Q1", "start": "2026-01-01", "end": "2026-03-31", "status": "open", "return_id": null},
{"period": "2026-Q2", "start": "2026-04-01", "end": "2026-06-30", "status": "open", "return_id": null},
{"period": "2026-Q3", "start": "2026-07-01", "end": "2026-09-30", "status": "open", "return_id": null},
{"period": "2026-Q4", "start": "2026-10-01", "end": "2026-12-31", "status": "open", "return_id": null}
]
3. Compute the draft return
The period is 2026-Q3 (a month, a quarter, a half year or a year can be asked for). The
computation reads every posted, tax-tagged journal line of the period, routes each one to a box
of the official form and sums the boxes. Nothing is booked. Idempotency-Key is required. The
first key prepares the return here; whoever creates a return or computes it last is its preparer,
and the preparer does not finalise it (step 8).
curl -X POST "$ERP_URL/v1/vat-returns" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-q3" \
-H "Content-Type: application/json" \
-d '{"entity": "CH01", "period": "2026-Q3"}'
{
"id": "<uuid>",
"reference": "CH01-VAT-2026-Q3",
"kind": "vat_return",
"status": "draft",
"period": "2026-Q3",
"start": "2026-07-01",
"end": "2026-09-30",
"payable": "680.40",
"errors": 0,
"warnings": 1,
"result": {
"form": {"id": "ch.estv.mwst.effective"},
"payable": "680.40",
"boxes": [
{"id": "200", "amount": "36717.17", "lines": 5},
{"id": "205", "amount": "0.00"},
{"id": "220", "amount": "0.00"},
{"id": "221", "amount": "24717.17", "lines": 4},
{"id": "225", "amount": "0.00"},
{"id": "230", "amount": "0.00"},
{"id": "235", "amount": "0.00"},
{"id": "280", "amount": "0.00"},
{"id": "289", "amount": "24717.17", "computed": true},
{"id": "299", "amount": "12000.00", "computed": true},
{"id": "303", "amount": "12000.00", "tax": "972.00", "lines": 1},
"..."
],
"issues": [
{"severity": "warning", "code": "earlier_period_unfiled", "details": {"periods": "2026-Q1, 2026-Q2"}}
],
"tax_fx": [
{"series": "ch-vat-monthly", "currency": "EUR", "period": "2026-08", "rate": "0.9439"},
{"series": "ch-vat-monthly", "currency": "SEK", "period": "2026-08", "rate": "0.085727"},
{"series": "ch-vat-monthly", "currency": "USD", "period": "2026-08", "rate": "0.8184"}
]
}
}
result.boxes lists every box of the form with its Ziffer (code), the amount, the tax where the
form computes tax (tax), the number of ledger lines behind it (lines) and whether the form
computes it from other boxes (computed). The rest of the boxes in this demo are:
| Ziffer | Meaning | Amount | Tax |
|---|---|---|---|
| 200 | Total consideration, worldwide turnover (all five invoices) | 36'717.17 | |
| 221 | Supplies abroad (German, French, Swedish and US customers) | 24'717.17 | |
| 299 | Taxable turnover (200 less deductions) | 12'000.00 | |
| 303 | Supplies at the standard rate 8.1% | 12'000.00 | 972.00 |
| 383 | Acquisition tax (EUR 2,000 of cloud hosting from an Irish vendor, at 0.9439) | 1'887.80 | 152.91 |
| 399 | Total tax due | 1'124.91 | |
| 405 | Input tax on investments and other operating expenses | 444.51 | |
| 500 | Amount payable to the ESTV | 680.40 |
The one warning, earlier_period_unfiled, says that the first and second quarters have no final
return yet (details.periods). The return may still be filed; the warning is there so they are not
forgotten (the close warns about overdue returns too).
The foreign-currency invoices and the bill are converted at the BAZG monthly average of the
document's month (result.tax_fx). The demo has one rate set for July to September; each line of
tax_fx says how many ledger lines were converted with it.
If the official rate of a month is not published yet, the return shows an error issue
tax_fx_not_published, the affected lines are shown at the booking rate, and finalising answers
409 until the rate is in the reference data (the BAZG publishes a month's average on the 25th of
the month before). The demo data has all rates, so this return has no error.
4. See the ledger lines behind a box
Every box can be traced to the journal lines that fill it: the entry, the account, the document, the counterparty and the rule that routed the line. A box that the form computes from other boxes has no lines.
curl "$ERP_URL/v1/vat-returns/$RETURN_ID/boxes/303/lines" \
-H "Authorization: Bearer $ERP_KEY"
[
{
"entry_date": "2026-08-03",
"account": "3400",
"account_name": "Dienstleistungserlöse",
"document": "<any>",
"counterparty": "Zürich Kunde AG",
"rule": "standard_rate",
"amount": "12000.00"
}
]
5. Adjust a box by hand
Some boxes cannot be derived from the ledger (import tax paid to customs, Einlageentsteuerung,
input tax corrections). Those boxes are adjustable, and you enter an amount with recompute.
A box the ledger fills or the form computes is refused.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/recompute" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-adjust-303" \
-H "Content-Type: application/json" \
-d '{"adjustments": [{"box": "303", "amount": "10.00"}]}'
{"error": {"code": "box_not_adjustable"}}
An adjustment that changes the amount payable needs an offset account: the settlement entry has to put the other side of the amount somewhere.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/recompute" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-adjust-410-no-account" \
-H "Content-Type: application/json" \
-d '{"adjustments": [{"box": "410", "amount": "25.00"}]}'
{"error": {"code": "box_offset_missing"}}
With the account, the amount is accepted. Box 410 (Einlageentsteuerung) is deducted like input tax, so the amount payable falls by 25.00. The adjustments sent replace the earlier ones.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/recompute" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-adjust-410" \
-H "Content-Type: application/json" \
-d '{"adjustments": [{"box": "410", "amount": "25.00", "account": "1171", "note": "Einlageentsteuerung"}]}'
{
"status": "draft",
"payable": "655.40",
"adjustments": [{"box": "410", "amount": "25.00", "account": "1171", "note": "Einlageentsteuerung"}],
"result": {"reconciliation": {"adjustments": "-25.00", "unexplained": "0.00"}}
}
This page does not file the adjustment, so clear it: an empty list replaces the adjustments, and
omitting adjustments would keep them.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/recompute" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-adjust-clear" \
-H "Content-Type: application/json" \
-d '{"adjustments": []}'
{"status": "draft", "payable": "680.40", "adjustments": []}
6. Download the file for the ESTV portal
format=xml is the eCH-0217 file that is uploaded in the ESTV portal. It works for a draft too.
Other formats are html and pdf (a printable form, marked as a draft until the return is final)
and json (the return as GET /v1/vat-returns/{id} gives it). The XML needs the entity's UID,
which comes from the VAT number of the entity.
curl "$ERP_URL/v1/vat-returns/$RETURN_ID/file?format=xml" \
-H "Authorization: Bearer $ERP_KEY"
<eCH-0217:uid>CHE123456788</eCH-0217:uid>
<eCH-0217:organisationName>Demo AG</eCH-0217:organisationName>
<eCH-0217:reportingPeriodFrom>2026-07-01</eCH-0217:reportingPeriodFrom>
<eCH-0217:reportingPeriodTill>2026-09-30</eCH-0217:reportingPeriodTill>
<eCH-0217:typeOfSubmission>1</eCH-0217:typeOfSubmission>
<eCH-0217:businessReferenceId>CH01-VAT-2026-Q3</eCH-0217:businessReferenceId>
<eCH-0217:totalConsideration>36717.17</eCH-0217:totalConsideration>
<eCH-0217:turnover>12000.00</eCH-0217:turnover>
<eCH-0217:payableTax>680.40</eCH-0217:payableTax>
typeOfSubmission 1 is an initial return; a correction of a final return is written with 2.
7. Reconcile with the ledger, and the unexplained difference
Every return carries a reconciliation: the movements on the VAT accounts of the period against what the return reports. The part that is on the accounts but not on the return is the unexplained difference. Here it is zero:
curl "$ERP_URL/v1/vat-returns/$RETURN_ID" \
-H "Authorization: Bearer $ERP_KEY"
{
"result": {
"reconciliation": {
"ledger": "680.40",
"reported": "680.40",
"payable": "680.40",
"unexplained": "0.00",
"settlement": [
{"account": "1171", "amount": "-444.50"},
{"account": "2200", "amount": "972.00"},
{"account": "2201", "amount": "152.90"},
{"account": "2202", "amount": "-680.40"}
]
}
}
}
settlement is the entry that finalising will book (amounts are signed debits): the output tax
account 2200 and the acquisition tax account 2201 are cleared with a debit, the input tax accounts
with a credit, and 2202 (the liability to the ESTV) takes the amount payable. The demo's purchases
are all overheads (cloud hosting, office costs), so their input tax is on 1171 (investments and
other operating costs, Ziffer 405); input tax on materials and services would be on 1170 and in
Ziffer 400 (see Rules the return follows).
Now someone posts 10.00 directly on the output tax account 2200, without a tax-tagged document.
curl -X POST "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-manual-2200" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"entry_date": "2026-09-01",
"description": "VAT booked by hand",
"lines": [
{"account": "6500", "debit": "10.00"},
{"account": "2200", "credit": "10.00"}
]
}'
{"result": {"status": "posted"}}
The draft still shows the state of its last computation. Recompute it (send no adjustments to
keep the current ones) and the difference appears, as a warning.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/recompute" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-recompute-1" \
-H "Content-Type: application/json" \
-d '{}'
{
"warnings": 2,
"payable": "680.40",
"result": {
"issues": [
{"severity": "warning", "code": "earlier_period_unfiled"},
{"severity": "warning", "code": "unexplained_difference"}
],
"reconciliation": {"ledger": "690.40", "reported": "680.40", "unexplained": "10.00"}
}
}
8. Finalise
Finalising recomputes the return, books the settlement entry through the approval policy and, once that entry is posted, locks the period against further VAT postings. Three rules decide who may do it:
- a human: the policy action
finalize_vat_returnis denied to agents (see "What can go wrong"); - a known person: the organization's first key, and every key created with it, belongs to nobody and never finalises;
- not the preparer: whoever created the return or computed it last (whichever of their keys or
sessions) does not finalise it. The policy action
finalize_own_vat_returnis denied by default (rulevat_preparer_does_not_finalise). An organization with a single person allows it in its approval policy.
The first key prepared this return, so it cannot finalise it. The refusal says why:
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/finalize" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-finalize-system-key" \
-H "Content-Type: application/json" \
-d '{}'
{
"error": {
"code": "forbidden",
"message": "a key the system issued (such as the organization's first admin key) belongs to no known person and cannot finalise VAT returns; sign in as a person, or use a key a person created"
}
}
Bob checks the figures and recomputes the draft. That makes him the last to compute it, so he is
now its preparer too and cannot finalise it either. This refusal is a policy refusal: the answer
has the code policy_denied and names the action and the rule.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/recompute" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-ch-recompute-bob" \
-H "Content-Type: application/json" \
-d '{}'
{"status": "draft", "payable": "680.40"}
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/finalize" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-ch-finalize-bob" \
-H "Content-Type: application/json" \
-d '{}'
{
"error": {
"code": "policy_denied",
"action": "finalize_own_vat_return",
"rule_id": "vat_preparer_does_not_finalise"
}
}
Alice created and computed nothing, so she can finalise. A return with an unexplained difference
needs a written explanation; without one the answer is 409:
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-finalize-no-explanation" \
-H "Content-Type: application/json" \
-d '{}'
{"error": {"code": "vat_unexplained"}}
The settlement entry is dated the last day of the period, or the first open day after it when the
period is closed (see step 10). posting_date can name a later day, never an earlier one:
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-finalize-early" \
-H "Content-Type: application/json" \
-d '{"posting_date": "2026-09-15", "explanation": "Manual VAT correction of 10.00 booked on 1 September, see memo"}'
{"error": {"code": "settlement_before_period_end"}}
With the explanation and the default date, the return is final. The answer holds the return and
the result of submitting the settlement entry; settlement.policy is the approval decision. A
person with the post scope posts at once. If the policy asked for a second person, the return
would be pending_approval until that person approves the entry, and a rejection would put it
back to draft.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-finalize" \
-H "Content-Type: application/json" \
-d '{"explanation": "Manual VAT correction of 10.00 booked on 1 September, see memo"}'
{
"vat_return": {
"reference": "CH01-VAT-2026-Q3",
"status": "final",
"payable": "680.40",
"explanation": "Manual VAT correction of 10.00 booked on 1 September, see memo",
"settlement_entry_id": "<uuid>"
},
"settlement": {
"status": "posted",
"policy": {"decision": "allow", "rule_id": "humans_allowed"},
"approval_id": null
}
}
The settlement is an ordinary journal entry with the source vat, dated 30 September (September is
open, and the return period ends on that day):
curl "$ERP_URL/v1/journal-entries/$SETTLEMENT_ID" \
-H "Authorization: Bearer $ERP_KEY"
{
"status": "posted",
"source": "vat",
"source_ref": "CH01-VAT-2026-Q3",
"entry_date": "2026-09-30"
}
The 10.00 stays on account 2200: the settlement clears only what the return reports.
Record the submission
Final is not filed. Finalising books the settlement and locks the period; it sends nothing to the
ESTV, and the application never files a return electronically (in a test copy the record below is
a note and nothing more). Upload the eCH-0217 file of step 6 in the ESTV portal, then record the
day and the confirmation number the portal gave. Until then the period's due_status stays
upcoming, due_soon or overdue, and the to-do counts and the close check keep reminding of it.
Recording it is a person's act like finalising (the policy action finalize_vat_return, denied to
agents); the day may not be before the period ends nor in the future.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/submitted" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-submitted-early" \
-H "Content-Type: application/json" \
-d '{"submitted_on": "2026-09-29"}'
{"error": {"code": "vat_submitted_before_period_end"}}
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/submitted" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-submitted" \
-H "Content-Type: application/json" \
-d '{"submitted_on": "2026-10-05", "submission_reference": "ESTV-2026-Q3-4711"}'
{
"reference": "CH01-VAT-2026-Q3",
"status": "final",
"filing_status": "submitted",
"submitted_on": "2026-10-05",
"submission_reference": "ESTV-2026-Q3-4711",
"settled_amount": "680.40",
"paid_amount": "0.00",
"payment_difference": "680.40",
"payments": []
}
A submission recorded by mistake is withdrawn with a reason (POST .../submitted/withdraw,
{"reason": "..."}), never deleted, and only while no payment is linked.
Record the payment
The 680.40 now sits on the settlement account 2202. The usual way to pay it is the bank: the bank
debit is matched to account 2202 on the bank worksheet (POST /v1/bank-lines/{line_id}/post with
"account": "2202", see bank reconciliation). Here a journal entry from
the cash account stands in for it.
curl -X POST "$ERP_URL/v1/journal-entries" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-payment-entry" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"entry_date": "2026-10-08",
"description": "ESTV VAT Q3 2026",
"lines": [
{"account": "2202", "debit": "680.40"},
{"account": "1000", "credit": "680.40"}
]
}'
{"entry": {"status": "posted"}}
The candidates are the posted entries of the company from the settlement's day on with a line on the settlement account on the paying side (a debit for VAT to pay, a credit for a refund) that no return has linked yet:
curl "$ERP_URL/v1/vat-returns/$RETURN_ID/payment-candidates" \
-H "Authorization: Bearer $ALICE_KEY"
[{"description": "ESTV VAT Q3 2026", "entry_date": "2026-10-08", "amount": "680.40"}]
Linking the entry makes the return paid once the links cover settled_amount. A part payment is
one link of several, and a different amount is accepted and shown as payment_difference. An
entry of another company, not posted, dated before the settlement or without a line on 2202 on the
paying side is refused (422); a link made by mistake is undone with a reason
(POST .../payments/{payment_id}/unlink).
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/payments" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-paid" \
-H "Content-Type: application/json" \
-d '{"journal_entry_id": "$VAT_PAYMENT_ID"}'
{
"filing_status": "paid",
"paid_amount": "680.40",
"payment_difference": "0.00",
"payments": [{"paid_on": "2026-10-08", "amount": "680.40"}]
}
The periods list shows the same: due_status is filed, filing_status paid. A period whose
return is final but unpaid after the due date has payment_overdue true.
9. Correct a final return
A final return is not edited. amend opens a correction: the original becomes amending, the
period's VAT postings are unlocked, and a new draft (CH01-VAT-2026-Q3/1, its amends_id names
the original) is computed from the current ledger. It needs the approve scope and, like
finalising, the policy action finalize_vat_return. Bob (approve scope) opens the correction; the
correction is then his draft, so Alice files it. Whoever opens a correction, or recomputes it, is
its preparer.
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/amend" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-ch-amend"
{
"reference": "CH01-VAT-2026-Q3/1",
"status": "draft",
"amends_id": "<uuid>",
"payable": "680.40"
}
curl "$ERP_URL/v1/vat-returns/$RETURN_ID" \
-H "Authorization: Bearer $ERP_KEY"
{"reference": "CH01-VAT-2026-Q3", "status": "amending"}
A late invoice of 100.00 for the Zurich customer can now be posted in September (by the first key, which may post).
curl -X POST "$ERP_URL/v1/documents" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-late-invoice" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"kind": "sales_invoice",
"party": "C-CH",
"document_date": "2026-09-10",
"lines": [{"description": "Additional consulting", "unit_price": "100"}]
}'
{"document": {"status": "posted", "total": "108.10"}}
The correction was computed before the invoice, and finalising checks that the ledger has not changed since: it refuses until the preparer recomputes and reviews. (Alice files the correction; had she recomputed it, she would be its preparer and could not.)
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-correction-stale" \
-H "Content-Type: application/json" \
-d '{"explanation": "Manual VAT correction of 10.00 booked on 1 September, see memo"}'
{"error": {"code": "vat_ledger_changed"}}
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/recompute" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-ch-correction-recompute" \
-H "Content-Type: application/json" \
-d '{}'
{
"status": "draft",
"payable": "688.50",
"result": {"boxes": [{"id": "200", "amount": "36817.17"}, "..."]}
}
The correction reports the full period (688.50 = 680.40 + 8.10 of tax on the invoice). Finalising it
books only the difference to the original in its 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-ch-correction-finalize" \
-H "Content-Type: application/json" \
-d '{"explanation": "Manual VAT correction of 10.00 booked on 1 September, see memo"}'
{
"vat_return": {"reference": "CH01-VAT-2026-Q3/1", "status": "final", "payable": "688.50"},
"settlement": {"status": "posted"}
}
curl "$ERP_URL/v1/journal-entries/$CORRECTION_ENTRY_ID" \
-H "Authorization: Bearer $ERP_KEY"
{
"source": "vat",
"source_ref": "CH01-VAT-2026-Q3/1",
"lines": [
{"line_no": 1, "debit": "8.10", "credit": "0.00"},
{"line_no": 2, "debit": "0.00", "credit": "8.10"}
]
}
curl "$ERP_URL/v1/vat-returns/$RETURN_ID" \
-H "Authorization: Bearer $ERP_KEY"
{"reference": "CH01-VAT-2026-Q3", "status": "superseded"}
The XML of a correction carries typeOfSubmission 2 (GET .../file?format=xml on the correction).
10. Discard a correction
A draft can be discarded; it stays in the list with the status discarded. Discarding a
correction puts the original back from amending to final and locks the period again. Here the
final correction is amended once more and that second correction is discarded.
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/amend" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-ch-amend-2"
{"reference": "CH01-VAT-2026-Q3/2", "status": "draft"}
curl -X POST "$ERP_URL/v1/vat-returns/$SECOND_CORRECTION_ID/discard" \
-H "Authorization: Bearer $BOB_KEY" \
-H "Idempotency-Key: vat-ch-discard"
{"reference": "CH01-VAT-2026-Q3/2", "status": "discarded"}
curl "$ERP_URL/v1/vat-returns?entity=CH01&status=final" \
-H "Authorization: Bearer $ERP_KEY"
[{"reference": "CH01-VAT-2026-Q3/1", "status": "final"}]
11. A return for a closed quarter
The settlement entry cannot be posted into a closed period. When the period of the return end is
closed, the entry is dated the first day of the next open period instead. January to August are
closed in the demo, so a second-quarter return (which ends on 30 June) settles on 1 September. The
quarter has no postings; to keep the example short it carries one adjustment, an
Einlageentsteuerung of 25.00 that the ESTV owes the company, which makes the return a credit
(payable is negative).
curl -X POST "$ERP_URL/v1/vat-returns" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-q2" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"period": "2026-Q2",
"adjustments": [{"box": "410", "amount": "25.00", "account": "1171", "note": "Einlageentsteuerung"}]
}'
{
"reference": "CH01-VAT-2026-Q2",
"status": "draft",
"payable": "-25.00",
"result": {
"issues": [
{"severity": "warning", "code": "earlier_period_unfiled", "details": {"periods": "2026-Q1"}},
{"severity": "info", "code": "no_vat_lines"}
]
}
}
curl -X POST "$ERP_URL/v1/vat-returns/$Q2_ID/finalize" \
-H "Authorization: Bearer $ALICE_KEY" \
-H "Idempotency-Key: vat-ch-q2-finalize" \
-H "Content-Type: application/json" \
-d '{}'
{"vat_return": {"status": "final"}, "settlement": {"status": "posted"}}
curl "$ERP_URL/v1/journal-entries/$Q2_ENTRY_ID" \
-H "Authorization: Bearer $ERP_KEY"
{"source": "vat", "source_ref": "CH01-VAT-2026-Q2", "entry_date": "2026-09-01"}
What can go wrong
An agent can prepare a return but never file it. Create an agent key (with the first key, which
holds admin); it holds draft, which is enough to compute and recompute, and the policy refuses
it the filing before any other rule is asked.
curl -X POST "$ERP_URL/v1/api-keys" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "VAT agent", "actor_type": "agent", "scopes": ["read", "draft"]}'
{"key": "<any>", "actor_type": "agent", "scopes": ["read", "draft"]}
curl -X POST "$ERP_URL/v1/vat-returns/$RETURN_ID/finalize" \
-H "Authorization: Bearer $AGENT_KEY" \
-H "Idempotency-Key: vat-ch-agent-finalize" \
-H "Content-Type: application/json" \
-d '{}'
{
"error": {
"code": "policy_denied",
"action": "finalize_vat_return",
"rule_id": "agents_never_approve"
}
}
While a return is final VAT postings into its period are refused (and the database refuses them
too): a document with VAT dated in the third quarter answers 409 vat_period_locked, naming the
return (document, vat_return_id) and its period (start, end). Other postings (without VAT)
are not affected. Amend the return to post corrections (step 9).
curl -X POST "$ERP_URL/v1/documents" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-locked-invoice" \
-H "Content-Type: application/json" \
-d '{
"entity": "CH01",
"kind": "sales_invoice",
"party": "C-CH",
"document_date": "2026-09-12",
"lines": [{"description": "Too late", "unit_price": "100"}]
}'
{"error": {"code": "vat_period_locked"}}
Only a draft can be finalised or recomputed; a final return answers 409.
curl -X POST "$ERP_URL/v1/vat-returns/$CORRECTION_ID/recompute" \
-H "Authorization: Bearer $ERP_KEY" \
-H "Idempotency-Key: vat-ch-recompute-final" \
-H "Content-Type: application/json" \
-d '{}'
{"error": {"code": "vat_return_not_draft"}}
Rules the return follows
These are the product's rules for a Swiss company (ADR-0058):
- Input tax on materials and services (Ziffer 400) or on investments and other operating costs
(Ziffer 405). Decided by the account the purchase line books to, by a rule of the chart: in
the Swiss KMU chart, class 4 (materials, goods for resale, third-party services, energy) is
Ziffer 400 and its input tax is booked to 1170; classes 1 (fixed assets), 5 and 6 (personnel,
rent, administration and other overheads) are Ziffer 405 and booked to 1171 (role
vat_input_investments). A CHF 3,000 server bought on 1520 is 243.00 in Ziffer 405 and on 1171; goods for resale on a class 4 account are Ziffer 400 and 1170. Acquisition tax that is deducted again follows the same rule. An account the company adds follows its class. - Due date. 60 days after the end of the period (MWSTG Art. 71 and 86); when that day is a
Saturday, a Sunday or a public holiday, the next working day of the Swiss business-day calendar
(SIC) the payment dates use.
GET /v1/entities/{entity}/vat-periodsshows it asdue_date. - Order. A return may be filed while an earlier period has none; it then carries the warning
earlier_period_unfiled, never a refusal. - Supplies abroad. A sale to a customer in another VAT area goes under the reverse charge only
when the customer is a business: it has a VAT number, or it is marked as one (
business). Without either, the supply is treated as one to a private person (a service: no Swiss VAT, Ziffer 221; an electronic service: the VAT of the customer's country) and the document carries the warningcustomer_not_business. A customer marked as a business without a VAT number keeps the reverse charge with the warningreverse_charge_without_vat_number.
Reference
GET /v1/entities/{entity}/vat-settingsGET /v1/entities/{entity}/vat-periodsPOST /v1/vat-returnsGET /v1/vat-returnsGET /v1/vat-returns/{return_id}GET /v1/vat-returns/{return_id}/boxes/{box}/linesPOST /v1/vat-returns/{return_id}/recomputeGET /v1/vat-returns/{return_id}/filePOST /v1/vat-returns/{return_id}/finalizePOST /v1/vat-returns/{return_id}/submittedPOST /v1/vat-returns/{return_id}/submitted/withdrawGET /v1/vat-returns/{return_id}/payment-candidatesPOST /v1/vat-returns/{return_id}/paymentsPOST /v1/vat-returns/{return_id}/payments/{payment_id}/unlinkPOST /v1/vat-returns/{return_id}/amendPOST /v1/vat-returns/{return_id}/discardPOST /v1/journal-entriesGET /v1/journal-entries/{entry_id}POST /v1/documentsPOST /v1/api-keys