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.

You needa sandbox key (lp_sandbox_*)
PackDE-2026.1 · §32a EStG 2026 · BBG €69.750/€101.400
vs Australia/calculate native · lodgement outputs-only
Writes requiregovernance_reason — always

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
The capability mirror-image. Australia: lodgement native (STP rail), stateless calculate emulated-at-best. Germany: stateless calculate native (the engine runs forecast jobs), lodgement unsupported — Lohnsteueranmeldung and Beitragsnachweis come back as statutory output files until the ELSTER + GKV/ITSG rails are built with a design partner. One API, honest per-pack differences.

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:

draftcreated previewedengine calc approvedenvelope finalisedPE payrun job · immutable
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_…"
}
Cross-check it yourself: set the Germany page's calculator to €60.000, children, no church tax — it shows net €3.155,00/Monat. The page's indicative math and the engine's computed result agree to the cent, because they implement the same DE-2026.1 statutory parameters. The page estimates; the engine is the source of truth — and here they're provably consistent.

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
Per-pack honesty again. Effective-dating is native to the Payroll Engine. On an AU tenant, a non-today 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.

ConcernTodayWith the rails
LohnsteueranmeldungOutput file from finalised runPOST …/lodge → ELSTER, receipt in the evidence chain
BeitragsnachweiseOutput data per KrankenkasseGKV transmission (ITSG-certified)
DEÜV-MeldungenEvent records exportedTransmitted on event

Full endpoint catalogue: the API reference · the AU mirror of this guide: AU payroll end to end · sandbox keys: request access.