Enrolling in a Dependent Care FSA?How to claim up to $7,500
Skip to main content

API Changelog

All notable changes to the NannyKeeper API, newest first.

v1.14.1MCP documentation corrections

Bundled guidance

  • Example prompts now direct clients to current tool results, with their assumptions and limitations, instead of outdated sample tax totals.
  • The privacy description explains the personal and financial information exchanged by account tools. State-account instructions document household selection for sandbox and multi-household accounts.
v1.14.0MCP transport support and connection safeguards

MCP server

  • All seven tools now share one server implementation for local and hosted connections. Existing stdio API-key configuration continues to work.
  • Hosted connections use account consent, short-lived OAuth access tokens, and revocable connections. Hosted payroll calls require an idempotency key so retries reuse the original request.
v1.13.2Payroll preview now checks your employee list the same way run does

POST /v1/payroll/preview

  • POST /v1/payroll/preview returns 400 for a duplicate employee id, a list over your plan's limit, an id that isn't on your household, or an employee whose start date falls after the pay period ends.
  • Before this, an id you don't own was dropped silently and the preview total quietly left someone out.
  • Any payload that run already accepted is unchanged.
v1.13.1Test keys stay out of real households

Two endpoints were not enforcing the test/live boundary

  • Test and live keys are meant to reach different households: a test key only sandbox ones, a live key only real ones. `GET` and `POST /v1/state-registrations` never actually checked, and the four Autopilot manage routes (`pause`, `resume`, `skip`, `disable`) checked in a way that could never fail. Both now enforce it, the same way every other endpoint already did.
  • The practical effect: a `nk_test_` key could read and write a real household's state agency account numbers — including decrypting them with `?reveal=true` — and could pause or disable Autopilot on live payroll. A mismatched call now returns `403` with a message naming which key type the household needs.
  • The Autopilot routes were also not enforcing employer ownership. Passing an `employer_id` you don't own did not fail; it silently fell back to your primary household and acted on that one instead. It now returns `403`.
  • `GET` and `POST /v1/state-registrations` now accept an optional `employer_id`, matching every other endpoint. Leave it off and you get the household your key is tied to, exactly as before — nothing to change. Pass it to reach a specific household, which is what a test key needs (its stamped household is your real one) and what a Professional account with more than one household needs to reach any but the primary.
  • ⛔ `employer_id` is not a way to switch modes. Whatever you pass is checked for both ownership and test/live, so a test key naming a real household is still refused — it just now tells you why instead of silently reading it.
  • ⚠️ No integration is affected. Every active key was checked before this shipped: all 12 are live keys pointed at real households, and there were no test-mode keys at all — so every existing call, with or without the new parameter, behaves exactly as it did. To build against the sandbox, create a sandbox household with `POST /v1/employers` using a test key, then pass its id as `employer_id`.
  • One narrow case to know about if you mint keys programmatically: a key stores the household it was created under. If a live key was somehow created while a sandbox household was selected, it would now be refused when called without `employer_id` — pass the real household id, or create a fresh key. No such key existed when this shipped.
v1.13.0State registrations a household doesn't owe

GET /v1/state-registrations — new `not_required` status

  • `status` gains a fourth value, `not_required`, alongside `registered`, `pending` and `required`. It means this household genuinely does not owe that registration — most often because the family and their employee agreed not to withhold state income tax in a state that treats withholding as voluntary.
  • Previously the status was derived from account-number presence and the registered flags alone, so a household that had opted out still came back as `required` with a registration URL, while every screen in the app told them the opposite. If your integration switches on `status`, add a `not_required` branch.
  • `registration_url` is now `null` on a `not_required` row. It is still returned for `required` and `pending` rows -- the accounts they have to open or finish -- and, on this endpoint, for `registered` rows too. Treat `required: false` rather than the presence of a URL as the signal to stop prompting. (The MCP tool additionally nulls it for `registered`, so an agent sees a URL only where there is something left to do.)
  • Each item also carries a new `required` boolean: true when there is still something to do for that account (nothing recorded, or recorded as pending), false once it is on file or the household does not owe it. If your integration branches on `registered` alone, switch to `required` — `registered` is `false` for a household that does not owe the account at all, so a `!registered` loop keeps prompting them.
  • ⚠️ Household-awareness currently covers the `withholding` account only. The `sui` row still reports `required: true` for a household whose whole workforce is family (a spouse, parent, or child under 21), where the app shows "Not required". If you surface unemployment registrations, do not treat that row as household-scoped yet.
  • Withholding rows are no longer returned for Alabama, Arizona, Arkansas, Oklahoma or South Carolina. Those states levy an income tax but exclude domestic service from withholding, so there is no account to open and no return to file -- the app has never shown that row, and this endpoint should not have either.
  • This is per-household, not per-state: two families in the same state can legitimately get different answers, and the answer changes when they hire, terminate, or turn withholding back on.

