Skip to content

Payroll

US payroll, gross to net, in one call.

Base URL https://payroll.geoverio.com

Overview

What Payroll does

US payroll, gross to net, in one call: federal income tax on the IRS percentage method, FICA with the wage-base and Additional Medicare thresholds handled across the year, state and local withholding, and the employee-paid disability and paid-leave programs. You get back every tax and deduction, net pay, updated year-to-date figures, a render-ready pay statement your own template can print by iterating, and the items the employee's state legally requires on a wage statement. Nothing is stored: year-to-date figures travel with the request, request bodies are never logged, and no employer or employee record is kept.

Perfect for Paystub and payroll products that need the numbers but keep their own design Running a monthly pay run for a company's own employees Bookkeeping and accounting tools that reconcile take-home pay Answering "what would I actually take home" for a salary or offer

Authentication

Every request needs a key

Send your key as a bearer token in the Authorization header. That is the only accepted scheme — there is no query-string key and no cookie login.

Header Authorization: Bearer gv_live_…
Question Answer
Where do I get one? Select a plan first — the free tier counts — then open Dashboard → Projects. Keys belong to a project: open one and choose Create key. If you are not signed in yet, that link takes you through sign-in first.
What does it look like? A long string beginning gv_live_. Copy the whole thing — a truncated key returns 401.
Where do I keep it? On your server, in an environment variable. Never in browser JavaScript, a mobile app bundle, or a public repository — anyone who reads it can spend your quota.
Leaked it? Revoke it from the project that owns it under Projects and create a new one. Revoking takes effect immediately.

New here? The getting started guide walks through account, plan, key and first call step by step.

Reference

Every endpoint, explained

Copy a sample, swap in your key, and you are live. Each endpoint lists exactly what it accepts and returns.

GET /v1/paycheck/quick Quick take-home estimate

The one-line version: what actually lands in the employee's account. Everything not supplied is defaulted, so this is an estimate - use POST /v1/paycheck for a real pay run.

Parameters

Name Type Required Description Example
gross number required Gross pay for one pay period. 5000
frequency string optional weekly, biweekly, semimonthly or monthly. Defaults to biweekly. semimonthly
state string optional Two-letter work state. Defaults to TX. CA
filing_status string optional single_or_mfs, married_jointly or head_of_household. single_or_mfs
state_allowances integer optional Allowances or exemptions claimed on the state withholding certificate. 1

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body "https://payroll.geoverio.com/v1/paycheck/quick?gross=5000&frequency=semimonthly&state=CA&filing_status=single_or_mfs&state_allowances=1" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY"
// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/paycheck/quick?gross=5000&frequency=semimonthly&state=CA&filing_status=single_or_mfs&state_allowances=1", {
    headers: {
      Authorization: `Bearer ${KEY}`
    }
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/paycheck/quick?gross=5000&frequency=semimonthly&state=CA&filing_status=single_or_mfs&state_allowances=1"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}

r = requests.get(url, headers=headers)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())
<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');

$ch = curl_init("https://payroll.geoverio.com/v1/paycheck/quick?gross=5000&frequency=semimonthly&state=CA&filing_status=single_or_mfs&state_allowances=1");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));
See a sample response
{
    "meta": {
        "tax_year": 2026,
        "data_version": "2026.1",
        "tables_effective_on": "2026-01-01",
        "module_version": "0.2.0",
        "disclaimer": "Calculated estimates for record-keeping. Not tax advice. Geoverio does not file or remit taxes and does not verify any figure supplied by the caller."
    },
    "gross": 5000,
    "net_pay": 3503.56,
    "taxes": [
        {
            "code": "federal_income_tax",
            "label": "Federal Income Tax",
            "amount": 732
        },
        {
            "code": "social_security",
            "label": "Social Security",
            "amount": 310
        },
        {
            "code": "medicare",
            "label": "Medicare",
            "amount": 72.5
        },
        {
            "code": "ca_income_tax",
            "label": "California Income Tax",
            "amount": 316.94
        },
        {
            "code": "ca_sdi",
            "label": "CA SDI",
            "amount": 65
        }
    ],
    "effective_tax_rate": 29.93,
    "jurisdiction": {
        "code": "CA",
        "name": "California",
        "status": "supported",
        "method": "ca_method_b",
        "effective_from": "2026-01-01",
        "effective_to": null,
        "source": {
            "name": "California Withholding Schedules for 2026 - Method B, Exact Calculation (EDD)",
            "url": "https:\/\/edd.ca.gov\/siteassets\/files\/pdf_pub_ctr\/26methb.pdf",
            "retrieved": "2026-08-31",
            "verified_by": "Golden tests reproduce the publication's annualized worked examples E and F."
        }
    },
    "warnings": [
        {
            "code": "jurisdiction_note",
            "severity": "info",
            "message": "California taxes HSA contributions - they reduce federal wages but not California wages."
        }
    ],
    "note": "An estimate with default assumptions. POST \/v1\/paycheck for a real pay run."
}

