DOCS
Test mode Trial ? Sign in

GUIDES / GETTING STARTED

Getting started

Calculate every tax line on a paycheck with one request. You’re in test mode, and the first 14 days are free.

01

Get your key

Your test key starts with sk_test_ and was shown once at sign-up. You can create a new one from your account at any time.

02

Make your first call

POST a paycheck to /v1/paycheck. The example on the right works as-is with your test key.

03

Read the tax lines

Each entry in result.taxes[] names the tax, who pays it, its jurisdiction, the taxable wages and the amount, with a detail explaining the math.

Authentication

Authentication

Send your secret key as a Bearer token in the Authorization header of every request. Keep keys on your server, never in a browser or mobile app.

Authorization: Bearer sk_test_...
sk_test_Test key. Same API, same results. For building and your trial.
sk_live_Live key. Metered and billed at $0.09 per paycheck.

Key lifecycle

Issued onceA key is issued at the end of signup, once a card or bank account is on file. Nothing is charged during the 14-day trial. The full key is shown once. Omnia.tax stores only a SHA-256 hash, so a lost key can be replaced but never recovered.
One per accountIssuing a new key revokes the previous one immediately.
Trial keys expireA trial key is time-boxed and GET /v1/me returns its exact expiresAt. An expired key returns 401 expired_key.
Server-side onlyA key can run any calculation on your account and every successful call is billable. Never ship it in a browser, a mobile app or a public repository.

Guides

Making requests

Requests and responses are JSON. Send Content-Type: application/json on any request with a body. Every endpoint below is served from this host; a self-hosted deployment answers the same paths on its own domain.

StatelessEach call is one self-contained calculation, so you can run a whole pay run in parallel, one request per employee.
RepeatableThe same input on the same checkDate always returns the same result. The only thing a call changes is usage metering.
No hidden defaultsFacts the engine cannot derive, such as your unemployment rate, nexus or a reciprocity election, are explicit inputs. Leave an optional block out and the rules that read it do not fire.

The same routes also answer under /api/ (for example /api/paycheck) with identical behavior.

Guides

Money & rounding

Every monetary value in a request or response is an integer number of cents. 300000 is $3,000.00 and a net pay of 219432 is $2,194.32. There are no floating-point dollars anywhere. Convert at your display edge only.

To the centWithholding is computed to the cent. Set roundToWholeDollars: true to round federal withholding to whole dollars, as IRS Publication 15-T permits.
RatesA percentage that is an input, such as a state unemployment rate, is a decimal: 0.031 is 3.1%.
UnsignedAmounts are never negative.

Guides

Rate limits

Each key is limited independently on a fixed one-minute window. The default is 120 requests per minute per key. A self-hosted deployment sets its own with RATE_LIMIT_PER_MIN. The headers in Response headers carry your current budget, so you can pace a pay run without guessing.

Over the limit you get 429 rate_limited. Wait for the Retry-After seconds and cap your own concurrency rather than running into the limit.

async function withRetry(call) {
  for (;;) {
    const res = await call();
    if (res.status !== 429) return res;
    const seconds = Number(res.headers.get("retry-after")) || 1;
    await new Promise((r) => setTimeout(r, seconds * 1000));
  }
}

Guides

Versioning & dates

Rules are effective-dated. The ruleset that runs is the one in force on your checkDate, never today’s. This is the most important thing to get right when you integrate.

CorrectionsRecalculating a prior period reproduces that period’s numbers because you send that period’s checkDate.
New yearThe first payroll dated in a new year picks up that year’s brackets, wage bases and local rates automatically.
Data, not breaking changesNew jurisdictions and rate changes ship as data. The request and response shapes on this page are stable. A new local tax is one more line in taxes, nothing more.
Tax yearsA checkDate outside the years this build covers returns 422 invalid_input. GET /v1/health lists the covered years as taxYears.
{ "checkDate": "2025-12-19", ... }   // 2025 rules
{ "checkDate": "2026-01-02", ... }   // 2026 rules, automatically

API reference

POST /v1/paycheck

Calculate one paycheck and get back every tax line that applies.

