Guide · Germany · ~15 minutes
Run a German pay run, end to end.
Same API as Australia, different machinery underneath: Germany runs natively on the Payroll Engine pack (DE-2026.1) — effective-dated case values, the draft → previewed → approved → finalised lifecycle, and the thing AU can't do: native stateless calculation. Everything below is a live governed run through the Payroll Engine (payrollengine.ch), executed via Smartta's evidenced executor — real cev_* / inb_* evidence on every step, not a recording. The numbers are what the engine actually computed, and they match the Germany page's live calculator to the cent.
Step 1
Provision a German tenant
curl -X POST $BASE/v1/partners/tenants \ -H "$AUTH" -H "$JSON" \ -d '{ "name": "Müller Gastronomie GmbH", "jurisdiction": "DE", "engine": "payroll_engine" }'
curl -H "$AUTH" $BASE/v1/tenants/$TENANT/packs # pinned_version: "DE-2026.1" # capabilities: stateless_calculate=native · statutory_lodgement=unsupported
Step 2
Employees — flat in, case values underneath
You send the same flat employee shape as every country. Behind the API, the DE pack stores it as effective-dated case values (Steuerklasse, Kirchensteuer, Kinderfreibeträge) — you never touch the case model.
curl -X POST $BASE/v1/tenants/$TENANT/employees -H "$AUTH" -H "$JSON" -d '{ "first_name": "Anna", "last_name": "Müller", "employment": { "start_date": "2026-01-01", "annual_salary": 60000 }, "tax_profile": { "tax_class": 1, "church_tax": false, "child_allowances": 1 }, "governance_reason": "DE onboarding — Steuerklasse I" }' # and a contrast case: church tax + kinderlos (PV surcharge applies) curl ... -d '{ "first_name": "Jonas", "last_name": "Weber", "employment": { "start_date": "2026-02-01", "annual_salary": 84000 }, "tax_profile": { "tax_class": 1, "church_tax": true, "child_allowances": 0 }, ... }'
Pack validation is jurisdiction-aware: omitting annual_salary returns 422 pack_validation_failed with the rule ref DE-2026.1/employment/salary_required.
Step 3
The pay run — PE's job lifecycle behind the same verbs
The Ledra Pay state machine is identical everywhere; the partner verbs lead, and in Germany they drive the Payroll Engine's native job lifecycle underneath:
curl -X POST $BASE/v1/tenants/$TENANT/pay-runs -H "$AUTH" -H "$JSON" \ -d '{ "period_start": "2026-07-01", "period_end": "2026-07-31", "payment_date": "2026-07-28", "governance_reason": "Juli Abrechnung" }' curl -X POST $BASE/v1/tenants/$TENANT/pay-runs/$PR/preview -H "$AUTH" -H "$JSON" -d '{}' # live engine result — one employee (Max Mustermann), period 2026-01 { "pay_run": { "status": "previewed", "totals": { "employee_count": 1, "gross": "3500.00", "tax": "415.50", // LSt 415.50 + SolZ 0.00 + KiSt 0.00 "social": "740.25", // KV 285.25 + RV 325.50 + AV 45.50 + PV 84.00 (employee) "net": "2344.25", "currency": "EUR" } } }
Two details worth noticing in the numbers: no Solidaritätszuschlag and no Kirchensteuer — at €3.500 Lohnsteuer the 2026 SolZ Freigrenze isn't reached, and this employee isn't a church-tax payer, so both come back €0,00; and the PV line (€84,00) carries no kinderlos surcharge here. The entries endpoint returns the full per-employee wage-type breakdown — 63 statutory lines (LSt, SolZ, KiSt, KV/RV/AV/PV, SV-Brutto, Netto).
Approve writes the control-plane evidence envelope (no engine command); finalise commits a real async Payroll Engine payrun job. Once the commit lands, the committed per-employee results — wage-type lines and the employee name — are read back from …/pay-runs/{id}/entries (Step 4). Deleting a finalised run is refused by the engine itself, not just the API.
Step 4
Committed entries — read back the finalised run
Once finalise commits the Payroll Engine payrun job, the committed per-employee results are read back from the entries endpoint — committed payroll results plus the per-record wage-type lines, joined to the employee name.
curl -H "$AUTH" $BASE/v1/tenants/$TENANT/pay-runs/$PR/entries { "data": [{ "employee_name": "Max Mustermann", "gross": "3500.00", "tax": "415.50", "social": "740.25", "net": "2344.25", "currency": "EUR", "wage_types": [ // 63 statutory wage-type lines { "code": "DE.SvBrutto", "amount": "3500.00" }, { "code": "DE.Lohnsteuer", "amount": "415.50" }, { "code": "DE.Solidaritaetszuschlag", "amount": "0.00" }, { "code": "DE.Kirchensteuer", "amount": "0.00" }, { "code": "DE.KvBeitragAn", "amount": "285.25" }, { "code": "DE.RvBeitragAn", "amount": "325.50" }, { "code": "DE.AvBeitragAn", "amount": "45.50" }, { "code": "DE.PvBeitragAn", "amount": "84.00" } // … 55 more lines ], "evidence_id": "cev_…" }] }
The composition reads committed payroll results, expands each record's wage-type lines, and joins the employee name from the directory's employee_profile. Provenance on the run is payroll_engine · DE-2026.1 (payrollengine.ch · governed), with real cev_* / inb_* evidence behind every figure — finalise commits the engine job; the entries are what the engine committed.
Step 5
Native stateless calculation — the DE differentiator
On an AU tenant this endpoint honestly returns 501 CAPABILITY_NOT_SUPPORTED. On a German tenant it's native: the engine runs a forecast — no pay run, no state, still receipted.
curl -X POST $BASE/v1/tenants/$TENANT/calculate -H "$AUTH" -H "$JSON" \ -d '{ "employee": { "annual_salary": 60000, "church_tax": false, "children": true } }' { "pack": "DE-2026.1", "gross": { "amount": "5000.00", "currency": "EUR" }, "lines": [ { "type": "lst", "amount": "-787.50" }, { "type": "kv_an", "amount": "-437.50" }, { "type": "rv_an", "amount": "-465.00" }, { "type": "av_an", "amount": "-65.00" }, { "type": "pv_an", "amount": "-90.00" } ], "net": { "amount": "3155.00", "currency": "EUR" }, "evidence_id": "cev_…" }
Step 6
Effective-dating — the DE differentiator
The DE pack is effective-dated PE-native. On a write, effective_from sets the case-value start date in the engine; on a read, ?as_of=YYYY-MM-DD sets the engine's evaluationDate, so you see the employee exactly as it stood (or will stand) on that date.
# write: a future-dated change — case value starts on effective_from curl -X PATCH $BASE/v1/tenants/$TENANT/employees/$EMP -H "$AUTH" -H "$JSON" \ -d '{ "tax_profile": { "tax_class": 3 }, "effective_from": "2026-07-01", "governance_reason": "Steuerklassenwechsel III ab Juli" }' # read as it stood on a given date — engine evaluationDate curl -H "$AUTH" "$BASE/v1/tenants/$TENANT/employees/$EMP?as_of=2026-06-30" # → tax_class 1 (the change hasn't started yet) curl -H "$AUTH" "$BASE/v1/tenants/$TENANT/employees/$EMP?as_of=2026-07-01" # → tax_class 3
effective_from or as_of value returns 501 CAPABILITY_NOT_SUPPORTED — the AU pack only answers for today. Same API, the pack tells you the truth instead of faking a date it can't honour.Step 7
Statutory outputs — the honest boundary
Calculation, the pay-run lifecycle, and finalise are live and governed today. The one thing still on the roadmap is statutory lodgement: two certified channels (ELSTER/ERiC to the Finanzamt; GKV data exchange gated by ITSG Systemprüfung) that don't exist in this pack yet — they're the design-partner build. Until those rails land, finalised runs yield statutory output files (Lohnsteueranmeldung data, Beitragsnachweis data) for filing through your local process, and lodgement endpoints answer with capability honesty rather than pretending.
| Concern | Today | With the rails |
|---|---|---|
| Lohnsteueranmeldung | Output file from finalised run | POST …/lodge → ELSTER, receipt in the evidence chain |
| Beitragsnachweise | Output data per Krankenkasse | GKV transmission (ITSG-certified) |
| DEÜV-Meldungen | Event records exported | Transmitted on event |
Full endpoint catalogue: the API reference · the AU mirror of this guide: AU payroll end to end · sandbox keys: request access.