POST /v1/paycheck Calculate one paycheck

The real thing. Send earnings, W-4 data, jurisdictions, pre-tax deductions and year-to-date figures; get every tax, net pay, updated YTD, the render-ready pay statement and the jurisdiction's statement requirements.

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body -X POST "https://payroll.geoverio.com/v1/paycheck" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;
const payload = {};

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/paycheck", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/paycheck"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}
payload = {}

r = requests.post(url, headers=headers, json=payload)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');
$payload = <<<'JSON'
{}
JSON;

$ch = curl_init("https://payroll.geoverio.com/v1/paycheck");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
    "Content-Type: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

See a sample response
{
    "meta": {
        "tax_year": 2026,
        "data_version": "2026.1",
        "tables_effective_on": "2026-01-01",
        "module_version": "0.1.0",
        "disclaimer": "Calculated estimates for record-keeping. Not tax advice. Geoverio does not file or remit taxes and does not verify any figure supplied by the caller."
    },
    "pay_statement": {
        "period": {
            "start": "2026-09-01",
            "end": "2026-09-15",
            "pay_date": "2026-09-15",
            "frequency": "semimonthly"
        },
        "sections": [
            {
                "key": "earnings",
                "label": "Earnings",
                "columns": [
                    "rate",
                    "hours",
                    "current",
                    "ytd"
                ],
                "lines": [
                    {
                        "key": "regular",
                        "label": "Regular",
                        "current": 2800,
                        "ytd": 2800,
                        "formatted": {
                            "current": "$2,800.00",
                            "ytd": "$2,800.00"
                        },
                        "rate": 35,
                        "hours": 80
                    },
                    {
                        "key": "overtime",
                        "label": "Overtime",
                        "current": 210,
                        "ytd": 210,
                        "formatted": {
                            "current": "$210.00",
                            "ytd": "$210.00"
                        },
                        "rate": 52.5,
                        "hours": 4
                    }
                ],
                "total": {
                    "key": "gross_pay",
                    "label": "Gross Pay",
                    "current": 3010,
                    "ytd": 53010,
                    "formatted": {
                        "current": "$3,010.00",
                        "ytd": "$53,010.00"
                    }
                }
            },
            {
                "key": "pre_tax_deductions",
                "label": "Pre-Tax Deductions",
                "columns": [
                    "current",
                    "ytd"
                ],
                "lines": [
                    {
                        "key": "retirement_401k",
                        "label": "401(k)",
                        "current": 150,
                        "ytd": 150,
                        "formatted": {
                            "current": "$150.00",
                            "ytd": "$150.00"
                        }
                    },
                    {
                        "key": "section125",
                        "label": "Section 125 (pre-tax benefits)",
                        "current": 100,
                        "ytd": 100,
                        "formatted": {
                            "current": "$100.00",
                            "ytd": "$100.00"
                        }
                    }
                ],
                "total": {
                    "key": "pre_tax_total",
                    "label": "Total Pre-Tax",
                    "current": 250,
                    "ytd": null,
                    "formatted": {
                        "current": "$250.00",
                        "ytd": null
                    }
                }
            },
            {
                "key": "taxes",
                "label": "Taxes Withheld",
                "columns": [
                    "current",
                    "ytd"
                ],
                "lines": [
                    {
                        "key": "federal_income_tax",
                        "label": "Federal Income Tax",
                        "current": 240,
                        "ytd": 240,
                        "formatted": {
                            "current": "$240.00",
                            "ytd": "$240.00"
                        }
                    },
                    {
                        "key": "social_security",
                        "label": "Social Security",
                        "current": 180.42,
                        "ytd": 180.42,
                        "formatted": {
                            "current": "$180.42",
                            "ytd": "$180.42"
                        }
                    },
                    {
                        "key": "medicare",
                        "label": "Medicare",
                        "current": 42.2,
                        "ytd": 42.2,
                        "formatted": {
                            "current": "$42.20",
                            "ytd": "$42.20"
                        }
                    },
                    {
                        "key": "ca_income_tax",
                        "label": "California Income Tax",
                        "current": 95.05,
                        "ytd": 95.05,
                        "formatted": {
                            "current": "$95.05",
                            "ytd": "$95.05"
                        }
                    },
                    {
                        "key": "ca_sdi",
                        "label": "CA SDI",
                        "current": 37.83,
                        "ytd": 37.83,
                        "formatted": {
                            "current": "$37.83",
                            "ytd": "$37.83"
                        }
                    }
                ],
                "total": {
                    "key": "taxes_total",
                    "label": "Total Taxes",
                    "current": 595.5,
                    "ytd": null,
                    "formatted": {
                        "current": "$595.50",
                        "ytd": null
                    }
                }
            }
        ],
        "net_pay": {
            "key": "net_pay",
            "label": "Net Pay",
            "current": 2164.5,
            "ytd": 2164.5,
            "formatted": {
                "current": "$2,164.50",
                "ytd": "$2,164.50"
            }
        }
    },
    "gross": 3010,
    "net_pay": 2164.5,
    "totals": {
        "gross": 3010,
        "pre_tax_deductions": 250,
        "taxes": 595.5,
        "post_tax_deductions": 0,
        "net_pay": 2164.5
    },
    "wage_bases": {
        "federal_income_tax": 2760,
        "fica": 2910,
        "state_income_tax": 2760
    },
    "taxes": [
        {
            "code": "federal_income_tax",
            "label": "Federal Income Tax",
            "amount": 240
        },
        {
            "code": "social_security",
            "label": "Social Security",
            "amount": 180.42
        },
        {
            "code": "medicare",
            "label": "Medicare",
            "amount": 42.2
        },
        {
            "code": "ca_income_tax",
            "label": "California Income Tax",
            "amount": 95.05
        },
        {
            "code": "ca_sdi",
            "label": "CA SDI",
            "amount": 37.83
        }
    ],
    "programs": [
        {
            "code": "ca_sdi",
            "label": "CA SDI",
            "amount": 37.83,
            "rate": 0.013,
            "taxable_wages": 2910,
            "wage_cap": null,
            "skipped": false,
            "source_url": "https:\/\/edd.ca.gov\/en\/payroll_taxes\/rates_and_withholding\/"
        }
    ],
    "locals": [],
    "ytd_out": {
        "gross": 53010,
        "federal_income_tax": 240,
        "ss_wages": 52910,
        "ss_tax": 180.42,
        "medicare_wages": 52910,
        "medicare_tax": 42.2,
        "additional_medicare": 0,
        "state_income_tax": 95.05,
        "net_pay": 2164.5,
        "supplemental_wages": 0,
        "ca_sdi": 37.83,
        "ca_sdi_wages": 2910
    },
    "employer": {
        "social_security": 180.42,
        "medicare": 42.2,
        "futa": 52.38,
        "futa_rate": 0.018,
        "futa_taxable": 2910
    },
    "reporting": {
        "qualified_tips": 0,
        "qualified_overtime": 0,
        "ttoc": null,
        "note": "Qualified tips and qualified overtime are reported (Form W-2 box 12 codes TP and TT, box 14b) but remain fully subject to income tax withholding, Social Security, Medicare and FUTA. Qualified overtime is the FLSA half-time premium only, not total overtime pay."
    },
    "jurisdiction": {
        "code": "CA",
        "name": "California",
        "status": "supported",
        "method": "ca_method_b",
        "effective_from": "2026-01-01",
        "effective_to": null,
        "source": {
            "name": "California Withholding Schedules for 2026 - Method B, Exact Calculation (EDD)",
            "url": "https:\/\/edd.ca.gov\/siteassets\/files\/pdf_pub_ctr\/26methb.pdf",
            "retrieved": "2026-08-31",
            "verified_by": "Golden tests reproduce the publication's annualized worked examples E and F."
        }
    },
    "warnings": [
        {
            "code": "jurisdiction_note",
            "severity": "info",
            "message": "California taxes HSA contributions - they reduce federal wages but not California wages."
        }
    ],
    "compliance": {
        "statement_requirements": {
            "jurisdiction": "CA",
            "authority": "California Labor Code section 226(a)",
            "source_url": "https:\/\/leginfo.legislature.ca.gov\/faces\/codes_displaySection.xhtml?lawCode=LAB&sectionNum=226",
            "required_items": [
                {
                    "key": "gross_wages",
                    "description": "Gross wages earned.",
                    "satisfied": true,
                    "conditional": false,
                    "note": null
                },
                {
                    "key": "total_hours_worked",
                    "description": "Total hours worked by the employee (non-exempt employees).",
                    "satisfied": true,
                    "conditional": false,
                    "note": null
                },
                {
                    "key": "piece_rate_units",
                    "description": "The number of piece-rate units earned and the applicable piece rate, if the employee is paid on a piece-rate basis.",
                    "satisfied": false,
                    "conditional": true,
                    "note": "Only the caller holds this."
                },
                {
                    "key": "all_deductions",
                    "description": "All deductions, itemized.",
                    "satisfied": true,
                    "conditional": false,
                    "note": null
                },
                {
                    "key": "net_wages",
                    "description": "Net wages earned.",
                    "satisfied": true,
                    "conditional": false,
                    "note": null
                },
                {
                    "key": "pay_period_dates",
                    "description": "The inclusive dates of the period for which the employee is paid.",
                    "satisfied": true,
                    "conditional": false,
                    "note": null
                },
                {
                    "key": "employee_identification",
                    "description": "The name of the employee and the last four digits of their social security number or an employee identification number.",
                    "satisfied": false,
                    "conditional": false,
                    "note": "Only the caller holds this; never send a full SSN."
                },
                {
                    "key": "employer_identification",
                    "description": "The name and address of the legal entity that is the employer.",
                    "satisfied": false,
                    "conditional": false,
                    "note": "Only the caller holds this."
                },
                {
                    "key": "hourly_rates_and_hours",
                    "description": "All applicable hourly rates in effect during the pay period and the corresponding number of hours worked at each hourly rate.",
                    "satisfied": true,
                    "conditional": false,
                    "note": null
                }
            ],
            "all_satisfied": false,
            "unsatisfied": [
                "employee_identification",
                "employer_identification"
            ],
            "note": "California also requires available paid sick leave to be shown on the statement or a separate writing (Labor Code section 246(i)); that balance is not payroll-tax data and must come from the caller."
        }
    }
}