All money in integer cents. 100000 = $1,000.00
FieldTypeDescription
CORE FIELDS
checkDatestringPay date, YYYY-MM-DD. Selects the rules in effect on that date.
payFrequencyenumweekly, biweekly, semimonthly, monthly, quarterly, semiannual, annual, daily. How often this employee is paid.
earnings[]arrayEarning: { code, category, amount }. One entry per earning on the check. At least one is required.
deductions[]arrayDeduction: { code, category, amount }. Pre-tax and post-tax deductions. Send [] if there are none.
federalW4objectFederalW4: { filingStatus, multipleJobs, dependentCredit, otherIncome, deductions, extraWithholding }. The employee’s federal Form W-4 (2020 or later).
ytdobjectYearToDate: { socialSecurity, medicare, futa }. Year-to-date wages before this check.
workStateobjectStateWithholding: { code, certificate }. Where the work happens, plus that state’s certificate (e.g. workCity). Omit for federal only.
OPTIONAL
residenceStateobject{ code, certificate }. Where the employee lives, when it differs from workState. Leave it out and every residence-based rule (reciprocity, nonresident allocation, resident credits) does not fire.
residenceStateWithholdingobject{ nexus?, voluntary? }. Whether to withhold the residence state’s tax when there is no reciprocity. Both are business facts only you know. Ignored when reciprocity already governs the period.
employerobjectEmployer facts the engine cannot derive, most importantly your assigned unemployment rate per state, for example { "stateUnemploymentRate": { "OH": 0.031 } }. Absent, the state’s published new-employer rate is used and noted in that line’s detail.
employmentCategoryenumstandard, clergy, statutory_employee, household, agricultural, railroad, election_worker. Which employment-tax rules apply. Defaults to standard.
priorRegularPaymentobject{ taxableWages, stateIncomeTaxWithheld? }. For states that tax a stand-alone bonus by aggregating it with the last regular check. Omit it and the bonus is taxed on its own.
hoursWorkednumberHours in this pay period, for minimum-wage checks.
roundToWholeDollarsbooleanRound federal withholding to whole dollars. Defaults to false.

Success shape

{ result: { checkDate, grossPay, pretaxDeductions, posttaxDeductions, employeeTaxTotal, employerTaxTotal, netPay, taxes: [ { id, name, payer, jurisdiction, taxableWages, amount, detail } ] } }

Example response

Real engine output for a $3,000.00 biweekly check dated 2026-08-15 with a $240.00 401(k) deferral, an Ohio work state and an assigned Ohio unemployment rate of 3.1%. Each line also carries a detail, left out here for length.

{
  "result": {
    "checkDate": "2026-08-15",
    "grossPay": 300000,
    "pretaxDeductions": 24000,
    "posttaxDeductions": 0,
    "taxes": [
      { "id": "US_FIT",    "name": "Federal Income Tax", "payer": "employee", "jurisdiction": "federal", "taxableWages": 276000, "amount": 26758 },
      { "id": "US_SS_EE",  "name": "Social Security",    "payer": "employee", "jurisdiction": "federal", "taxableWages": 300000, "amount": 18600 },
      { "id": "US_MED_EE", "name": "Medicare",           "payer": "employee", "jurisdiction": "federal", "taxableWages": 300000, "amount": 4350 },
      { "id": "OH_SIT",    "name": "Ohio Income Tax",    "payer": "employee", "jurisdiction": "state",   "taxableWages": 276000, "amount": 6860 }
      // employer lines: US_SS_ER, US_MED_ER, US_FUTA, OH_SUI_ER
    ],
    "employeeTaxTotal": 56568,
    "employerTaxTotal": 34050,
    "netPay": 219432
  }
}

API reference

GET /v1/states

The states and DC this build can compute, read from the rulesets on the server, so the list never claims a jurisdiction the engine cannot calculate. Use it to fill a dropdown or to check a workState.code before you send a paycheck. No key needed.

yearThe tax year listed. Pass ?year=2025 to ask for another covered year. An unknown year falls back to the latest.
statesArray of { code, name }, sorted by name. code is the two-letter USPS code.
curl https://your-host/v1/states

{ "year": 2026, "states": [ { "code": "AL", "name": "Alabama" }, { "code": "AK", "name": "Alaska" }, ... ] }

API reference

GET /v1/me

What the calling key is: its mode, plan, expiry and call counts. Send the key as a Bearer token. Use it to check a key before a pay run or to show its expiry in your own admin screen. It does not count as a billable call.

FieldTypeDescription
keyPrefixstringThe non-secret start of the key.
modeenumtest or live.
planstringFor example evaluation, trial or active.
createdAt / expiresAtstringISO timestamps for this key.
expiredbooleanWhether expiresAt has passed.
lastUsedAtstring | nullWhen the key last made a call.
totalCallsnumberLifetime calls on this key.
callsTodaynumberSuccessful calls in the last 24 hours.
limitsobject{ requestsPerMinute }.
curl https://your-host/v1/me -H "Authorization: Bearer sk_test_..."