MCP server — shipped in 1.10.2

  • `get_state_filing_status` reports the new status and withholds the registration URL for it, so an agent drafting filing instructions no longer tells a household to open an account they don't owe. Its tool description now defines `not_required` for the model.
  • ⚠️ Update to 1.10.2. npm sat on 1.7.1 from June while this, the `employer_id` parameter, and the Autopilot tools were all finished, so any earlier install is missing every one of them. `npx @nannykeeper/mcp-server@latest` picks up the current build; pinned installs need the pin moved.
  • ⛔ Skip 1.9.0, 1.10.0 and 1.10.1. 1.9.0 was published from a stale build directory and contains none of the above — it is mid-June code carrying a later version number. 1.10.0 has the right code but introduces itself to your client as `1.9.0`, because the version in the handshake was a separate hardcoded string that release checklists never covered; it is now derived from the package itself and cannot drift again. 1.10.1 is functionally identical to 1.10.2 and safe to stay on. Nothing in any of them is broken or unsafe and no behaviour regressed, but npm versions are immutable, so none could be corrected in place.
  • Why the old build matters specifically: 1.7.1's tool description still promises "a registration URL for any that are missing" and has never heard of `not_required`. An agent handed that promise, a status its prompt never defines, and a null URL may well treat the null as an omission and tell the family to register anyway — the exact thing this change exists to stop. The REST endpoint has been correct for everyone throughout; it is the prompt that was stale.
v1.12.0Employee invitations actually send

POST /v1/employees/{id}/invite now emails the employee

  • This endpoint created a valid invitation but never sent the email, while reporting `email_sent: true`. It now sends the same email the dashboard sends, and `email_sent` reflects what actually happened.
  • If you were working around this by delivering `portal_url` yourself, nothing breaks — but the employee will now also get our email. Pass `send_email: false` to keep delivery entirely in your hands.
  • The endpoint now shares one implementation with the dashboard rather than its own. Three behaviors come with that: sending a new invitation cancels any pending one of the same type (previously every call minted another live token), bank invitations require direct deposit to be available, and bank invitations are limited to one per employee per 24 hours.
  • That rate limit returns HTTP 412 with the time you can retry. It does not apply when `send_email` is false, since nothing reaches the employee's inbox.
v1.11.0Bank setup docs & one retirement

POST /v1/ach/transfer is retired — use /v1/payroll/run

  • This endpoint returned `{"status": "initiated"}` without moving money — the transfer itself was never implemented. It now returns 410 Gone and has been removed from the reference, so it can't be mistaken for a working call.
  • Direct deposit is initiated by `POST /v1/payroll/run` with `payment_method: "direct_deposit"` on each employee. That single call runs the payroll and fires the ACH, with the debit authorization and large-payroll checks applied.
  • If you integrated against /v1/ach/transfer, no ACH was ever sent by it — check those payrolls in `GET /v1/payrolls` and re-run any that never funded.

POST /v1/employees/{id}/invite — now documented

  • Creates a secure portal link so an employee can enter their own bank account (`type: "bank_account"`) or tax details (`type: "tax_info"`). It was always available; it just wasn't in the reference.
  • Bank account and SSN details are never accepted as API fields, by design — the account holder enters and authorizes their own direct deposit. This endpoint is how you get them there.
  • Returns `portal_url`, valid for 7 days. Automatic email delivery isn't wired up yet, so `email_sent` now always reports false (it previously reported true without sending) — deliver the link yourself for now.

Connecting an employer's bank

  • There's no API equivalent for the employer's own funding bank yet — the employer connects it once at Settings → Bank in the app, after which your integration can run direct deposit for them indefinitely. A hosted link version is on the roadmap.
v1.10.0Test mode & sandbox households

Test-mode API keys (nk_test_)

  • You can now build against NannyKeeper without creating households in production. Create a test key at /developers/keys (Professional plan) — it looks like `nk_test_…` and works on every endpoint a live key does.
  • A test key reaches ONLY sandbox households, and a live key reaches ONLY real ones. There's no request parameter to switch — the credential you call with decides, so a test key can never write into a real household by accident.
  • Sandbox households are exempt from all outbound side effects: no email to employees, no Stripe charges, no ACH, no filings. Tax math is the real engine, so previews and payroll runs return the same numbers production would.