POST /v1/paycheck/batch Calculate a whole pay run

Up to 200 employees in one call. Send shared fields once in "default" and only what differs per employee. One employee failing (an unsupported jurisdiction, say) never fails the run - it comes back in "errors" with the reason. Each employee counts as one calculation.

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body -X POST "https://payroll.geoverio.com/v1/paycheck/batch" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;
const payload = {};

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/paycheck/batch", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/paycheck/batch"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}
payload = {}

r = requests.post(url, headers=headers, json=payload)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');
$payload = <<<'JSON'
{}
JSON;

$ch = curl_init("https://payroll.geoverio.com/v1/paycheck/batch");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
    "Content-Type: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

POST /v1/paycheck/series Back-fill a run of pay periods

One employee, many periods, one call. Give a date range, a frequency and the earnings that repeat; get every pay period back with year-to-date chained across them. Periods are generated the way the calendar falls (semimonthly is the 1st-15th and 16th-to-month-end, quarters start in January), and a period that has not finished yet is never generated. Use overrides for the weeks that differ - a bonus, extra hours.

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body -X POST "https://payroll.geoverio.com/v1/paycheck/series" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;
const payload = {};

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/paycheck/series", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/paycheck/series"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}
payload = {}

r = requests.post(url, headers=headers, json=payload)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');
$payload = <<<'JSON'
{}
JSON;