{ "keyPrefix": "sk_test_a1b2c3", "mode": "test", "plan": "trial",
  "createdAt": "2026-10-01T12:00:00.000Z", "expiresAt": "2026-10-15T12:00:00.000Z", "expired": false,
  "lastUsedAt": null, "totalCalls": 0, "callsToday": 0, "limits": { "requestsPerMinute": 120 } }

API reference

GET /v1/health

An unauthenticated liveness and readiness probe for a load balancer or uptime monitor. It reports the API version and how many states this build can compute, so a probe also catches a deploy that shipped without its tax data. Returns 200, or 503 with status: "degraded" if the rule data or the account store cannot be read. Also answers at /healthz.

curl https://your-host/v1/health

{ "status": "ok", "version": "1.0.0", "states": 51, "store": "ok",
  "taxYears": [2025, 2026], "time": "2026-10-01T12:00:00.000Z" }

Tax lines

Tax lines

Each object in result.taxes is one line. A jurisdiction this build does not compute comes back as a line whose detail starts with NOT MODELLED and whose amount is 0, with the detail telling you not to treat that as zero tax. That is separate from calculation_error, which is an engine failure after the input validated.

idStable id for the line.
nameHuman-readable tax name.
payeremployee or employer.
jurisdictionfederal, state, or local.
taxableWagesInteger cents.
amountInteger cents withheld or owed.
detailThe math, or a NOT MODELLED explanation.

Errors

Errors

Every error returns the same three fields with a matching HTTP status. A missing rule is never returned as a silent zero.

errorA human-readable message.
codeA stable, machine-readable code from the table below.
requestIdIdentifies the request. Include it when you contact us.
detailsinvalid_input only. An array of { path, message }. path is a dotted path such as earnings[0].amount.
HTTPCodeWhen
400invalid_jsonThe request body isn’t valid JSON. The message is “Request body must be valid JSON.”
401missing_keyNo API key was sent in the Authorization header.
401invalid_keyThe key isn’t recognized, or it is inactive.
401expired_keyThe key has expired. The message includes the expiry timestamp. Create a new one from your account.
402payment_method_requiredA card or bank account must be on file, even during the 14-day trial.
402account_suspendedThe account is suspended for a billing issue, so calls are refused.
422invalid_inputThe request did not pass validation. details lists each field error as { path, message }.
422calculation_errorThe input was valid, but the paycheck couldn’t be calculated. This is not the “not modelled” line — that is a normal tax line with a NOT MODELLED detail.
429rate_limitedToo many requests from this key in the current minute. The default limit is 120 requests per minute, per key. Retry after the number of seconds in the Retry-After header.

Response headers

Response headers

Every response carries a request ID. Rate limits are per key, per minute.

HeaderSent onMeaning
X-Request-IdEvery responseIdentifies the request. Include it when you contact us.
Omnia-ModeOnce the key is checked (not on 401s)test or live: the mode of the key that made the call.
RateLimit-LimitOnce past the rate limiter, including 429 and later errorsRequests this key may make per minute.
RateLimit-RemainingOnce past the rate limiter, including 429 and later errorsRequests left in the current minute.
RateLimit-ResetOnce past the rate limiter, including 429 and later errorsUnix epoch seconds when the current window resets.
Retry-After429 rate_limitedSeconds to wait before retrying.

Next Tax lines

Objects

Earning

One line of pay. Amounts are integer cents.

FieldTypeDescription
codestringYour own label, for example REG, OT or BONUS. Passed through untouched.
categoryenumDrives how the line is taxed: regular, supplemental, imputed, reimbursement, housing_allowance. supplemental may use a flat supplemental rate. imputed is taxable but not paid in cash. reimbursement is paid in cash but not taxable. housing_allowance is only excluded when employmentCategory is clergy.
amountintegerThe amount of this line, in cents.
[ { "code": "REG",   "category": "regular",      "amount": 300000 },
  { "code": "BONUS", "category": "supplemental", "amount": 50000 } ]

Objects

Deduction

One deduction from pay. The category decides which taxes it reduces: the same deferred dollar can be exempt from one tax and fully taxable under another.

FieldTypeDescription
codestringYour own label, for example 401K or MED.
categoryenum | nullnull means post-tax: it reduces net pay and changes no taxable base. Otherwise one of section125, hsa, fsa, dependent_care, deferral_401k, deferral_403b, deferral_457, deferral_simple, commuter.
amountintegerThe amount withheld this period, in cents.
[ { "code": "401K", "category": "deferral_401k", "amount": 24000 },
  { "code": "MED",  "category": "section125",    "amount": 12500 },
  { "code": "GARN", "category": null,            "amount": 10000 } ]

