StabisoDocs

REST API

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_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 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 draft scope, reading needs read, amending a final return needs approve. 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_return is 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_return is denied by default (rule vat_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-periods shows it as due_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 warning customer_not_business. A customer marked as a business without a VAT number keeps the reverse charge with the warning reverse_charge_without_vat_number.

Reference

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