$ch = curl_init("https://payroll.geoverio.com/v1/paycheck/series");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
    "Content-Type: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

POST /v1/gross-up Gross-up: the gross that produces a target net

"Give them $1,000 in hand - what does that cost in gross?" Solved by running the full engine, so wage-base caps and bracket edges are respected rather than approximated.

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body -X POST "https://payroll.geoverio.com/v1/gross-up" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;
const payload = {};

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/gross-up", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/gross-up"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}
payload = {}

r = requests.post(url, headers=headers, json=payload)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');
$payload = <<<'JSON'
{}
JSON;

$ch = curl_init("https://payroll.geoverio.com/v1/gross-up");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
    "Content-Type: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

POST /v1/employer-cost What the employee costs the employer

The employer side: Social Security and Medicare matches, FUTA including any state credit reduction, and SUTA from a rate you supply (state unemployment rates are per-employer and are not public data).

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body -X POST "https://payroll.geoverio.com/v1/employer-cost" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;
const payload = {};

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/employer-cost", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/employer-cost"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}
payload = {}

r = requests.post(url, headers=headers, json=payload)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');
$payload = <<<'JSON'
{}
JSON;

$ch = curl_init("https://payroll.geoverio.com/v1/employer-cost");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
    "Content-Type: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

POST /v1/statement/validate Check a pay statement against its jurisdiction

