Docs

A deterministic tax rules engine. Every calculation returns the figure and the ordered list of rules that produced it, each citing its provision, hashed and signed so anyone can replay it.

Quickstart

Three calls. Nothing to install, no key needed to try it.

# 1. What can this build actually do?
curl -s https://api.taxkernel.com/v1/rulesets

# 2. Calculate. Amounts are integer minor units, so $95,000 is 9500000 cents.
curl -s -X POST https://api.taxkernel.com/v1/calculate \
  -H 'content-type: application/json' \
  -d '{"ruleset":"au-2025-26","input":{"taxable_income_cents":9500000,"has_help_debt":true}}'

# 3. Check the certificate independently.
curl -s -X POST https://api.taxkernel.com/v1/verify \
  -H 'content-type: application/json' \
  -d "$(curl -s -X POST https://api.taxkernel.com/v1/calculate \
        -H 'content-type: application/json' \
        -d '{"input":{"taxable_income_cents":9500000}}' | jq '{certificate}')"

What determinism means here

It is a property the code is built to have, not a promise in a document.

The certificate

Returned beside every result. The fields below are the whole of it.

FieldWhat it is
engineVersionThe engine build that produced it.
ruleset, versionWhich rules ran, exactly. For example au-2025-26 at 1.0.0.
rulesetHashSHA-256 of the canonical artefact. Pins the rules themselves, not just their version string.
inputThe normalised input: every declared field with defaults filled in. This is what gets replayed.
inputHashSHA-256 of the canonical input.
resultLine items, the net figure, the currency and its minor unit scale.
traceEvery rule considered, in order: n, ruleId, cites, label, applied, amountCents, detail. A rule that did not fire is still here, with the guard that excluded it and the values that decided it.
traceHashSHA-256 of the canonical trace.
signature, keyIdEd25519 over the canonical bytes of everything above.
signedAtMetadata only. Not covered by the signature.

Canonical form: object keys sorted by UTF-16 code unit, no whitespace, integers only. A float in a certificate would mean a float reached the calculation, which is itself the bug.

POST /v1/calculate

POST /v1/calculate
{
  "ruleset": "au-2025-26",   // optional, defaults to au-2025-26
  "version": "1.0.0",        // optional, defaults to the newest bundled version
  "input": { "taxable_income_cents": 9500000, "has_help_debt": true }
}

Input fields are per jurisdiction and validated against that jurisdiction's own shape before the engine runs, so sending a UK field to the Australian ruleset is rejected by name rather than quietly ignored. GET /v1/rulesets/{id} returns the exact schema.

Two kinds of refusal, both deliberate. A 400 means the request is malformed. A 422 means the ruleset understands the request and will not answer it, because the combination is not encoded. Australian family Medicare levy thresholds are the current example: applying the single thresholds would understate the reduction, so the ruleset refuses instead.

POST /v1/verify

POST /v1/verify

Send the certificate, bare or wrapped as {"certificate": ...}. Verification is a replay, not a comparison: the embedded input is run back through the rules the certificate names and every hash is recomputed.

{
  "valid": false,
  "mismatches": [
    { "field": "result",
      "expected": "...", "actual": "...",
      "detail": "Replaying the input produced a net figure of 2538800 minor units where the certificate claims 1." }
  ],
  "checked": {
    "rulesetResolved": true, "rulesetHashMatches": true, "inputHashMatches": true,
    "replayMatches": false, "traceHashMatches": true, "signatureValid": false
  }
}

You do not have to take our word for any of this. The browser verifier runs the identical code in your own browser.

GET /v1/rulesets

GET /v1/rulesets

Every jurisdiction this build carries, with a status of live or scaffold, its currency and minor unit scale, its fiscal year and convention, its modules, and how many figures still require confirmation against a primary source. A scaffold is declared but not encoded, and calling it returns a refusal rather than an estimate.

GET /v1/rulesets/{id}?version=1.0.0

One ruleset in full: its input schema, its refusals, its verification table and every rule with its citation.

GET /v1/rulesets/{id}?format=artefact

The canonical JSON artefact, byte for byte, with its hash in the x-tk-ruleset-hash header. This is what the verifier fetches.

GET /v1/keys

GET /v1/keys

Every public key a certificate might have been signed under, current and retired, so a certificate keeps verifying across a key rotation. The response also states plainly whether the deployment is signing with the development key committed to the repository, which is public knowledge and proves nothing about the issuer.

The rule format

A rule is data. It carries an id, the provision it encodes, a priority that fixes evaluation order, an optional guard, and a formula drawn from a fixed whitelist.

{
  id: "resident_base_tax",
  cites: "ITAA 1997 s 4-10; Income Tax Rates Act 1986 Sch 7 Pt I",
  priority: 200,
  appliesWhen: "residency == 'resident'",
  formula: { type: "brackets", over: "taxable_income", bands: [
    { upToCents: 1_820_000, rateBp: 0 },
    { upToCents: 4_500_000, rateBp: 1600 },
    { upToCents: 13_500_000, rateBp: 3000 },
    { upToCents: 19_000_000, rateBp: 3700 },
    { upToCents: null, rateBp: 4500 },
  ]},
  accumulate: { into: "tax_on_income", op: "add" },
  verified: true
}

Guards are parsed, never evaluated as code. The language has comparison, boolean and integer arithmetic operators and nothing else: no property access, no calls, no way to reach anything but the declared variables. A decimal literal is a syntax error, because a rate is basis points.

Formula types available to every jurisdiction: brackets, bpOf, tiered, shadeIn, taper, sum, less, min, max, clamp, quantise, copy, const. Adding a jurisdiction that needs a new shape means adding to this list, never adding a branch to the engine. A test reads the engine's source and fails if it names any jurisdiction at all.

MCP

POST /mcp

The engine is a remote MCP server over streamable HTTP. Two read-only tools, calculate_tax and verify_certificate, with schemas mirroring the API. The tool descriptions tell an agent to put the trace in front of its human, because a tax figure without provenance is exactly what a language model should not be asked to vouch for.

{
  "mcpServers": {
    "taxkernel": { "url": "https://api.taxkernel.com/mcp" }
  }
}

Limits and honesty