API Reference · v1 · Beta

The payroll layer, documented.

One REST API for the full embedded payroll lifecycle: tenants, certified country regulation packs, employees, pay runs, statutory lodgement, payslips, and an evidence chain on every write. Jurisdiction is a parameter — never a separate integration.

Base URLhttps://api.ledrapay.com/v1
Sandboxhttps://sandbox.ledrapay.com/v1
AuthPartner-scoped bearer keys
FormatJSON · UTF-8 · ISO 8601 / 4217
StatusBeta — schemas stable, additive changes only

Getting started

Overview

Ledra Pay is provider-shaped: the API models what your platform ships — employees, pay runs, payslips, filings — not the internals of a calculation engine. Country logic lives in certified regulation packs attached per tenant; the request and response shapes are identical for every country.

All tenant-scoped routes are prefixed /v1/tenants/{tenantId}. Partner-scoped routes (tenant provisioning and lookup) are prefixed /v1/partners.

# Your first calculation — tenant-scoped, against a PE-native (DE) binding
curl https://sandbox.ledrapay.com/v1/tenants/ten_8f3c…/calculate \
  -H "Authorization: Bearer lp_sandbox_..." \
  -H "Content-Type: application/json" \
  -d '{
    "frequency": "monthly",
    "employee": { "residency": "resident" },
    "earnings": [ { "type": "ordinary", "amount": 3500.00 } ]
  }'

# calculate is capability-gated (stateless_calculate): native on PE-native
# (DE) bindings, 501 CAPABILITY_NOT_SUPPORTED otherwise. The unscoped
# POST /v1/calculate is a 501 stub — always call the tenant-scoped route.

Getting started

Authentication

Every request carries a partner-scoped bearer key. Keys are issued per environment (lp_sandbox_*, lp_live_*; legacy ps_* keys remain valid) and grant access only to tenants registered to your partner account — strict partner-to-tenant isolation is enforced at the gateway, not in your code.

Embedded UI components authenticate with short-lived embed tokens minted server-side via POST /v1/tenants/{tenantId}/embed/token. Partner keys never touch the browser, and embed tokens cannot mint further tokens — a leaked token dies at its 15-minute TTL. Tokens are pinned to one tenant; reads within that tenant are allowed, while writes are gated by the component allowlist (components in the mint body — e.g. a payslip-viewer token cannot approve a pay run). Two optional mint fields narrow scope further: employee_id pins the token to a single employee for employee-facing surfaces, and origin binds it to one browser origin (requests presenting a different Origin header are rejected).

Components can call https://api.ledrapay.com/v1 directly from your pages — CORS is enabled, bearer-only (cookies are never accepted cross-origin). Direct cross-origin calls require an origin-bound token: mint with { "origin": "https://app.yourdomain.com" } and set api-url="https://api.ledrapay.com/v1" on the components. Alternatively, mount a same-origin reverse proxy on your own domain and skip CORS entirely — both integrations are first-class. The vendor embedding guide covers both topologies, token scoping, and the launch checklist.

Access control

User tokens & role-based access

For a full app where many users of one organisation each get restricted access (the enterprise model), mint a user token — an embed token plus a role and a data scope. Mint server-side with a partner key (or a tenant admin user token) via POST /v1/tenants/{tenantId}/users/token:

{ "role": "preparer",
  "scope": { "cost_centres": ["OPS-ACS"], "sites": "*" },
  "user_id": "usr_…", "user_name": "Priya P." }

Roles enforce segregation of duties (not a linear ladder). The API gate is the boundary; the SDK mirrors it by hiding/disabling buttons (query GET /v1/tenants/{id}/me for the caller's role + scope):

viewerreads only
preparercreate & edit pay runs, employees — cannot approve/finalise/submit/void
approverapprove · finalise · submit (STP) · void — cannot prepare (four-eyes)
admineverything within the tenant, incl. offboard & config; may mint lower user tokens (never above its own role or wider than its own scope)

Data scope restricts which rows a user sees by cost-centre/site. It fails closed: a scoped user querying a data source that can't attach the cost-centre dimension receives an empty result with scope_not_enforceable: true — never unfiltered data. (Cost-centres are a governed overlay, not engine-native; enforcement is full where the overlay is present and honest-empty where it isn't yet.) Unscoped users ("*") are unaffected.

Bureau

Portfolio API

