Jahresabschluss.io
DESign inRegister →

ELSTER check API

ELSTER validation via API, for developers and agents

With the jahresabschluss.io API, a developer or an AI agent runs the official ELSTER validation (ERiC, with the test flag) on a German company's tax filing with one call — POST /v1/tax/check — and nothing is transmitted. The same API freezes a filing as a prepared operation (POST /v1/tax/operations) and creates an approval link, so the user files in the app with their own PIN. It is not a general ELSTER gateway: it validates filings the software builds from that company's books and saved returns, not XML you upload.

Last reviewed: 2026-10-19

What the endpoint is — and what it isn't

ERiC is the client library the German tax administration provides to software makers: it checks data against the official rules and transmits it securely. POST /v1/tax/check runs that validation, with the test flag, on a filing jahresabschluss.io has built from one company's data. The response says whether the data passes, which fields fail and what else would stop a filing.

It is not a pass-through to ELSTER. You don't upload XML, you don't pick a taxpayer and you never handle a certificate. The company follows from the API key; the filing is identified by art plus a period, a source_id, a filing_id or a year. The check is the same one that runs when someone clicks “Run the official check” (“Amtlich prüfen”) in the app, so API and app give the same answer.

Endpoints at a glance

All paths below https://api.jahresabschluss.io/functions/v1/bookkeeping-api/v1, with the matching MCP tool in brackets.

  • GET /tax/filings (list_tax_filings) — every filing with status, transfer ticket and test flag; its IDs are the source_id or filing_id for checks. read.
  • POST /tax/check (check_tax_filing) — official validation, nothing sent. write.
  • POST /tax/drafts (create_tax_draft) — EC sales list or Dauerfristverlängerung built from the books, nothing sent. write.
  • POST /tax/operations (prepare_tax_filing) — freezes values and fingerprint for 30 minutes, answers 201 with the operation. write.
  • GET /tax/operations/{id} (get_tax_operation) — the operation plus the filing's current state, also after the user filed in the app. read.
  • POST /tax/operations/{id}/approval-link (request_tax_filing_approval) — a link into the app, valid for 7 days. write.
  • GET /tax/deadlines (list_tax_deadlines) — the year's deadlines as in the app's tax calendar. read.
  • GET /vat-returns plus GET and POST /vat-returns/supplementary (get_vat_return, get_vat_supplementary, confirm_vat_supplementary) — VAT return figures and the form's conditional questions.
  • POST /tax/operations/{id}/submit is listed in the schema but needs the right file, which is not granted at present; a key without it does not even see the matching MCP tool.

The OpenAPI 3.1 schema is public, no key needed: https://api.jahresabschluss.io/functions/v1/bookkeeping-api/openapi.json. The hosted MCP server at https://api.jahresabschluss.io/functions/v1/bookkeeping-mcp maps each tool onto exactly these routes, with the caller's key.

A check request and its answer

art selects the filing type; exactly one identifier goes with it:

export JAB_API_KEY="jab_live_…"
BASE="https://api.jahresabschluss.io/functions/v1/bookkeeping-api/v1"

curl -s -X POST "$BASE/tax/check" \
  -H "Authorization: Bearer $JAB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"art":"ustva","period":"2026-09"}'

# Response (shortened, values illustrative)
{
  "art": "ustva",
  "zeitraum": "2026-09",
  "valid": true,
  "eric": { "valid": true, "field_errors": [], "hinweise": [] },
  "hindernisse": [
    { "code": "zusatzangaben_offen", "text": "Die bedingten Angaben des Vordrucks … sind nicht bestätigt." }
  ],
  "manifest": { "art": "ustva", "zeitraum": "2026-09", "kennzahlen": { "81": "…", "83": "…" }, "zahllast": "…" },
  "manifest_sha256": "<64 hex characters>"
}

valid comes from ERiC; each field error carries feld, text, regel_id and schwere. hindernisse are the reasons the software itself would not send — unconfirmed conditional questions (zusatzangaben_offen), a missing or expired ELSTER certificate of the company, an open payment, a period overlapping one already sent, or sandbox. A filing is ready only when valid is true and the blocker list is empty.

Identifiers: ustva takes period (YYYY-MM or YYYY-Qn); zm, dauerfrist, sovz and ustje take source_id; ebilanz takes filing_id; kst, gewst and euer take year. A field that does not belong to the type returns 400 invalid_request. Response texts are German; an agent translates them.

Prepare, hand over the link, poll

The prepared operation is the contract between agent and user:

# 1. Freeze values and fingerprint (valid 30 minutes)
curl -s -X POST "$BASE/tax/operations" \
  -H "Authorization: Bearer $JAB_API_KEY" -H "Content-Type: application/json" \
  -d '{"art":"zm","source_id":"<id from GET /tax/filings>"}'