Does this stub satisfy the wage-statement law where the employee works? Returns the required items, which you already supply, and which are still missing.

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body -X POST "https://payroll.geoverio.com/v1/statement/validate" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;
const payload = {};

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/statement/validate", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(payload)
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/statement/validate"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}
payload = {}

r = requests.post(url, headers=headers, json=payload)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');
$payload = <<<'JSON'
{}
JSON;

$ch = curl_init("https://payroll.geoverio.com/v1/statement/validate");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
    "Content-Type: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

This endpoint takes a JSON body. The object above is a placeholder built from the documented parameters — the exact shape is described in the endpoint summary above.

GET /v1/jurisdictions Coverage matrix

Every jurisdiction with its status - supported, partial or unsupported - the method used, the official source behind it, and the local jurisdictions covered. Read this before assuming a state works.

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body "https://payroll.geoverio.com/v1/jurisdictions" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY"
// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/jurisdictions", {
    headers: {
      Authorization: `Bearer ${KEY}`
    }
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/jurisdictions"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}

r = requests.get(url, headers=headers)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())
<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');

$ch = curl_init("https://payroll.geoverio.com/v1/jurisdictions");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

GET /v1/limits/{year} Federal rates and limits

The year's Social Security wage base, Medicare and Additional Medicare thresholds, FUTA rates and credit reductions, supplemental rates, and the 401(k), HSA, FSA and commuter limits.

Parameters

Name Type Required Description Example
year integer required Tax year. 2026

Code samples

# Keep the key in the environment, not in your shell history:
#   export GEOVERIO_API_KEY="gv_live_…"

curl --fail-with-body "https://payroll.geoverio.com/v1/limits/2026" \
  -H "Authorization: Bearer $GEOVERIO_API_KEY"
// Call this from your server, never from the browser:
// front-end JavaScript ships your key to every visitor.
const KEY = process.env.GEOVERIO_API_KEY;

async function main() {
  const res = await fetch("https://payroll.geoverio.com/v1/limits/2026", {
    headers: {
      Authorization: `Bearer ${KEY}`
    }
  });

  // Never parse the body before checking the status: an expired key answers
  // 401 with a perfectly valid JSON envelope.
  if (!res.ok) {
    throw new Error(`HTTP ${res.status} — ${await res.text()}`);
  }

  console.log(await res.json());
}

main().catch((err) => {
  console.error(err);
  process.exitCode = 1;
});
# pip install requests
import os, requests

url = "https://payroll.geoverio.com/v1/limits/2026"
headers = {"Authorization": "Bearer " + os.environ["GEOVERIO_API_KEY"]}