POST /v1/employers (test mode) — create a sandbox household

  • Same endpoint and payload as creating a real household, but called with a test key. `email` is optional in test mode (nothing is ever sent to it) and the response carries `sandbox: true`.
  • Up to 3 sandbox households per account, and they don't count toward your Professional employer limit or your bill.
  • GET /v1/employers is mode-scoped the same way: a live key lists your real households, a test key lists your sandboxes.

DELETE /v1/employers/{id} — clean up after yourself

  • Delete a sandbox household and everything in it, or pass `?reset=true` to wipe its employees and payrolls while keeping the household id so your fixtures keep resolving.
  • Test keys only. Real households can't be deleted through the API — their payroll records are tax records.
  • You can also create, reset, and delete sandboxes from /developers/keys if you'd rather click.
v1.9.0Autopilot management

GET /v1/autopilot + /v1/autopilot/history

  • New read endpoints for Autopilot — recurring auto-run payroll that generates, approves, and fires each period's payroll automatically so a household never misses a paycheck. GET `/v1/autopilot?employer_id=…&employee_id=…` returns the live enrollment, eligibility, and a 3-payday schedule preview; omit `employee_id` to list the employer's active enrollments.
  • GET `/v1/autopilot/history?employer_id=…&employee_id=…` returns the durable event ledger (newest first) — every enroll/skip/pause/generate/fire/fail decision with actor and source, for support and reconciliation.
  • Requires the `ach` scope (Plus/Professional) — Autopilot is a Plus+ feature.

POST /v1/autopilot/{pause,resume,skip,disable}

  • Manage an existing enrollment: `pause` (stops new generation), `resume` (re-checks eligibility, clears the failure counter), `skip` (skip the next period; add `through_date` to skip through a vacation), `disable` (turn off — soft-deleted so the ledger and standing ACH authorization are retained).
  • Enrolling is deliberately NOT exposed via the API — turning Autopilot on captures a standing recurring ACH authorization that must be employer-driven in the app. POST `/v1/autopilot/enroll` returns 403 pointing back to the in-app flow.
  • Note: `pause` stops NEW generation only — a payroll already in `scheduled` is a separate delayed task and still fires unless you void it from the payroll page.
  • `disable` is different: it is a revocation of the standing ACH authorization, so it ALSO voids that employee's AUTOPILOT-generated payrolls still in `scheduled` (not yet sent to the banking network). A scheduled payroll you created manually is left alone — void it yourself if you meant to cancel it. A run already firing, funding, or processing has been initiated and cannot be recalled.

MCP server (1.9.0)

  • New `get_autopilot` (read status/enrollments) and `manage_autopilot` (pause/resume/skip/disable via an `action` enum) tools. Enrolling stays in-app by design — there is no enroll tool.
v1.7.0State filing status

GET /v1/state-registrations

  • New read-only endpoint returning the employer's state agency account numbers (UC, withholding, SDI) per (state, registration_type) tuple. Last-four only by default; pass `?reveal=true` for full account numbers (audit-logged).
  • Response includes the agency name and a registration URL for any states the employer is registered in but missing an account on file. Use before generating quarterly filing instructions or prompting customers to complete state setup.
  • Write operations are not exposed via the API — state account numbers must be entered by the customer via /settings/states.

MCP server (1.7.0)

  • New `get_state_filing_status` tool. No arguments — calls the new endpoint and returns the same shape. Use before drafting filing instructions so agents can cite the actual last-four (e.g., "file Form UC-2 for Q2 — your PA UC account is ••••5678").
  • Read-only by design. Upsert of account numbers is deliberately not exposed via MCP — LLMs are very capable of fabricating plausible numbers and we don't want one silently landing on a quarterly UC return.
v1.6.0Voluntary set-aside override

POST /v1/payroll/run + /v1/payroll/preview

  • New optional `voluntary_set_aside` field on each employee in the request. Use it to override or skip the employee's recurring voluntary set-aside rule for this paycheck only.
  • Shape: `{ skip?: boolean, amount?: number }`. `skip: true` bypasses the rule for this paycheck; `amount: <number>` overrides the computed amount ($0–$9,999). Omit the field to apply the recurring rule normally.
  • The recurring rule itself (rate-of-gross or fixed-per-period) is configured via the dashboard on the employee profile — the API can only override or skip per-paycheck.
  • Voluntary set-asides are post-tax — they reduce the employee's net pay and accrue to a balance the employer holds in escrow. They do NOT affect gross pay, federal/state taxes, FICA, FUTA, SUTA, W-2 box 1, or Schedule H.

