US payroll tax, as an API call.

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.

Quickstart

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.

If the data ever goes stale, the API says so. These tables are for one tax year, and the whole product rests on re-verifying all 51 jurisdictions every autumn. Should that pass ever slip, every response grows a 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.

What this is not

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.

Endpoints

MethodPathDoes
Post/v1/paycheckGross-to-net with a full line-item receipt
Post/v1/bonusSupplemental wage withholding — both the flat and aggregate IRS methods
Post/v1/self-employmentSchedule SE tax on net profit
Post/v1/overtimeOvertime pay and the OBBBA premium deduction
Post/v1/employer-costEmployer FICA, FUTA and SUI — what an employee costs above salary
Post/v1/withholdingReal Form W-4 federal withholding, per IRS Publication 15-T Worksheet 1A
Get/v1/statesPer-jurisdiction coverage — free, no key
Get/v1/localCounty income tax — Maryland's 24, Indiana's 92 — and what is not covered
Get/v1/payrollUnemployment bases, contribution limits, SDI, reciprocity — free, no key
Get/v1/datasetEvery table as one versioned document — one call, then compute in process. Paid plans only
Get/v1/metaTax year, data version, plans — free, no key
Post/v1/keys/rotateRevoke the presented key and get a replacement, in one call
Post/v1/keys/revokeKill the presented key with no replacement

Coverage, stated plainly

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_basisCountMeans
statutory42Brackets, standard deduction and exemptions computed from the state's own law
none9No wage income tax — returns exactly zero
estimate0No jurisdiction is estimated
unset0No 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.

Every jurisdiction computes from statute. Arkansas, DC and Vermont were resolved from their own revenue departments; California and New York replaced an effective-rate estimate with their published exact-calculation schedules. One caveat worth reading before you buy: Vermont follows its 2026 withholding tables because the Department has not published an annual rate schedule yet. It is disclosed per state on /v1/states.

What comes off the cheque besides income tax

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.

FieldWhat it does
annual.insurance_deductionsThe 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.

Where each number came from

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"
  }
}
This is not decoration. A widely-syndicated payroll compilation lists Ohio's supplemental rate as 3.5%. The Department of Taxation publishes 2.75% — a 27% overstatement on every Ohio bonus. Where we cannot read a rate from the issuing department, we publish no rate rather than a compiled one. Only Montana is in that position, and it returns "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.

What comes out, not what is owed

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.

County income tax

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.

Pennsylvania and Ohio are deliberately not covered, and it is not effort. Pennsylvania has roughly 2,500 earned-income-tax jurisdictions and Ohio around 600 municipalities plus school districts, both drawn on municipal boundaries that do not follow ZIP codes. Getting them right needs address-level geocoding — a different product with a different failure mode. A ZIP-based approximation would be wrong silently, and only for people who live near a line, which is worse than not shipping it. /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.

Working in one state, living in another

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.

The District of Columbia is not a reciprocity agreement. DC levies no income tax on nonresidents at all — Form D-4A asks only whether you live outside DC and spend fewer than 183 days there, and names no states because none is relevant. Anything that lists DC as having agreements with Maryland and Virginia is understating it: work in DC and live in Texas and DC withholds nothing either.

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.

Reading the agencies corrected our own guidance, not just a rate. We had said that in Colorado, Maine, Minnesota, Oregon and Washington the smallest employers could pass the whole contribution to the employee, roughly doubling the deduction. That is backwards. The small-employer exemption relieves the employer of its half — the employee pays the same rate either way. Maine's FAQ is clearest: under 15 employees the total drops from 1% to 0.5%, and the employee's ceiling stays 0.5% in both cases. What shrinks is the employer's bill, never the worker's.

Don't burn a call per keystroke

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.

Never put your key in a browser. Every example above assumes the fetch runs on your server. A key in client-side JavaScript is a key you have published — proxy through your own backend, cache there, and your users never touch ours. If one does leak, 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.

Errors

{ "error": { "code": "invalid_parameter",
             "message": "`state` must be a two-letter US state or DC code.",
             "param": "state" } }
StatusCodeWhen
400missing_parameter · invalid_parameterBad input — param names the field
401missing_api_key · invalid_api_keyNo key, or revoked
402account_inactivePayment failed; update billing
429quota_exceededMonthly quota exhausted

Every metered response carries x-withholdwell-limit, -used, -remaining and -period, so you never have to guess where you stand.

Leaked a key?

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.

Both routes authenticate with the key being acted on. That means anyone holding your key can also rotate it — which locks you out but grants them nothing, and is strictly better than a key you cannot kill without opening a support ticket. If you have lost the key entirely, contact support and we will revoke every key on your account.

Pricing

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.

PlanPriceReachesCalls / month
Free$0Modeling2,000
Calculate$149/moModeling100,000
Payroll$899/moEverything500,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.

Data licence — $12,000/year

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.

Privacy

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.

Accuracy

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.