r = requests.get(url, headers=headers)
r.raise_for_status()   # turns 4xx / 5xx into an exception instead of silent bad data
print(r.json())
<?php
// Read the key from the server environment — never commit it.
$key = getenv('GEOVERIO_API_KEY');

$ch = curl_init("https://payroll.geoverio.com/v1/limits/2026");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer $key",
    "Accept: application/json",
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status < 200 || $status >= 300) {
    fwrite(STDERR, "Request failed — HTTP $status: $body\n");
    exit(1);
}

print_r(json_decode($body, true));

Response fields

What every field means

No mystery keys. Here is a plain-English explanation of everything the API sends back.

meta
tax_year, data_version and tables_effective_on - the exact tables that produced this answer, so any calculation can be reproduced later. Also the disclaimer you should carry on the document.
pay_statement
The render-ready statement: ordered sections (earnings, pre-tax deductions, taxes, post-tax deductions), each with labelled lines carrying current and YTD amounts plus pre-formatted strings. Loop over it and print - your template needs no payroll knowledge of its own.
wage_bases
The three taxable wage bases, which are NOT the same number: federal income tax, FICA, and state income tax. A 401(k) deferral reduces the first but not the second; Pennsylvania taxes it in the third.
taxes
Every withholding line with a stable code and a display label: federal income tax, Social Security, Medicare, Additional Medicare when it applies, state income tax, state disability and paid-leave programs, and local taxes.
ytd_out
Updated year-to-date figures. Store these on your side and send them back next period: they are what makes the Social Security cap and the Additional Medicare threshold come out right in December.
employer
The employer side of the same paycheck: Social Security and Medicare matches and FUTA with any state credit reduction applied.
reporting
qualified_tips, qualified_overtime and the Treasury Tipped Occupation Code - the new 2026 Form W-2 reporting (box 12 codes TP and TT, box 14b). Neither reduces taxable wages, and qualified overtime is the FLSA half-time premium only, not total overtime pay.
compliance.statement_requirements
What the employee's jurisdiction legally requires on a wage statement, which items this response already satisfies, and which only you can supply (employee name, employer address, the last four of the SSN). This is the difference between numbers and a compliant pay stub.
warnings
Things worth knowing that never change the math: a deduction annualizing past its IRS limit, a state quirk that applies to this employee, deductions exceeding available pay.

Good to know

How to use this API well

Payroll is unforgiving in specific, well-known ways. These are the ones that matter most.

Year-to-date is an input, not just an output
Send the ytd block every time. Social Security stops at the annual wage base and Additional Medicare starts above a year-to-date threshold, so an engine that only sees this period is correct for eleven months and wrong in December for every high earner. Take ytd_out from each response and send it back next period.
The three wage bases are different numbers
A traditional 401(k) deferral reduces federal income tax wages but NOT Social Security and Medicare wages. Section 125 premiums reduce both. Roth reduces neither. Several states then override the federal treatment - Pennsylvania taxes 401(k) deferrals, California taxes HSA contributions, New Jersey taxes 403(b), 457 and ordinary Section 125. Send your deductions with their real codes and the engine applies the right matrix per jurisdiction.
An unsupported jurisdiction is an error on purpose
When this data version has no sourced tables for a jurisdiction you get a 422 that says exactly what is missing - never a zero. A silent zero is a wrong paycheck that nobody notices until an agency notice arrives. Call GET /v1/jurisdictions to see what is covered before you go live in a state.
Tables are chosen by pay date, not by year
Several states publish more than one table set inside one calendar year - Ohio changed on 1 August 2026, Utah on 1 June, Georgia retroactively, Arkansas in May. Send the real pay_date and the engine picks the tables that were in force; meta.tables_effective_on tells you which ones it used.
Batch and series are not the same thing
POST /v1/paycheck/batch is many employees in ONE pay period - this Friday's payroll for forty people. POST /v1/paycheck/series is ONE employee across many periods - reconstructing a year of pay stubs. They are different because periods are not independent: year-to-date has to chain from one to the next, which is why the series endpoint does that chaining for you rather than leaving it to your loop.
We calculate; we do not file, remit, or verify
There is no money movement and no filing here, and nothing you send is stored or verified. Keep the disclaimer on any document you generate, and make sure your terms prohibit using generated pay stubs as fabricated income verification - that is the one thing this category of product gets misused for.
Never send a full SSN
employee_ref is your own opaque reference and the API rejects anything that looks like a Social Security number. Pay statements should print at most the last four digits, and that belongs in your template, not in a request to us.

