One request, one cited answer.
The engine runs as a stateless HTTP service. Send the facts, get back the number, every assumption it made, the statute behind each rule, and on request the proof tree you can verify offline. Nothing you send is stored.
Three steps
1. Get a key
Sign in at the console with your email. A six-digit code arrives, you enter it, and your key is on the screen. The evaluation plan is free and has no expiry.
Calls without a key work too, at the anonymous rate budget (600 requests an hour). A key raises that to 6,000 and attributes usage to your account so you can see it.
2. Make the first call
A married couple, two children, $120,000 of wages, tax year 2025. Note asOf: it is the law-in-force date and it is required. The engine refuses to guess which year you mean.
curl -s https://opentax.invaro.ai/v1/tools/calculate_tax \
-H "Authorization: Bearer $OPENTAX_KEY" \
-H "Content-Type: application/json" \
-d '{
"filing": {
"filingStatus": "mfj"
},
"income": {
"wages": 120000
},
"credits": {
"qualifyingChildren": 2
},
"asOf": "2025-12-31"
}'3. Read the answer
{
"ok": true,
"target": "us.federal.net_tax",
"asOf": "2025-12-31",
"answer": "$5,746.00",
"valueCents": "574600",
"meaning": "net federal income tax",
"assumptions": [
{ "factId": "taxableInterest", "value": { "type": "money", "value": "0" },
"source": "default", "rationale": "Assumed no taxable interest absent contrary input" },
…
],
"corpusMerkleRoot": "sha256:5f34e0bc…",
"proof": { … }
}answer is formatted for people, valueCents is the integer your code should use. A negative value is a refund. assumptions lists every fact the engine filled in with a default, each with a reason. Treat that list as the questions you still have to ask the taxpayer. proof is the full derivation, and corpusMerkleRoot is the hash of the exact rule corpus that produced it.
Conventions
- Money is dollars in requests:
50000or"1234.56". Responses give cents as a string and a formatted dollar amount. - asOf picks the tax year:
2025-12-31for TY2025,2026-12-31for TY2026. - Schemas are strict. An unknown key is a 400, not a silent ignore. The exact input schema for every tool is on its page and in the OpenAPI document.
- Facts are grouped on the individual tools:
filing,income,credits,itemized,retirementand so on. Fill the groups that apply. - Nothing is stored. The facts are computed in memory and the answer returned. The usage record holds the tool name, the account, a hash of your return id, and a timestamp. Never the arguments.
Refusals
When the corpus cannot answer, the engine says so instead of estimating. The HTTP status is still 200 with ok: false, because the request was well-formed and the engine did its job. The interesting part is error.code:
| code | meaning | what to do |
|---|---|---|
| NEEDS_FACTS | a required input is missing; error.data.missing lists each fact id, type and description | collect those facts and call again |
| NO_APPLICABLE_RULE | no rule version is valid on asOf, typically a 2026 state amount the Department has not published | check the coverage matrix for the publication that unblocks it |
| UNHANDLED_ENUM_CASE | a filing status or classification combination the corpus does not encode | treat as out of scope |
| ERROR | a rule-level refusal with a message, e.g. a situation the rule declares out of scope, or a missing asOf | read the message |
Transport-level problems use HTTP status codes:
| status | code | meaning |
|---|---|---|
| 400 | INVALID_ARGUMENTS | the server's own validation message, e.g. an unrecognized key or a wrong type |
| 400 | BAD_JSON | the body is not a JSON object |
| 401 | UNKNOWN_API_KEY | the bearer key does not exist or was rotated |
| 404 | UNKNOWN_TOOL | no such tool; the response lists the valid names |
| 413 | BODY_TOO_LARGE | over 512 KB |
| 429 | RATE_LIMITED | the hourly budget is spent; Retry-After is set |
Tagging a return
Send X-OpenTax-Return-Id with any identifier you choose for the taxpayer-year the call belongs to. It is stored as a hash and shown in the console as "computed returns". One taxpayer, one tax year, any number of federal and state calls and recalculations, counts as one. This is the unit usage pricing will use later. There is no charge on the evaluation plan.
curl -s https://opentax.invaro.ai/v1/tools/compute_return \
-H "Authorization: Bearer $OPENTAX_KEY" \
-H "X-OpenTax-Return-Id: client-4821-ty2025" \
-H "Content-Type: application/json" \
-d @return.jsonVersioning
Every response carries corpusMerkleRoot, the content hash of the rule corpus that produced it. Rules are never edited in place. A change in the law adds a new version with its own validity window, so an old proof still verifies against the old corpus. A breaking change to a tool's inputs ships as a new tool name, never as a silently changed schema. The current corpus is 0.39.0.
The 15 tools
Eight compute, two verify, five discover. Start with calculate_tax for any federal figure, compute_return for the Form 1040 line set from documents, and compute_state_return for a state return. The full list has the rest.