Gross-to-net, supplemental wages, self-employment tax and overtime — computed from published statute for 51 of 51 jurisdictions.
Published pricing. Self-serve keys. No sales call.
curl https://api.withholdwell.com/v1/paycheck \
-H "Authorization: Bearer ww_live_..." \
-H "Content-Type: application/json" \
-d '{
"gross_annual": 85000,
"state": "NC",
"filing_status": "single",
"pretax_retirement_pct": 6,
"pay_frequency": "biweekly"
}'
{
"object": "paycheck",
"annual": {
"gross": 85000,
"pretax_deductions": 5100,
"federal_taxable": 63800,
"federal_income_tax": 8748,
"federal_marginal_rate": 0.22,
"social_security": 5270,
"medicare": 1232.5,
"state_income_tax": 2679.29,
"net": 61970.22
},
"per_period": { "periods_per_year": 26, "gross": 3269.23, "net": 2383.47 },
"state_basis": "table",
"receipt": [ ... ],
"meta": { "tax_year": 2026, "data_version": "b77be68e3a09", "disclaimer": "..." }
}
Every response carries meta.data_version — a hash of the exact tax tables that produced the figure. Quote it in a support ticket and the number is reproducible.
meta.data_stale object and carries x-withholdwell-data-stale: true plus a Warning header from 1 January onward. It never fails your request — you get the number and the warning. Alert on that header.This calculates. It does not file returns, remit taxes, or move money. If you need a full payroll processor, you want an embedded payroll provider, not this. Withholdwell is for the case where you need the number and nothing else — an offer-letter tool, a comp benchmark, a contractor-payment estimate, a take-home projection.
| Method | Path | Does |
|---|---|---|
| Post | /v1/paycheck | Gross-to-net with a full line-item receipt |
| Post | /v1/bonus | Supplemental wage withholding — both the flat and aggregate IRS methods |
| Post | /v1/self-employment | Schedule SE tax on net profit |
| Post | /v1/overtime | Overtime pay and the OBBBA premium deduction |
| Post | /v1/employer-cost | Employer FICA, FUTA and SUI — what an employee costs above salary |
| Post | /v1/withholding | Real Form W-4 federal withholding, per IRS Publication 15-T Worksheet 1A |
| Get | /v1/states | Per-jurisdiction coverage — free, no key |
| Get | /v1/local | County income tax — Maryland's 24, Indiana's 92 — and what is not covered |
| Get | /v1/payroll | Unemployment bases, contribution limits, SDI, reciprocity — free, no key |
| Get | /v1/dataset | Every table as one versioned document — one call, then compute in process. Paid plans only |
| Get | /v1/meta | Tax year, data version, plans — free, no key |
| Post | /v1/keys/rotate | Revoke the presented key and get a replacement, in one call |
| Post | /v1/keys/revoke | Kill the presented key with no replacement |
Call GET /v1/states with no key and read it before you buy. Every jurisdiction is classified, and every response tells you which classification produced its number in the state_basis field.
| state_basis | Count | Means |
|---|---|---|
statutory | 42 | Brackets, standard deduction and exemptions computed from the state's own law |
none | 9 | No wage income tax — returns exactly zero |
estimate | 0 | No jurisdiction is estimated |
unset | 0 | No jurisdiction asks you for a rate |
Where a statutory state leaves something out, the API says so per state rather than in a footnote — Indiana returns "omits": "county income tax, which every Indiana county levies at 0.5%–3%". Nothing is silently dropped.
/v1/states.Fourteen jurisdictions withhold a disability or paid-family-leave contribution that is neither income tax nor FICA. /v1/paycheck subtracts them and shows each as its own receipt line. California SDI is 1.3% of uncapped wages, so it keeps growing with salary; Washington takes 0.808% for paid leave despite having no income tax at all.
| Field | What it does |
|---|---|
annual.insurance_deductions | The total withheld, and 0 in the 37 jurisdictions that levy none |
residence_state (input) | Applies a reciprocity agreement where one exists, citing the work state's own publication. Work in NJ and live in PA and Pennsylvania taxes the wages — but New Jersey still takes its disability and family-leave contributions, because those follow the work. Work in DC and live anywhere: the District taxes no nonresident at all |
state on /v1/bonus (input) | Adds the state's flat supplemental rate, and cites the department publication it was read from. Optional, so callers written against the federal-only version keep working |
supplemental_kind (input) | bonus or other. Only California distinguishes them — 10.23% on bonuses and equity, 6.6% on a separately paid holiday balance |
Two jurisdictions levy one of these and are deliberately not modeled, and /v1/states says why for each: Delaware lets the employer choose how much to deduct, so there is no universal employee rate, and Maryland does not start collecting until 1 January 2027.
Provenance is per figure, not per dataset. Every supplemental rate names the department publication it was read from, and GET /v1/payroll returns the registry so you can open the same document we did.
"state_supplemental": {
"state": "OH", "rate": 0.0275, "amount": 275, "basis": "flat_rate",
"source": {
"doc": "Ohio Department of Taxation — Employer Withholding",
"url": "https://tax.ohio.gov/business/employer-withholding",
"read": "2026-09-09"
}
}
"basis": "not_read_from_department" with the reason. A number you can't source is worse than an honest gap.Two states return no rate for reasons that are answers rather than gaps, and say which: Oklahoma publishes no flat supplemental rate at all — its 989-page withholding packet directs employers to the ordinary percentage formula — and Vermont withholds 30% of the federal amount rather than a percentage of the payment.
Everything above answers what will this person owe. POST /v1/withholding answers what does the employer take off this cheque — IRS Publication 15-T Worksheet 1A, driven by the Form W-4 the employee actually filed rather than by their circumstances. Filing status, the Step 2 multiple-jobs checkbox, Step 3 dependent credits, Step 4(a) other income, 4(b) deductions and 4(c) extra withholding all move it, and two people on identical salaries can have very different withholding and identical liability.
A Form W-4 from 2019 or earlier is a different calculation rather than a variant — no Step 2 box, no Step 3 credit, no Step 4 fields. Pass pre_2020_allowances and the endpoint switches to that path and lists anything you sent that it therefore ignored, rather than dropping it quietly.
Maryland's 24 jurisdictions and all 92 Indiana counties are modeled, from the Comptroller's Withholding Tax Facts and Departmental Notice #1. Pass locality to /v1/paycheck and it comes off the net as its own receipt line, charged on the state taxable base rather than on gross. Anne Arundel and Frederick are no longer flat, and are modeled on their graduated bands.
/v1/paycheck says so on any Pennsylvania or Ohio response, and GET /v1/local lists all eight states with local levies we do not compute.Pass residence_state and /v1/paycheck applies the reciprocity agreement where one exists, moving income tax to the home state while leaving disability and paid-leave contributions with the state the work was done in. Every agreement is read from the work state's own department, and the response cites it.
It stays an explicit input rather than something inferred, and that is not caution about the data — reciprocity only applies once the employee actually files the work state's non-residency certificate with the employer. An API that has never seen a W-4 cannot assume one was filed.
State unemployment wage bases come from the Department of Labor's own January 2026 table, and all fourteen disability and paid-leave figures from the agency that administers each one — CT Paid Leave, Colorado FAMLI, Hawaii's Disability Compensation Division, Mass.gov, Maine Paid Leave, Minnesota Paid Leave, RI DLT, and New York's Workers' Compensation Board for disability alongside the Paid Family Leave board.
The metric is per call, and that punishes exactly one kind of integrator: an interactive calculator, where dragging a salary slider can fire sixty requests in the time it takes to read this sentence. Three patterns, cheapest first.
1. Take the tables and compute in process. GET /v1/dataset returns every bracket, deduction, wage base and limit as one versioned document. One call a day, unlimited calculations, and data_version pins any figure you show back to the tables that produced it. This is the right answer for a slider, and for a payroll run over ten thousand employees.
const ds = await fetch('https://withholdwell.com/v1/dataset', {
headers: { authorization: `Bearer ${KEY}` }
}).then(r => r.json());
// Cache by ds.data_version. Re-fetch daily; it changes when the tax data does.
2. Debounce, and skip calls that cannot change the answer. If you would rather call the API live, wait for the input to settle and drop any request whose parameters match the last one. Sixty slider events become one call.
let timer, lastKey, cache = new Map();
function quote(params, render) {
const key = JSON.stringify(params);
if (cache.has(key)) return render(cache.get(key)); // free, and instant
clearTimeout(timer);
timer = setTimeout(async () => {
if (key === lastKey) return;
lastKey = key;
const r = await fetch('https://withholdwell.com/v1/paycheck', {
method: 'POST',
headers: { authorization: `Bearer ${KEY}`, 'content-type': 'application/json' },
body: JSON.stringify(params),
}).then(r => r.json());
cache.set(key, r);
render(r);
}, 250);
}
3. Round the inputs you cache on. Nobody needs a separate answer for $85,000 and $85,001. Rounding salary to the nearest $500 before it becomes a cache key collapses a slider's whole range into a few dozen calls, and the number moves by less than a rounding error on the receipt.
POST /v1/keys/rotate kills it and issues a replacement in a single call.Quota headers come back on every metered response — x-withholdwell-remaining is the one to watch — so you can see a caching change working without waiting for an invoice.
{ "error": { "code": "invalid_parameter",
"message": "`state` must be a two-letter US state or DC code.",
"param": "state" } }
| Status | Code | When |
|---|---|---|
| 400 | missing_parameter · invalid_parameter | Bad input — param names the field |
| 401 | missing_api_key · invalid_api_key | No key, or revoked |
| 402 | account_inactive | Payment failed; update billing |
| 429 | quota_exceeded | Monthly quota exhausted |
Every metered response carries x-withholdwell-limit, -used, -remaining and -period, so you never have to guess where you stand.
Rotate it yourself, immediately — you do not need to email anyone:
curl -X POST https://withholdwell.com/v1/keys/rotate \
-H "Authorization: Bearer <the key you want replaced>"
The old key stops working the instant that call returns, and the response carries the replacement. Store it — like the first one, it is shown once and cannot be recovered.
POST /v1/keys/revoke does the same without issuing a replacement, for when you want a key gone and nothing in its place.
Priced on what you can reach, not on how often you call. Call counts are a fair-use ceiling — they are not the meter, which is why they are large.
| Plan | Price | Reaches | Calls / month |
|---|---|---|---|
| Free | $0 | Modeling | 2,000 |
| Calculate | $149/mo | Modeling | 100,000 |
| Payroll | $899/mo | Everything | 500,000 |
Modeling answers what will this person owe — /v1/paycheck, /v1/bonus, /v1/overtime, /v1/self-employment. Payroll adds the endpoints that answer what does the employer actually take off, and what does this person cost: /v1/withholding, /v1/employer-cost and county tax.
They are separated because they are a different purchase with a different failure mode. A wrong estimate is a wrong number on a web page. A wrong Publication 15-T figure is a payroll error with a penalty attached, and it should not cost the same.
Coverage and reference data — /v1/states, /v1/payroll, /v1/local, /v1/meta — stay free and need no key at all. What this does not model is a buying decision, and it should not sit behind one.
Every table as one versioned document, refreshed as the data is. It is a separate purchase rather than a plan tier, and not metered, because /v1/dataset replaces calls rather than consuming them — a meter cannot price a thing whose value is not calling. Internal use within your own product; redistribution as a competing dataset is not included. support@withholdwell.com.
Every plan signs up the same way, and all of them take a card — including Free. A $0 plan with a card on file is what keeps the free tier from being farmed, and it makes upgrading a plan change rather than a fresh signup. You are not charged for Free, ever.
Quotas are a hard stop, not an overage meter — you cannot be surprised by an invoice. Need the tables on your own infrastructure instead of over the wire? That's an annual data licence; ask.
Calculation inputs and outputs are never logged or stored. The database holds an email, a Stripe customer id, a SHA-256 hash of your key, and a monthly call count. The plaintext key is shown once at creation and is unrecoverable afterwards — a breach of our database cannot be used to call the API as you.
Figures are withholding and liability estimates computed from published statute. They are a calculation aid, not tax advice. Verify against your own authority before relying on them for filing or payment. Tax tables are re-verified against each state's own revenue department every January and February; data_version changes when they do.