Objects

FederalW4

The employee’s Form W-4 (2020 revision or later). Dollar fields are in cents.

FieldTypeDescription
filingStatusenumsingle, married_joint, married_separate, head_of_household.
multipleJobsbooleanStep 2 checkbox: two jobs, or a spouse who works.
dependentCreditintegerStep 3: the annual credit amount as entered.
otherIncomeintegerStep 4(a): other annual income.
deductionsintegerStep 4(b): annual deductions beyond the standard deduction.
extraWithholdingintegerStep 4(c): extra withholding per pay period.
exemptbooleanOptional. The employee claimed exempt from federal withholding.
nonresidentAlienbooleanOptional. Applies the Publication 15-T nonresident-alien adjustment.
voluntaryWithholdingAgreementbooleanOptional. The one way a minister’s pay carries federal withholding. Ignored for other categories.
{ "filingStatus": "married_joint", "multipleJobs": true, "dependentCredit": 400000,
  "otherIncome": 0, "deductions": 0, "extraWithholding": 2500 }

Objects

YearToDate

Year-to-date wages in cents, needed for every wage-capped tax. For a new hire or the first check of a year, send zeros. The engine never assumes prior wages you did not state.

FieldTypeDescription
socialSecurityintegerYTD wages subject to Social Security. Caps that line.
medicareintegerYTD wages subject to Medicare. Also drives Additional Medicare.
futaintegerYTD wages subject to FUTA.
supplementalintegerOptional. YTD supplemental wages. Drives the mandatory 37% federal rate past $1,000,000.
stateUnemploymentmapOptional. YTD wages per state unemployment tax, keyed by state code.
statePaidLeave, stateDisabilityEmployee, stateLongTermCaremapOptional. Per-state YTD toward paid-leave, disability and long-term-care wage bases.
{ "socialSecurity": 7800000, "medicare": 7800000, "futa": 700000, "stateUnemployment": { "OH": 900000 } }

Objects

StateWithholding

Used for workState and residenceState.

FieldTypeDescription
codestringTwo-letter state code, for example PA. Check it against GET /v1/states.
certificateobjectOptional. The state’s own withholding-certificate fields (its equivalent of a W-4), such as allowances, exemptions or workCity. The shape varies by state. Omit it for the state’s default treatment.
{ "workState": { "code": "NJ" }, "residenceState": { "code": "PA" } }

{ "code": "GA", "certificate": { "filingStatus": "married_joint", "allowances": 2 } }

Objects

PaycheckResult

Returned under result on a 200. Amounts are integer cents.

FieldTypeDescription
checkDatestringEchoes the input check date.
grossPayintegerCash earnings. Excludes imputed income.
pretaxDeductionsintegerSum of all pre-tax deductions.
posttaxDeductionsintegerSum of all post-tax deductions.
taxesarrayOne tax line per jurisdiction and tax, employee and employer side.
employeeTaxTotalintegerSum of all taxes withheld from the employee.
employerTaxTotalintegerSum of all employer-borne taxes. Not withheld from the employee.
netPayintegerTake-home: gross, less pre-tax deductions, less employee taxes, less post-tax deductions.
noticesarrayOptional. Present only when a line rests on something weaker than a confirmed primary source, or the engine does not model something the input asked for. Each is { taxId, tier, note } with tier one of secondary_source, inferred, not_modelled, conflicting_sources. The amounts are still returned. Read the notice before you pay.
{ "taxId": "NH_SUI_ER", "tier": "inferred",
  "note": "New Hampshire's published new-employer rate is confirmed only through 2026-09-30; the state has not yet published the rate for this check date, so the last confirmed 1.70% was used. ..." }   // one entry of notices[]

Account

Pricing

Usage-based, never per employee per month. Every jurisdiction is included for everyone and you pay for the calculations you make: a flat $0.09 per call at any volume.

One callOne POST /v1/paycheck that returns 200 is one billable call. Authentication failures, validation errors and rate-limited requests are not billed.
Rooftop lookupsWhen a local tax turns on a precise address, the address resolution is $0.09 per address, once per address and not once per pay run.
Free trialThe first 14 days are free and unmetered. Trial keys (sk_test_) run the full engine.

To size a bill before you sign up, the public estimator takes a head count and a pay frequency. No key needed.

curl https://your-host/api/estimate \
  -H "Content-Type: application/json" \
  -d '{ "email": "you@company.com", "employees": 8000, "payFrequency": "biweekly" }'

Account

Billing

Sign in to see this account’s trial and usage. Sign in