Errors

When things go wrong

Every failure is JSON, and every failure carries the same field. Here is the shape, then every status code you can actually receive.

The error envelope

The API returns one field, detail, with a human-readable explanation. Parse that and you have handled every error on every endpoint.

401 https://payroll.geoverio.com
{
  "detail": "Invalid API key"
}
Status Name What to do
401 Missing or invalid API key The Authorization header is missing, malformed, or the key does not exist. Check you copied the whole key, including the gv_live_ prefix.
403 Key not allowed Your key is valid but not allowed to call this API - usually the key was revoked or your plan does not include this module.
422 Jurisdiction not supported This data version does not have sourced withholding tables for that jurisdiction. The response says exactly what is missing. A jurisdiction we cannot source is always an error, never a zero: a silent zero is a wrong paycheck nobody notices.
422 Invalid request The body failed validation - an unsupported pay frequency, an unknown filing status, or a full SSN sent in employee_ref (never send one).
429 Rate limit exceeded You sent too many requests this minute. Check X-RateLimit-Reset for when the window resets, then retry.
400 Bad request The query string or JSON body could not be parsed — an unencoded character, a truncated body, or the wrong Content-Type. Fix the request; retrying it unchanged will fail again.
404 No such endpoint The path does not exist on this API. Check the base URL and the version prefix — every path on this page starts with /v1.
405 Wrong method Right path, wrong verb — for example a GET against an endpoint documented as POST. The reference above lists the verb for every endpoint.
429 Monthly quota exhausted Different from the rate limit above: your plan's allowance for the calendar month is gone, so waiting a minute will not help. X-RateLimit-Remaining is 0 and X-RateLimit-Reset points at the start of the next period. Upgrade your plan or wait for the reset.
500 Internal error Something failed on our side. The request was not your fault and was not billed — retry with backoff.
502 Bad gateway An upstream hop failed mid-request. Safe to retry with backoff.
503 Temporarily unavailable The API is draining or deploying. Safe to retry with backoff; honour Retry-After when it is present.
504 Upstream timeout The request took longer than the gateway allows. Safe to retry with backoff — and make sure your own client timeout is generous enough not to abandon a request the API is still answering.

Retrying safely

4xx means stop. The request is wrong and will stay wrong — fix it rather than repeating it. The one exception is 429: wait for the window in X-RateLimit-Reset and try again.

5xx means retry, but back off. Every endpoint on this page is a read, so a retry is safe. Wait 1s, then 2s, then 4s, then 8s — up to four attempts — and add a random 0–250 ms jitter so a fleet of your servers does not retry in lockstep. Give up after that and surface the failure; honour Retry-After whenever the response carries one.

Limits

How much you can call, and how fast

Two separate budgets, and you can hit either one. Quota is a monthly total, counted per account — every key in every project spends the same pool. Rate is a per-minute ceiling on how quickly you spend it. Both are reported on every response by the X-RateLimit-* headers, so your code can see either limit coming before it arrives.

Plan Requests / month Requests / minute API keys
Free 1,000 30 1
Pay As You Go Metered — no monthly cap 600 5
Starter 100,000 300 3
Growth 1,000,000 1,200 10
Scale 5,000,000 5,000 50

Pay As You Go is metered rather than capped: it has no monthly allowance to exhaust, it draws on a prepaid balance, and it stops when that balance runs out. Every other plan returns 429 with X-RateLimit-Remaining: 0 once its monthly quota is spent, until the next period begins. Full pricing lives on the pricing page.

Rate limits

Headers that keep you informed

Check these response headers to see exactly how much room you have left — before you ever hit a limit.

Header Meaning
X-RateLimit-Limit Your plan's requests-per-minute allowance.
X-RateLimit-Remaining Requests you have left in the current minute.
X-RateLimit-Reset Unix timestamp when the window resets.

Ready for your own key?

Create a free account, mint a key, and try every endpoint live from your dashboard’s Test APIs page.