# → 201 { "vorgang_id": "…", "status": "vorbereitet", "manifest": {…},
#         "manifest_sha256": "…", "hindernisse": [], "sendebereit": true, … }

# 2. Approval link for the user (valid 7 days)
curl -s -X POST "$BASE/tax/operations/<vorgang_id>/approval-link" \
  -H "Authorization: Bearer $JAB_API_KEY" -H "Content-Type: application/json" -d '{}'
# → { "status": "freigabe_angefragt",
#     "freigabe_url": "https://jahresabschluss.io/api-freigabe/…", "gueltig_bis": "…" }

# 3. Poll until the user has filed in the app
curl -s "$BASE/tax/operations/<vorgang_id>" -H "Authorization: Bearer $JAB_API_KEY"
# → status plus aktueller_stand with transferticket and test_mode

Show the user art, period, the values and the fingerprint before you hand over the link. The app page recomputes the fingerprint and says whether the figures are unchanged; if they changed, the filing page shows the current values that would be sent. The user files there with the second factor and the ELSTER PIN.

There is no webhook for tax events — poll get_tax_operation, which reads the state from the app's own tables. A prepare call is not idempotent: every call creates a new operation and counts toward the hourly limit. A link for an operation that is already closed answers 409 vorgang_abgeschlossen.

Status codes worth handling

  • 400 invalid_request — unknown art, or an identifier that does not fit it.
  • 403 nur_steuer_routen — a key of a tax-only company called a bookkeeping route.
  • 404 not_found — no filing or operation with this ID for this company.
  • 409 fiscal_year_ambiguous — the VAT period does not fall into exactly one fiscal year.
  • 409 stand_geaendert — the saved return changed during the check; prepare again.
  • 409 nicht_erzeugt — the E-Bilanz file has not been generated in the app yet.
  • 422 api_berichtigung_nur_in_app and api_zerlegung_nur_in_app — corrections and trade tax with apportionment are done in the app; the response carries app_seite, a link to the right page.
  • 429 rate_limited — more than 30 checks, preparations or drafts per hour with this key; the general limit is 120 requests per minute per key, answered with Retry-After.
  • 503 limits_unavailable or steuer_dienst_nicht_bereit — nothing was executed; try again later.

Keys, scopes and the sandbox

  • Send the key as Authorization: Bearer jab_live_…; clients that cannot add “Bearer” may send it bare or as X-API-Key. One key belongs to one company; only its hash is stored.
  • GET routes need read, which every key has. Check, drafts, operations and the approval link need write.
  • Administrators and the workspace creator create keys in the app under “AI agent & API”, confirmed with the second factor. Every key expires — 30 days, 90 days or one year to choose, never more than 366 days — and up to 10 can be active per company.
  • With only the tax module, keys get read and write and reach /v1/tax/…, /v1/vat-returns…, /v1/, /v1/entity and /v1/fiscal-years.
  • A sandbox is a hidden twin of a company with its own keys. Every response then carries X-Jab-Sandbox: true, and get_entity reports sandbox: true. Checks and preparations work and carry the blocker sandbox; nothing is ever transmitted. A company whose year-end data — trial balances, provisions, accruals — is already in the system cannot be copied into a sandbox yet.

Legal frame and deliberate limits

  • No upload of your own XML and no free choice of taxpayer.
  • No corrections, no trade-tax apportionment, no payroll tax, withholding tax, partnership determination or income tax.
  • No transmission by the agent: the right file is not granted.
  • The ELSTER certificate and the PIN stay in the app.

VAT returns and the annual VAT return are transmitted electronically in the officially prescribed format (§ 18(1) and (3) UStG), the E-Bilanz likewise (§ 5b EStG). Every return is a declaration made truthfully to the best of one's knowledge and belief (§ 150(2) AO). That is why the API validates and prepares, and leaves the transmission to a person in the app.

Frequently asked questions

Is this an ELSTER API I can send my own data through?

No. The jahresabschluss.io API runs the official ELSTER validation on filings the software builds from a company's books and saved returns, and leaves the transmission to the user in the app. It does not forward XML you supply.

Does POST /v1/tax/check transmit anything?

No. It runs ERiC with the test flag and returns valid, field errors, blockers and the fingerprint of the current state. Nothing reaches the tax office, in a sandbox or in the real company.

Which scope does the check need?

write, like drafts, preparations and the approval link. Reading filings, deadlines and operations needs only read, which every key includes.

How do I know that the user has filed?

Poll GET /v1/tax/operations/{id}. It returns the operation and the filing's current state from the app's tables, including the transfer ticket once ELSTER has accepted the transmission.

Can I test without touching the real company?

Yes, where a sandbox twin is available: create a key inside the sandbox. Checks and preparations behave as usual, every response carries X-Jab-Sandbox: true, and nothing is transmitted.

What are the limits?

30 checks, preparations and drafts per hour per key, and 120 requests per minute per key in general. The per-minute limit answers 429 with Retry-After.