For a payroll bureau / MSP running payroll across many client companies (the bureau model), the portfolio endpoints aggregate across every tenant on the partner key. Partner key only — tenant-scoped embed/user tokens are refused (they can't span a portfolio). First-party use behind an operator session; the partner key is never exposed to the browser.

GET/partners/portfolio/summaryAggregate KPIs: total gross, active workers, runs in flight, risk flags, client count
GET/partners/portfolio/pipelinePay runs across all clients, grouped by lifecycle (draft · approved · blocked · finalised)
GET/partners/portfolio/clientsPer-client status rows — headcount, gross, in-flight runs, next pay date, health

The bureau SDK components (ledrapay-portfolio-dashboard, ledrapay-client-list, ledrapay-pipeline-board) render these. Because they carry portfolio-wide access they authenticate via a same-origin session only and must never be embedded in an untrusted page — see the SDK docs.

Getting started

Conventions

Idempotency

All POST endpoints accept an Idempotency-Key header. Retries with the same key return the original response for 24 hours. Use it on everything that creates pay runs or submits to an authority.

Pagination

List endpoints return data, has_more, and next_cursor. Pass ?cursor= and ?limit= (max 200).

Money and dates

Amounts are decimal strings with an explicit currency (ISO 4217). Dates are YYYY-MM-DD; timestamps are UTC ISO 8601. Identifiers are prefixed: ten_, emp_, pr_, ps_, ldg_, evd_.

Versioning

The API is versioned in the path (/v1) and changes additively. Regulation packs are versioned independently (AU-2026.1) — statutory updates ship as new pack releases, never as silent behaviour changes under a pinned version.

Getting started

Core concepts

ObjectWhat it is
PartnerYou — the platform embedding Ledra Pay. Holds API keys and a portfolio of tenants.
TenantAn employer (your customer). Owns employees, pay runs, and attached regulation packs. Fully isolated.
Regulation packA certified, versioned country ruleset — tax scales, social contributions, statutory outputs. Attached to a tenant, pinned to a version.
Pay runThe lifecycle object: draft → previewed → approved → finalised. lodged ROADMAP (AU statutory rail), plus forecast / retro ROADMAP variants.
Shadow run ROADMAPA pay run calculated for comparison only — returns a line-level variance report against incumbent results you supply.
Lodgement ROADMAPA statutory submission (STP, RTI, payday filing) generated from a finalised pay run.
EvidenceAn immutable, hash-chained governance record written on every state-changing call.

Platform

Partners & tenants

Provision a tenant per employer. A tenant is created with its engine binding (pack + version) attached at provisioning time — the pack is pinned on the binding, not patched in later.

POST/partners/tenantsProvision a tenant (employer) + engine binding under your partner account. Pack pinned here (jurisdiction, engine, optional pack_version)
GET/partners/tenantsList your tenant portfolio
GET/partners/tenants/{tenantId}Get tenant details and bindings (engine, pinned version, capabilities)
PATCH/partners/tenants/{tenantId}ROADMAP Update tenant registration
DELETE/partners/tenants/{tenantId}ROADMAP Unregister — revokes access, data retained per policy
POST/partners/batch/pay-runsROADMAP Create pay runs across many tenants in one call
POST/partners/batch/lodgeROADMAP Batch statutory lodgement across tenants
GET/partners/dashboardROADMAP Portfolio summary: upcoming runs, alerts, lodgement states

Platform

Regulation packs

Packs are how Ledra Pay stays one integration across countries. Attach a pack to a tenant and pin a version; the pay run API never changes shape. Certification status, maintainer, and changelog are queryable — compliance is an API object, not a PDF.

GET/packsList available packs with certification status and versions
GET/packs/{packCode}Pack detail: maintainer, statutory scope, changelog
GET/tenants/{tenantId}/packsAttached packs for a tenant with the live capability matrix (per binding)
POST/tenants/{tenantId}/packsROADMAP Attach a pack post-hoc. Today the pack is pinned at tenant provisioning (POST /partners/tenants)
PATCH/tenants/{tenantId}/packs/{packCode}ROADMAP Move the pinned version (e.g. annual statutory update)
{
  "code": "AU",
  "current_version": "AU-2026.1",
  "status": "beta",                  // draft | beta | certified
  "maintainer": "Ledra Pay Compliance (AU)",
  "scope": ["paye_tax", "medicare_levy", "stsl", "super_guarantee", "awards", "payroll_tax_state"],
  "lodgement_rails": [{ "type": "STP2", "channel": "ATO-SBR", "status": "in_build" }],
  "versions": [{ "version": "AU-2026.1", "effective": { "from": "2026-07-01", "to": "2027-06-30" } }]
}
PackCalculationLodgement rail
AUBetaSTP Phase 2 — in build (sandbox simulation live)
NZBetaPayday filing — planned next, with design partner
UKBetaRTI FPS/EPS — planned next, with design partner
DEBetaELSTER + GKV/ITSG — design-partner project
FR · NL · BE · ES · PT · AT · CH · LU · USBetaStatutory outputs only

Capability matrix

GET /tenants/{tenantId}/packs returns the attached pack(s) with a capability matrix per binding. Capabilities are how the one API stays honest across engines that genuinely differ: a feature is either native or unsupported, never silently faked. An unsupported call returns 501 CAPABILITY_NOT_SUPPORTED with the alternatives, not a wrong answer.

{
  "data": [{
    "binding_id": "bnd_…", "engine": "…", "pinned_version": "DE-2026.1",
    "capabilities": {
      "effective_dating": "native",        // as_of reads / effective_from writes
      "stateless_calculate": "native",     // forecast /calculate with no pay run
      "statutory_lodgement": "unsupported",
      "payment_file": "unsupported",
      "payslip_pdf": "unsupported"
    }
  }]
}
CapabilityAU packDE packWhat it gates
effective_datingunsupportednative?as_of= reads · effective_from writes
stateless_calculateunsupportednativePOST /calculate with no pay run
statutory_lodgementnativeunsupportedSTP / lodgement rails
payment_file · payslip_pdf · retirement_batchnativeunsupportedABA / payslip PDF / SuperStream
Read capabilities before you branch. The two engines are mirror images — AU lodges statutorily but has no effective-dated history; DE has native effective-dating + stateless calc but its lodgement rails are a design-partner build. Gate your UI on the matrix (the SDK exposes getCapabilities() / supportsEffectiveDating()) so an unsupported feature is hidden, not a runtime 501.

Platform

Organisation

Roadmap — not yet available

GET/tenants/{tenantId}/organisationROADMAP Company details, registration numbers (ABN, PAYE ref…)
PATCH/tenants/{tenantId}/organisationROADMAP Update company details
GET/tenants/{tenantId}/sitesROADMAP List work sites / locations (drives state payroll tax, regional rules)
POST/tenants/{tenantId}/sitesROADMAP Create work site
GET/tenants/{tenantId}/cost-centresROADMAP List cost centres
POST/tenants/{tenantId}/cost-centresROADMAP Create cost centre
POST/tenants/{tenantId}/validateROADMAP Payroll-readiness check: returns blocking issues before first run

People

Employees

One employee model for every country. Country-specific fields (TFN declarations, UK tax codes, NI numbers) live under tax_profile, validated by the tenant's attached pack — your integration code stays identical.

POST/tenants/{tenantId}/employeesCreate employee: personal, employment, tax profile, bank, retirement. Optional effective_from
GET/tenants/{tenantId}/employees?as_of=YYYY-MM-DDList with pagination and filters; optional as_of point-in-time read
GET/tenants/{tenantId}/employees/{empId}?as_of=YYYY-MM-DDFull record, optionally as of a past/future date
PATCH/tenants/{tenantId}/employees/{empId}Partial update; optional effective_from dates the change. Tax/bank/retirement fields are nested in the same payload (under tax_profile etc.)
POST/tenants/{tenantId}/employees/{empId}/terminateROADMAP Terminate with final-pay calculation (unused leave, ETPs)
GET/tenants/{tenantId}/employees/{empId}/tax-profileROADMAP Country tax settings as a sub-resource
PATCH/tenants/{tenantId}/employees/{empId}/tax-profileROADMAP Update tax settings as a sub-resource (use the employee PATCH today)
PATCH/tenants/{tenantId}/employees/{empId}/bank-accountsROADMAP Update bank account split
PATCH/tenants/{tenantId}/employees/{empId}/retirementROADMAP Super fund / pension scheme details
POST/tenants/{tenantId}/employees/bulkROADMAP Bulk import up to 500 employees
GET/tenants/{tenantId}/employees/{empId}/documentsROADMAP Statutory documents (payment summaries, P60, P45)

Effective dating

Employee facts are effective-dated on engines that support it (capability effective_dating: native): a write carries an optional effective_from that stamps when the change takes effect, and a read carries an optional as_of that resolves the value in force on that date (default: today). A future-dated raise is the canonical case — it does not change "now", but it is already on file.

// DE / PE-native packs only — AU (myaccountant) returns 501 for a non-today date
// future-dated raise — takes effect 2026-09-01, not today
PATCH /tenants/{t}/employees/emp_8f3c…
{ "employment": { "annual_salary": 90000 }, "effective_from": "2026-09-01" }

GET …/employees/emp_8f3c…?as_of=2026-08-01   → annual_salary 60000   // before
GET …/employees/emp_8f3c…?as_of=2026-10-01   → annual_salary 90000   // after
GET …/employees/emp_8f3c…                  → annual_salary 60000   // today (raise still future)
Capability-gated. Against an engine without effective-dating, a non-today as_of/effective_from returns 501 CAPABILITY_NOT_SUPPORTED rather than silently ignoring the parameter (today/omitted always works). Check the capability matrix first. Effective-dating is scoped to the employee record: a finalised pay run already froze the values in force at its period-end, so its entries are point-in-time by construction.

People

Timesheets

Roadmap — not yet available

POST/tenants/{tenantId}/timesheetsROADMAP Create timesheet with daily entries
POST/tenants/{tenantId}/timesheets/bulkROADMAP Bulk create for multiple employees
GET/tenants/{tenantId}/timesheetsROADMAP List with filters (employee, status, period)
POST/tenants/{tenantId}/timesheets/{id}/approveROADMAP Approve for pay-run import

Payroll

Pay runs

The pay run is a state machine: draft → previewed → approved → finalised. Preview never advances state — call it as often as you like; approve is a control-plane transition (an evidence envelope, no engine command); finalise commits to the engine. lodged, forecast and retro variants are roadmap.

POST/tenants/{tenantId}/pay-runsCreate pay run (period_start, period_end, optional payment_date; kind stored). Requires governance_reason
GET/tenants/{tenantId}/pay-runsList with status and totals (filter ?status=)
GET/tenants/{tenantId}/pay-runs/{prId}Pay run with totals and evidence refs
GET/tenants/{tenantId}/pay-runs/{prId}/entriesPer-employee entries (persisted after preview/finalise; else read from engine)
POST/tenants/{tenantId}/pay-runs/{prId}/entriesAdd an entry — draft only. Requires governance_reason
POST/tenants/{tenantId}/pay-runs/{prId}/previewCalculate without advancing past previewed; persists entries + payslips
POST/tenants/{tenantId}/pay-runs/{prId}/approveControl-plane transition previewed → approved (evidence envelope)
POST/tenants/{tenantId}/pay-runs/{prId}/finaliseCommit the approved run to the engine — a real, async Payroll Engine payrun job; persists committed entries (statutory wage-type lines). Does not generate bank/super files (AU-only, capability-gated)
DELETE/tenants/{tenantId}/pay-runs/{prId}Tear down a draft or previewed run (evidence-chained); finalised runs cannot be deleted
PATCH/tenants/{tenantId}/pay-runs/{prId}/entries/{id}ROADMAP Update hours, rates, pay items (re-add the entry today)
DELETE/tenants/{tenantId}/pay-runs/{prId}/entries/{id}ROADMAP Remove a single entry from draft
POST/tenants/{tenantId}/pay-runs/{prId}/import-linesROADMAP Import approved timesheet lines
POST/tenants/{tenantId}/pay-runs/{prId}/voidROADMAP Void a finalised run (evidence-chained)
POST/tenants/{tenantId}/pay-runs/{prId}/cloneROADMAP Clone structure for the next period
POST/tenants/{tenantId}/pay-runs/eoyROADMAP End-of-year finalisation for the attached pack's year-end
# Preview response (abridged) — identical shape for every country
{
  "pay_run": "pr_8f2c", "status": "previewed", "pack": "AU-2026.1",
  "entries": [{
    "employee": "emp_31ad",
    "gross": { "amount": "3269.23", "currency": "AUD" },
    "lines": [
      { "type": "tax.paye",          "amount": "-612.00" },
      { "type": "tax.medicare_levy", "amount": "-65.38" },
      { "type": "employer.super_guarantee", "amount": "392.31", "employer_cost": true }
    ],
    "net": { "amount": "2591.85", "currency": "AUD" }
  }],
  "evidence": "evd_a90b"          // every calculation is receipted
}

Entries — the cross-country shape

GET …/pay-runs/{prId}/entries returns one row per employee with a jurisdiction-stable core and pack-variant extras. The core is guaranteed identical in every country; extras are additive per pack and never replace core fields.

{
  "data": [{
    // core — every pack, always (amounts are 2dp decimal strings)
    "id": "…", "employee_id": "emp_0e4c888c5987c068",
    "employee_name": "Alex Chen",
    "gross": "1140.00", "tax": "299.00", "net": "841.00",

    // pack-variant extras (additive):
    "super": "136.80",          // AU/NZ: superannuation / KiwiSaver
    "social": "1057.50",        // DE: KV+RV+AV+PV employee share
    "wage_types": [ "…" ]        // PE-native packs: full statutory line breakdown
  }],
  "evidence_id": "cev_…"
}

Rows with all-zero amounts are engine truth, not errors: employees active in the period but with no pay items yet. Currency lives on the run's totals, never per entry.

Payroll

Shadow runs

Roadmap — not yet available

Prove the numbers before anything goes live. Post the inputs from a real payrun together with your incumbent provider's results; Ledra Pay calculates independently and returns a line-level variance report. Shadow runs never lodge, never pay, and never touch production state.

POST/tenants/{tenantId}/shadow-runsROADMAP Create shadow run with inputs and incumbent results
GET/tenants/{tenantId}/shadow-runs/{id}ROADMAP Variance report: matched, divergent, and unexplained lines
{
  "shadow_run": "sh_c41e",
  "summary": { "entries": 640, "matched": 634, "divergent": 6, "max_variance": "4.85" },
  "divergences": [{
    "employee": "emp_99c2", "line": "tax.stsl",
    "incumbent": "128.00", "ledrapay": "123.15",
    "explanation": "Incumbent applied 2024-25 STSL thresholds after 1 July"
  }]
}

Payroll

Calculation engine

Stateless gross-to-net for embedding calculations in your own UX — quote a net salary in onboarding — without creating payroll objects. Tenant-scoped and capability-gated: stateless_calculate is native on PE-native (DE) bindings and unsupported elsewhere (501). The unscoped POST /v1/calculate is a 501 stub that returns the capability matrix — always call the tenant-scoped route.

POST/tenants/{tenantId}/calculateGross-to-net: tax, social contributions, employer costs. 501 CAPABILITY_NOT_SUPPORTED on a non-native binding
POST/calculateSTUB Unscoped — returns 501 with alternatives (use the tenant-scoped route)
POST/calculate/award-interpretROADMAP AU: interpret award rates, penalties, loadings for a shift
POST/calculate/leaveROADMAP Leave entitlements and accruals
POST/calculate/terminationROADMAP Final pay including unused leave and termination payments

Payroll

Statutory lodgement

Rail status — roadmap, not yet callable. No lodgement routes are live today. The state machine ends at finalised; lodged exists as a Phase-1 (AU) target. Australia (STP Phase 2 over ATO SBR) is the lead rail, in build with sandbox simulation planned. DE statutory rails (ELSTER + GKV/ITSG) are a design-partner build, advertised on the DE pack as design_partner_next and gated by the statutory_lodgement capability (unsupported on DE today).

Roadmap — not yet available

POST/tenants/{tenantId}/pay-runs/{prId}/lodgeROADMAP Lodge to the authority via the pack's rail (STP2 / RTI / payday)
GET/tenants/{tenantId}/lodgementsROADMAP List lodgements with authority responses
GET/tenants/{tenantId}/lodgements/{ldgId}ROADMAP Lodgement detail, receipt, and error remediation hints
GET/tenants/{tenantId}/pay-runs/{prId}/statutory-outputsROADMAP Statutory output files for packs without a rail

Payroll

Payslips & payments

GET/tenants/{tenantId}/pay-runs/{prId}/payslipsPer-employee payslips from the persisted entries — available after preview or finalise (gross, net, wage-type lines). pdf is null today
POST/tenants/{tenantId}/embed/tokenMint a short-lived, tenant-scoped token for embeddable SDK components. Body: components (write allowlist, enforced), optional employee_id (employee pinning) and origin (origin binding). Partner keys only — embed tokens cannot mint
GET/tenants/{tenantId}/payslips/{psId}/pdfROADMAP Download payslip PDF (white-label theming) — payslip_pdf is unsupported on PE today
GET/tenants/{tenantId}/pay-runs/{prId}/paymentsROADMAP Payment instructions and bank files (e.g. ABA, Bacs)
POST/tenants/{tenantId}/super-batchesROADMAP AU: SuperStream contribution batches
GET/embed/componentsROADMAP List embeddable components — the SDK ships the catalogue today
Money movement: Ledra Pay does not hold or move funds. Bank/super file generation (ABA, SuperStream) is AU-only and capability-gated; finalise against the DE/PE engine commits the payrun and persists entries but does not emit payment files.

Trust

Governance

Every state-changing call writes an immutable evidence record into a hash chain: actor, reason, inputs digest, pack version, result digest. When a regulator, auditor, or customer asks "why was this payslip this number", the answer is one API call.

GET/tenants/{tenantId}/governance/evidence/{evdId}Single evidence record
GET/tenants/{tenantId}/governance/audit-trailQuery by entity, actor, or date range
POST/tenants/{tenantId}/governance/verifyVerify chain integrity over a range

Trust

Reference data

Roadmap — not yet available

GET/reference/{packCode}/tax-tablesROADMAP Current tax scales and thresholds for a pack version
GET/reference/AU/awardsROADMAP Modern Awards with classifications
GET/reference/AU/awards/{awardCode}ROADMAP Award rates, penalties, loadings
GET/reference/AU/super-fundsROADMAP Super fund directory (USI lookup)
GET/reference/{packCode}/leave-typesROADMAP Leave type definitions per jurisdiction
GET/reference/{packCode}/income-typesROADMAP Statutory income type codes (e.g. STP income types)
Available today: the pack registry itself — GET /v1/packs and GET /v1/packs/{packCode} return statutory scope, versions and lodgement-rail status. The granular reference endpoints above are roadmap.

Integrate

Webhooks

Roadmap — not yet available

Subscribe to events; deliveries are signed (Ledra Pay-Signature, HMAC-SHA256) and retried with backoff for 72 hours.

POST/webhooksROADMAP Create subscription with event filters
GET/webhooksROADMAP List subscriptions
DELETE/webhooks/{id}ROADMAP Remove subscription
EventFires when
pay_run.previewedPreview calculation completes
pay_run.finalisedRun committed to the engine; entries persisted (bank/super files AU-only)
lodgement.accepted / lodgement.rejectedAuthority responds to a submission
shadow_run.completedVariance report ready
employee.created / employee.terminatedEmployee lifecycle changes
pack.version_releasedA statutory update ships for a pack you use

Integrate

MCP & agents

The REST write path is already governance-first: write operations require a governance_reason, supplied either as a body field or the x-governance-reason header — it lands in the evidence chain on every state-changing call. Omitting it returns 400 governance_reason_required.

Roadmap — MCP server not yet available

Ledra Pay will ship an MCP server so AI agents can operate payroll safely. Every tool carries a risk tier; all write tools require a governance_reason; high and critical tools support human-approval gates configured per partner. The tool catalogue below is the planned surface, not yet callable.

Tool ROADMAPRiskDescription
list_employees · get_employee · list_pay_runs · get_pay_runLOWRead operations with governance hashes
calculate_wage · interpret_award · employee_pay_summaryLOWStateless calculations
create_timesheet · approve_timesheet · preview_pay_runMEDReversible writes, no money movement
create_employee · update_employee · create_pay_run · update_pay_entryHIGHWrites requiring governance_reason
finalise_pay_run · submit_to_authority · terminate_employee · run_full_payrollCRITIrreversible or outward-facing; approval-gated by default
get_audit_trail · verify_chain · payroll_readiness_check · run_shadow_runLOWTrust and verification surface

Integrate

Errors

Errors are JSON with a stable code, human message, and — for pack validation failures — the statutory rule that rejected the input. code values are snake_case (pack_validation_failed, state_conflict, governance_reason_required), except the load-bearing CAPABILITY_NOT_SUPPORTED.

{
  "error": {
    "code": "pack_validation_failed",
    "message": "tax_profile.tfn failed checksum validation",
    "rule": "AU-2026.1/identity/tfn_algorithm",
    "status": 422
  }
}
StatusMeaning
400 / 422Malformed request (bad_request, governance_reason_required) / pack validation failure (pack_validation_failed)
401 / 403Invalid key / tenant not in your partner portfolio
404Object not found within your isolation boundary
409state_conflict (e.g. finalising an unapproved run, adding entries outside draft); idempotency replay mismatch
429Rate limited — honour Retry-After
501CAPABILITY_NOT_SUPPORTED — the bound pack cannot do this (e.g. stateless_calculate on AU, statutory lodgement on DE, a non-today as_of/effective_from on a non-native engine). Body carries capability, pack, and alternatives — never a wrong answer