State tax docs / Library
The state tax engine as a library
The engine behind POST /api/state-tax is also available as a library you run inside your own application: all 50 states and the District of Columbia, the same rules, the same citations, and the same output, byte for byte. No network call, no per-request latency, and no household data leaving your system.
One engine, the same answer
The library is compiled from the same C sources as the API, and its JSON entry point uses the API's own request evaluator. Every build is checked by a parity gate: a generated corpus of 41,453 requests (every state, every filing status, incomes from zero to tens of millions, every age threshold the engine uses, every income type, some 2,400 couples with their own ages and incomes, and deliberately invalid requests) is run through the C library, the WebAssembly package, the Python package and the production API handler, and all four must return identical bytes for every request.
One build setting matters more than it looks: the library is always compiled without fused multiply-add. When we compiled the same sources with it allowed, 268 of 6,814 test results changed in their last digits. The build pins the setting and the test suite rejects a binary that contains a fused instruction; the parity gate then requires the library and the API to agree to the byte on every request of its corpus.
Packages
| Package | For | Size | In a licensed build |
|---|---|---|---|
| C library (shared + static), header, CMake and pkg-config files | C, C++, and any language with a C foreign-function interface | 2001 KB download | Yes |
| npm package (WebAssembly + TypeScript types) | Browsers, Node.js 18+, web workers | 609 KB gzipped module | No |
| Python wheel | CPython 3.9 and later, one wheel for all versions | 989 KB wheel | No |
| Examples for Go, Rust, Java 22, .NET 8 and Swift | Calling the C library from those languages | source | No (a C example is) |
The Linux build targets the manylinux2014 baseline (glibc 2.17), so one binary needs nothing newer than glibc 2.17 (its newest symbol version is 2.14). Linux on ARM64, macOS and Windows have no binary built or tested today, so there is no evaluation build for them yet: each needs its own build and a full parity run first. Say in your first email which platform you need. Every release is reproducible: rebuilding it from the same sources produces the same bytes, and ships with SHA-256 checksums.
A licensed build is the native C library above, for server-side use, delivered privately by email; the WebAssembly and Python packages and the other languages' examples are not delivered under a licence.
Each package also comes without citations: a build that leaves out the
official sources the JSON response cites (rules.sources, and the statements and sources of the indexing rules a projected response cites). Its every response is
byte-identical to the full library's answer to the same request with
"includeSources": false, and it is 42% smaller (the shared library 2327 KB instead of 4003 KB): a 1312 KB C
download, a 429 KB gzipped WebAssembly module and a 629 KB
Python wheel. The sources stay published on the documentation pages. Both builds pass the
same tests and the same byte-for-byte comparison with the API, and both have size budgets
the release build enforces.
Using it
These packages are not published on npm or PyPI. A licensed build is delivered privately, by email, as the native C library (the C sample below); the JavaScript and Python samples show the same interface in packages that are not part of a licensed build.
JavaScript or TypeScript, in the browser or in Node:
import loadStateTax from '@quantcalc/statetax';
const st = await loadStateTax();
const r = st.compute({
state: 'NY', filingStatus: 'mfj', age: 68,
ordinaryIncome: 92000, socialSecurity: 34000, capitalGains: 15000,
});
r.tax; // the same figure the API returns
r.rules.socialSecurityRule; // and the rules that produced it
Python:
import quantcalc_statetax as st
doc = st.compute({"state": "NY", "filingStatus": "mfj", "age": 68,
"ordinaryIncome": 92000, "socialSecurity": 34000})
A couple, each with their own age and income (married filing jointly):
const r = st.compute({
state: 'AL', filingStatus: 'mfj', age: 66, pensionMilitary: 30000, iraDistributions: 20000,
spouse: { age: 64, pensionPublic: 25000, iraDistributions: 9000 },
});
r.perPerson; // each spouse's exclusions and the rule behind each one
C:
qcst_request req;
qcst_request_init(&req);
snprintf(req.state, sizeof req.state, "NY");
req.filing_status = QCST_FILING_MFJ;
req.age = 68;
req.ordinary_income = 92000;
req.social_security = 34000;
qcst_result res; qcst_result_init(&res);
qcst_error err; qcst_error_init(&err);
if (qcst_compute(&req, &res, &err) == QCST_OK)
printf("%s: $%.2f\n", res.state_name, res.tax);
Each language gets two ways in. The JSON interface takes the API's
request and returns the API's response, including the full rules block. The
typed interface takes a struct or object and returns the headline figures
without any JSON, for tight loops such as a Monte Carlo projection.
Request and response
The fields are exactly those of the HTTP API, documented at /docs/state-tax-api/. The per-spouse ages and the income types (wages, IRA distributions, private, public and military pensions, Roth conversions) are explained in couples and income types. A field you do not supply is treated as absent, not as zero, exactly as when it is missing from a JSON request.
Since library 1.2 (engine 1.5.0) a request can also say that the filer is blind
(under the federal §63(f)(4) test; states with a blind amount, whether an additional
standard deduction, an exemption or a credit, apply it) or totally and permanently
disabled (read only by the state rules that condition on it, such as Idaho's
retirement-benefits deduction from age 62), and the same two facts for a spouse on a joint
return (spouse.blind, spouse.disabled). Each is an optional
true/false; left out, it is treated as false. In the typed C interface they are the
tri-state fields blind, disabled, spouse_blind and
spouse_disabled (QCST_BOOL_ABSENT, QCST_FALSE,
QCST_TRUE), and every language binding carries them.
Since library 1.8 (engine 1.10.0) a request can estimate a later tax year:
"projection": {"year": 2040, "inflation": 0.025} (or your own cumulative
cpiFactor) moves every dollar amount and birth-year age cutoff of the state's
rules the way that state's law moves it, from an indexing registry in which every rule has an
official source (how each state's amounts move); the
response says the result is projected, lists every amount that bore on it with its 2026 and
projected values and the rule that moved it, and lists the changes the state has enacted for
later years that the projection does not apply. A result that depends on an amount no
official source settles is refused. "indexing": "inflation" instead lets every amount
QuantCalc's planner scaled before September 2026 rise with inflation, whatever the law says (the
default is "law"). Every response also lists the inputs the request did not
give that could change it (inputsNotSupplied), and "strict": true
refuses keys the API does not know. In the typed C interface the projection is three fields
appended to qcst_request.
Since library 1.3 (engine 1.7.0) a request can also carry the household's
dependents (each with an age and whether they are a full-time student or
permanently and totally disabled; an empty list means none, a missing one means not said),
whether a qualifying person of an earlier head-of-household return died
(priorHohQualifyingPersonDied), a federal civil service annuity
and disability pay as their own amounts, the part of the IRA distributions
taken before age 59½ with its federal exception, the
IRA basis recovered, and whether the income is received as a
survivor or the Social Security benefits are disability
benefits; on a joint return the spouse's own copies of these and the spouse's share of the
gross Social Security, and on a separate return the other spouse's age and blindness. In the
typed C interface they are fields appended to qcst_request (a dependents array
of up to 20 entries, amounts that default to absent, tri-state flags and an exception code),
and every language binding carries them.
Library 1.4 (engine 1.8.0) adds the qualifying surviving spouse filing
status ("qss": one person on the return, each state applying its own treatment
of the status), Railroad Retirement (the Tier 2 annuity, which no state
taxes, and the gross Tier 1 benefits, which are part of the gross Social Security),
401(k), 403(b) and 457(b) distributions as their own amount (early
distributions can then come from them too), qualified dividends, and, in
Indiana and Maryland, the filer's own county tax rate in place of the
statewide average (a rate outside the state's published county range, or in another state,
is rejected). On a joint return the spouse carries their own railroad and employer-plan
amounts. Income amounts are now all range-checked the same way (finite, not negative, at
most $100 billion), including the three original ones. In the typed C interface they are
eight amounts appended to qcst_request and a fifth filing status, read only
when the caller's struct_size covers them.
Library 1.5 (engine 1.8.0) completes the federal tax figures and three facts about a
person. federalTax is the federal tax after the nonrefundable credits (Form 1040
line 22); the federal tax before credits (which Nebraska caps its tax at and
which limits the Oklahoma child credit) and the federal refundable credits
(which Alabama's federal tax deduction subtracts) are now their own amounts. For disability
pay, the plan's minimum retirement age, from which the pay is a pension
(Connecticut, Maine, Ohio); for a surviving spouse, the age the deceased
spouse would have reached (South Carolina's higher deduction from 65); and whether
the Social Security benefits are survivor benefits (Colorado under 55). Each
has the spouse's own copy on a joint return. In the typed C interface they are two amounts
and six whole-number or tri-state fields appended to qcst_request, read only
when the caller's struct_size covers them.
Library 1.6 carries the federal tax engine too. A
JSON request that says "computeFederal": true is answered as the API answers
it: the federal engine computes the year's federal return from the same request and supplies
the federal figures the state rules read (the taxable part of Social Security, the federal
tax before and after credits, the net investment income tax, federal taxable income), and
the response shows them in a federal object with the federal return's lines. A
request the federal engine cannot price, such as one with disability pay, is refused with the
API's message. Since library 1.9 (federal engine 2.0) it also prices wages and
self-employment income (selfEmploymentIncome, with computeFederal
only) and projects the federal return to a later year with projection. Without the option every answer is byte for byte what 1.5 returned. The state and
federal engines are one binary, so they are always the same version; a state-only build
without the federal engine remains available.
Errors
Every call returns a status code: OK, INVALID_ARGUMENT (a
programming error such as a missing pointer), INVALID_JSON,
INVALID_REQUEST (for example an unknown state, a negative amount, or an age
outside 0–120), UNSUPPORTED (such as a tax year the build does not
carry), OUT_OF_MEMORY or INTERNAL. A rejected request comes
with the API's own message, and the JSON interface also returns the API's exact
{"error": "…"} body, so your application can relay it unchanged.
Threads
Every function is safe to call from any number of threads at once, with no locking on
your side. The rate tables are built once, on first use or when you call
qcst_init(), and are read-only afterwards; there is no other shared state.
The test suite checks this under ThreadSanitizer, including sixteen threads racing to
make the very first call. The Python package releases the interpreter lock during each
call, so a thread pool computes in parallel.
Tax year 2025
Since library 1.16 (state engine 1.17.0, federal engine 2.5.0) a request can be for a
2025 return: "taxYear": 2025, or tax_year = 2025
in the typed C interface. It is computed on the law in force for 2025 returns: each
jurisdiction's 2026 rules with its 2025 changes applied, each sourced to the 2025 forms,
instructions or statute, local income taxes on their 2025 rows, and with
computeFederal the federal engine's own 2025 rules (including the 2025 changes
of P.L. 119-21). The response's taxYear is "2025", and
yearNotes says in plain language what the 2025 result rests on, naming any part
still taken from another year. A 2025 request cannot be combined with
projection, which estimates later years from the 2026 rules. The year key given twice (taxYear
and tax_year, or two cases of one) is refused. Every other 2026 request
answers exactly as before.
Which builds carry it: the full build and the licensee build (and the build
behind POST /api/state-tax). The build without citations and the
WebAssembly/npm packages carry tax year 2026 only and refuse 2025 as a year they do not carry.
qcst_build_info() says which: tax years 2025, 2026 or
tax year 2026. A licensee build computes a year only when the licence lists it; a projection is computed from the 2026 rules, so a licence that lists 2026 covers every projection year.
Versions and the annual update
| Number | Example | Changes when |
|---|---|---|
| Library | 1.27.0 | The programming interface changes. Within version 1, nothing is ever removed, so updates are drop-in replacements. |
| Engine | 1.28.0 | Any rule, rate or threshold changes. The same value the API reports as engineVersion. |
| Tax year | 2026 | The rules year the engine encodes. Since library 1.16 the full and licensee builds also carry tax year 2025. |
A correction in 1.4: in libraries 1.1 to 1.3, qcst_request_init() initialised
the library's own, larger request structure, so a C program built against an older header
could have memory just past its request overwritten. Since 1.4 the header passes the
caller's own size (qcst_request_init_sized()), and every language binding does
the same. Programs built against 1.3 are safe with 1.4 as they are; C programs built
against 1.0 to 1.2 should be recompiled against the 1.4 header, with no source change.
All three can be read at run time, along with a fingerprint of the exact rule sources compiled in. Each new tax year ships as a new engine in a drop-in release (one binary can carry more than one: see tax year 2025); corrections within a year ship as patch releases whose changelog names the states and rules affected. A request can state the tax year it expects, and the library refuses to answer with a different year's law. How each update is monitored, gated and released is described in the update cycle, and how correctness is established, state by state, in verification.
Performance
Measured on library 1.27.0 with engine 1.28.0 (2026-11-11), one core of an Intel Core i7-12800H laptop (Linux), as CPU time per call, the median of 21 runs. The machine was shared (load average 13 to 24 on 20 logical CPUs), which is why these are CPU times and not wall-clock rates; a quiet machine is faster.
| Interface | CPU time per calculation |
|---|---|
| C, typed | about 4.6 µs |
| C, JSON in and out, the default response | about 300 µs |
| C, JSON, without citations, inputs-not-supplied probes or the not-modelled list | about 38 µs |
| WebAssembly (Node), typed | about 20 µs |
| WebAssembly (Node), JSON, the default response | about 580 µs |
| Python, batch | about 14 µs per element |
Most of a default JSON response is not the tax: it lists the facts the request did not give that could change the result (re-running the engine for each), the parts of the state's law the engine does not model, and the official sources of every rule applied. Each can be turned off per request; the tax is the same. The typed interface produces none of them and runs at the same rate in both builds.
A 10,000-path, 40-year Monte Carlo projection needs 400,000 state-tax evaluations; through the typed C interface that is about 1.8 seconds of CPU. Loading the WebAssembly module and making the first call takes about 33 milliseconds of CPU in Node.
Hardware and load change these figures: one licensee measured about 276 µs for the default JSON response of libraries 1.15.5 and 1.16.0 on its own servers, when this machine measured about 180 µs for library 1.13.2. Size a workload on your own measurement.
Security and privacy
- The library is a pure calculation. No build opens a network connection or sends
telemetry. The full build and the build without citations open no files and read no
environment variables. A licensee build does: it reads its licence file, which is named by
the
QCST_LICENCE_FILEenvironment variable or loaded with one call at start-up, and when the licence loads it checks whether a debugger is attached to the process. - The WebAssembly module needs no
eval. It runs under a strict Content Security Policy that adds only'wasm-unsafe-eval'. - The test suite runs clean under AddressSanitizer and UndefinedBehaviorSanitizer, including a fuzzer that mutates requests to the JSON interface (20,000 per build; a run of one million on library 1.3.0 found no failures under either).
- Only the documented functions are exported, so the library cannot clash with other code in your application, including your own copy of the JSON parser it uses internally.
Licensing
The library is licensed commercially, for embedding in a named product and running server-side in that product. Get in touch at [email protected] with your platform (operating system, CPU architecture and the language you would call the library from) and where the calculation would run, and we will send an evaluation build. Each email gets a reply within one business day.