MCP server (1.6.0)

  • `run_payroll` and `preview_payroll` accept the new `voluntary_set_aside` override field. Existing 1.5.0 callers continue to work without any change — omitting the field applies the recurring rule normally.
v1.5.0Scheduled payroll

POST /v1/payroll/run — new `scheduled` return status

  • When `pay_date` is more than 5 business days in the future on a direct-deposit payroll, the API no longer fires ACH immediately. Instead the response returns `status: scheduled` and the payroll auto-fires at `scheduled_send_at` (5 biz days before pay_date).
  • New response fields: `scheduled_send_at` (ISO UTC timestamp) and `is_estimated: true`. The net pay and tax amounts returned are estimates — they're recomputed against current YTD and rate configs at fire time, which may shift the final numbers slightly if a rate fix shipped or another payroll landed in between.
  • In-window pay dates (within 5 business days) behave exactly as in v1.4.0 — `status: processing`/`pending_funding`/`completed`. No changes to that path.
  • Non-DD payrolls (check/cash) always fire immediately regardless of pay_date — no scheduled status.
  • Amount safeguards (`confirm_large_payroll`) and ACH debit authorization (`confirm_ach_debit`) gates now run at approve time for scheduled payrolls; callers still pass them through the same parameters.

POST /v1/payroll/preview

  • Response now includes `is_estimated: true` when pay_date is far enough out that a real run would schedule. Flag lets agents tell users that scheduled numbers will be recomputed at fire.

MCP server (1.5.0)

  • `run_payroll` and `preview_payroll` pass through the new `scheduled` status, `scheduled_send_at`, and `is_estimated` fields with no parameter changes — v1.4.0 callers continue to work (the new status is an additive enum value).
v1.4.0Single-call payroll (breaking)

POST /v1/payroll/run now finalizes in a single call

  • Breaking: the endpoint now creates, approves, and processes the payroll end-to-end. Previously it stopped at `status: draft` and required a human in the dashboard to finish.
  • Response `status` now reflects the real finalized state — `processing` (direct deposit ACH debit in flight), `pending_funding` (async DD funding), or `completed` (check/cash).
  • `pay_date` is now OPTIONAL. When omitted, the server picks the earliest valid pay date based on ACH submission lead time (5 business days, holiday-aware) and echoes it back as `pay_period.pay_date` in the response.
  • If `pay_date` is supplied and past the submission deadline, the API returns HTTP 400 with `details.next_valid_pay_date` so callers can self-correct.
  • Direct-deposit callers must now set `confirm_large_payroll: true` for totals >$5,000 or any single net pay >$3,000, and `confirm_ach_debit: true` for first-ever DD or when >30 days have elapsed since the last authorization. Mirrors the dashboard safety gates.

POST /v1/payroll/preview

  • `pay_date` is now optional on preview too, defaulting to the same server-computed value. Use this to validate a request before calling /v1/payroll/run.

MCP server

  • `run_payroll` and `preview_payroll` tools: `pay_date` is now optional.
  • `run_payroll`: added `confirm_large_payroll` and `confirm_ach_debit` parameters for direct-deposit safety gates.
v1.0.0Initial release

Public endpoints (no account required)

  • POST /v1/calculate — calculate household employer taxes for a single pay period across all 50 states
  • GET /v1/threshold — check if annual wages cross the IRS FICA threshold ($3,000 for 2026) and state-specific thresholds

Authenticated endpoints (Starter and above)

  • POST /v1/payroll/run — run payroll for one or more employees with YTD tax tracking
  • GET /v1/payroll/:id — retrieve a completed payroll by ID
  • GET /v1/employees — list employees for an employer (cursor-based pagination, PII-safe)
  • POST /v1/employees — create a new employee
  • GET /v1/employers — list all employers in your organization (Professional)
  • POST /v1/employers — create a new employer in your organization (Professional)
  • GET /v1/employers/:id/dd-status — check direct deposit readiness
  • POST /v1/documents/paystub — generate a pay stub PDF for a completed payroll
  • POST /v1/documents/w2 — generate a W-2 for a given tax year
  • POST /v1/ach/transfer — initiate direct deposit ACH transfer (Plus and above)

MCP server

  • Published @nannykeeper/mcp-server to npm
  • Supports calculate_nanny_taxes, check_threshold, and run_payroll tools via stdio transport
  • Compatible with Claude Desktop, Cursor, and any MCP-compliant client

Rate limits

  • Free tier: 50 requests/day
  • Starter: 500 requests/day
  • Plus: 2,000 requests/day
  • Professional: 2,000 requests/day