API reference
Who this is for: Developers integrating the hosted Takehome API who need every route, field, and error code in one place.
When you finish: You can call any documented route with the correct method, body shape, and typed error handling.
Responses follow T4127 (Payroll Deductions Formulas). Year projection responses may cite YMPE (Year's Maximum Pensionable Earnings) and CPP2 (second additional CPP) cap fields.
Generated from services/api/openapi.json (OpenAPI 3.0.3). Regenerate with node scripts/generate-api-reference.mjs.
Base URL: https://takehome.gautamkhosla.com
API version: 0.1.0
Authenticated routes expect Authorization: Bearer np_test_… or np_live_… unless noted.
GET /health
Liveness only. No versions, dependencies, or environment.
Authentication: none.
Responses
#### HTTP 200
OK
Body schema: HealthResponse
-
status("ok", required)
#### HTTP 429
Rate limited (public routes: 120 a minute per IP address) (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
Engine failed the probe
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429, 503.
GET /openapi.json
OpenAPI document generated from the same schemas as this service.
Authentication: none.
Responses
#### HTTP 200
OpenAPI envelope
Body schema: OpenApiResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
document(object, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429.
DELETE /v1/account
Delete this account: email, keys, usage, tokens, terms history and webhooks. Needs the live key and the account email as confirmation. Refused attempts (no or wrong key, a test key, a bad body, a confirmation that does not match): about 5 a minute per IP, counted per server instance, so approximate, and the IP comes only from the CF-Connecting-IP header. A deletion with the live key and the matching email is never refused by our own limits or budgets; Cloudflare's per-IP rule (10 requests per 10 seconds) still applies. Keys stop working at once.
Request body
-
confirm(string, required) The email address of the account (case-insensitive).
Responses
#### HTTP 200
Deleted
Body schema: AccountDeleteResponse
-
deleted(boolean, required) -
message(string, required)
#### HTTP 400
Confirmation missing or wrong; nothing deleted
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key, or not the live key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Too many refused attempts from this IP address (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 413, 429, 503.
POST /v1/billing/checkout
Paid checkout is deferred (ADR-006). Returns billing_unavailable.
Request body
-
plan("starter" | "growth" | "business", required) -
success_url(string, optional) -
cancel_url(string, optional)
Responses
#### HTTP 400
Unknown field, or a body that is not one JSON object
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown live key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 501
Billing unavailable; all tiers are free during early access
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 413, 429, 501, 503.
POST /v1/billing/webhook
Not listening. Billing deferred (ADR-006).
Authentication: none.
Responses
#### HTTP 404
No endpoint
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Error responses on this route
HTTP statuses that return ErrorEnvelope: 404, 413.
GET /v1/changes
Machine-readable change log.
Authentication: none.
Responses
#### HTTP 200
Changes
Body schema: ChangesResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
feed_title(string, optional) -
feed_link(string, optional) -
feed_description(string, optional) -
items(array, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429.
GET /v1/changes.rss
RSS 2.0 change log.
Authentication: none.
Responses
#### HTTP 200
RSS
Body: RSS 2.0 XML string.
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429.
GET /v1/conformance
PDOC conformance record, including every disagreement.
Authentication: none.
Responses
#### HTTP 200
Conformance
Body schema: ConformanceResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
record(object, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429.
POST /v1/deductions
Calculate T4127 payroll deductions for one pay period.
Requires a Bearer np_test_ or np_live_ key. Metering counts calculations, not HTTP requests. A batch of 25 (the largest) on POST /v1/deductions/batch counts 25. Test keys are free and unmetered. Live keys hard-stop at the plan limit; Takehome does not surprise-bill. Every money field is zero or more: a negative amount is a 400 invalid_request naming the field, and nothing is billed. A live key may make at most 3,600 calls a UTC day per account, each request counted once (a batch or a year projection is one call); above it, HTTP 429 rate_limited and nothing is counted.
Request body
T4127 request. Unknown fields are rejected by name. as_of is required by core; if omitted here the Worker sets the UTC request date and does not nearest-match a rule set.
-
as_of(string, optional) -
province(string, required) -
pay_period(1 | 2 | 4 | 10 | 12 | 13 | 22 | 24 | 26 | 27 | 52 | 53 | 240 | 2000, required) Pay periods a year: 1, 2, 4, 10, 12, 13, 22, 24, 26, 27, 52, 53, 240 (daily) or 2000 (hourly). -
gross_pay(string, required) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
calculation_option("option1" | "option2", optional) -
pensionable_earnings(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
insurable_earnings(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
federal_claim_code(integer | string, optional) Federal claim code (the TD1 box value that sets the personal amount): 0 to 10 (a whole number, or its digits as a string) or E (or e). 0 is no claim amount; E is no claim and no federal income tax; 1 (or no code) is the basic personal amount from its formula; 2 to 10 use the amount the CRA prints for that code in the claim-code table of the as_of edition. Not with federal_tc in the same request. Anything else is a 400 invalid_request naming the field. -
provincial_claim_code(integer | string, optional) Provincial claim code (the TD1 box value that sets the personal amount): 0 to 10 (a whole number, or its digits as a string) or E (or e). 0 is no claim amount; E is no claim and no provincial income tax; 1 (or no code) is the basic personal amount from its formula; 2 to 10 use the amount the CRA prints for that code in the claim-code table of the as_of edition. Not with provincial_tcp in the same request. Anything else is a 400 invalid_request naming the field. -
federal_tc(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
provincial_tcp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
cpp_months(integer, optional) CPP contributory months (PM). A whole number from 0 to 12; default 12. -
k2_method("pdoc_observed" | "t4127_literal" | "year_to_date" | "annualized", optional) annualized is another name for pdoc_observed. -
rounding_compat("t4127" | "pdoc", optional) -
ytd_pensionable_earnings(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_insurable_earnings(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_cpp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_cpp2(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_ei(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_federal_tax(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_provincial_tax(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
pay_periods_elapsed(integer, optional) Pay periods already paid this year: a whole number from 0 to pay_period. -
bonus(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
retroactive_pay(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
bonus_method("regular" | "year_to_date", optional) -
ytd_bonus(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
f5b_ytd(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
most_recent_i(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
rpp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
bonus_rrsp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_bonus_rrsp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_income(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_rpp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_union_dues(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
f5a_ytd(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
commission_income(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
commission_expenses(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
estimated_annual_expenses(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
ytd_qpp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
ytd_qpp2(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
ytd_qpip(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
prior_province(string, optional) Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
date_of_birth(string, optional) Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
cpp_exempt(boolean, optional) -
cpp_election_after_65(boolean, optional) Documented, but the engine does not read it, so it is refused by name (400 invalid_request) whenever it is sent with a value, on every calculation route; null is read as not sent. -
additional_tax_requested(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
taxable_benefits(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
union_dues(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
alimony(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
child_care_expenses(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
prescribed_zone_deduction(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
lcf_purchase(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
lcp_purchase(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
dependants_under_19(integer, optional) A whole number from 0 to 99. -
dependants_disabled(integer, optional) A whole number from 0 to 99.
Responses
#### HTTP 200
Deduction
Body schema: DeductionResponse
-
rule_set_version(string, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
prorated_rules_applied(boolean, optional) -
employee(object, required) -
employer(object, required) -
annual_projection(object, optional) -
breakdown(object, optional) -
citations(array, optional) -
warnings(array, optional)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 400
Rejected request
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 402
Plan calculation limit reached
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 409
The account was deleted while this request ran; nothing was counted or returned (code invalid_request)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 415
Content-Type is not application/json (code unsupported_media_type)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 422
Jurisdiction not supported
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 500
An unexpected engine failure (code engine, a generic message); no result is returned
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The usage store could not be read or written (fail closed: no result)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 402, 409, 413, 415, 422, 429, 500, 503.
POST /v1/deductions/batch
Calculate T4127 payroll deductions for a pay run of up to 25 employees a call.
Requires a Bearer np_test_ or np_live_ key. One HTTP call, up to 25 calculations (a larger batch is a 400 batch_too_large; send a pay run in calls of at most 25), metered as N not 1. The body is at most 48 KiB. Each result is {ok, response} or {error}; one bad employee record does not abort the others. An item with a negative amount gets {error} with code invalid_request naming the field (the HTTP status of the call stays 200). If the live plan cannot cover the whole batch, the call is 402 and usage is unchanged. A live key may make at most 3,600 calls a UTC day per account, each request counted once (a batch or a year projection is one call); above it, HTTP 429 rate_limited and nothing is counted.
Request body
Up to 25 T4127 calculations (Workers Free: a larger batch is a 400 batch_too_large, nothing calculated or counted; send a pay run in calls of at most 25). Partial success: each result is {ok, response} or {error}. Item failures do not abort the pay run. The envelope takes only requests: any other key (for example a claim code or as_of meant for every item) is a 400 unknown_field naming it, and a bare JSON array is a 400 invalid_request. Put shared values on each item.
-
requests(array, required)
Responses
#### HTTP 200
Pay run results, in input order
Body schema: BatchResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
results(array, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 400
Rejected request, including batches over 25 (batch_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 402
Plan calculation limit reached; nothing billed
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 409
The account was deleted while this request ran; nothing was counted or returned (code invalid_request)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 415
Content-Type is not application/json (code unsupported_media_type)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 500
An unexpected engine failure (code engine, a generic message); no result is returned
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The usage store could not be read or written (fail closed: no result)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 402, 409, 413, 415, 429, 500, 503.
POST /v1/deductions/year
Project every pay period in a year with YTD CPP, CPP2, and EI carried forward.
Requires a Bearer np_test_ or np_live_ key. Metered as P calculations, not 1. Identifies the CPP cap period, EI cap period, YMPE crossing, and CPP2 start (spec §21.5). Sum of per-period federal tax versus annual T1 is allowed to differ by one cent per pay period because of per-period rounding. The body is checked exactly like POST /v1/deductions first; see YearRequest for the fields applied to every period and the ones refused. Pay dates: monthly keeps the day of the month, the rest step a fixed number of days, and all fall in the first pay date's calendar year. At most 27 pay periods (pay_period 1 to 27; 28 engine calculations including the check of the body); a weekly, daily or hourly year is a 400 year_too_large (Workers Free CPU limit), nothing calculated or counted: send those pay periods to POST /v1/deductions/batch. A refused request is not billed. A live key may make at most 3,600 calls a UTC day per account, each request counted once (a batch or a year projection is one call); above it, HTTP 429 rate_limited and nothing is counted.
Request body
A year projection: the fields listed here are applied to every pay period exactly as POST /v1/deductions applies them to one cheque. The body is first checked exactly like POST /v1/deductions (an unknown field is unknown_field, an invalid value the same 400 naming it). The other DeductionRequest fields (the year-to-date totals and pay_periods_elapsed, bonus and retroactive pay fields, commission fields, Quebec and change-of-province fields, date_of_birth, cpp_election_after_65) are refused with a 400 invalid_request naming the field and saying why, and so are a calculation_option other than the default (Option 2 needs the tax withheld so far) and a bonus_method other than regular. as_of is the first pay date and defaults to 1 January of the current UTC year; every pay date must fall in that calendar year (a 400 names the latest first pay date that fits).
-
as_of(string, optional) -
province(string, required) -
pay_period(1 | 2 | 4 | 10 | 12 | 13 | 22 | 24 | 26 | 27, required) Pay periods a year, at most 27 in a projection: 1, 2, 4, 10, 12, 13, 22, 24, 26 or 27. A weekly, daily or hourly year (52, 53, 240, 2000) is a 400 year_too_large (Workers Free CPU limit); nothing is calculated or counted. -
gross_pay(string, required) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
calculation_option("option1", optional) Only the default, Option 1, is accepted here; Option 2 is a 400 invalid_request naming the field. -
pensionable_earnings(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
insurable_earnings(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
federal_claim_code(integer | string, optional) Federal claim code (the TD1 box value that sets the personal amount): 0 to 10 (a whole number, or its digits as a string) or E (or e). 0 is no claim amount; E is no claim and no federal income tax; 1 (or no code) is the basic personal amount from its formula; 2 to 10 use the amount the CRA prints for that code in the claim-code table of the as_of edition. Not with federal_tc in the same request. Anything else is a 400 invalid_request naming the field. -
provincial_claim_code(integer | string, optional) Provincial claim code (the TD1 box value that sets the personal amount): 0 to 10 (a whole number, or its digits as a string) or E (or e). 0 is no claim amount; E is no claim and no provincial income tax; 1 (or no code) is the basic personal amount from its formula; 2 to 10 use the amount the CRA prints for that code in the claim-code table of the as_of edition. Not with provincial_tcp in the same request. Anything else is a 400 invalid_request naming the field. -
federal_tc(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
provincial_tcp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
cpp_months(integer, optional) CPP contributory months (PM). A whole number from 0 to 12; default 12. -
cpp_exempt(boolean, optional) -
k2_method("pdoc_observed" | "t4127_literal" | "year_to_date" | "annualized", optional) annualized is another name for pdoc_observed. -
rounding_compat("t4127" | "pdoc", optional) -
taxable_benefits(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
rpp(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
union_dues(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
alimony(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
child_care_expenses(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
prescribed_zone_deduction(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
additional_tax_requested(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
lcf_purchase(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
lcp_purchase(string, optional) Decimal money as a lexical string, for example "1000.00". Never a JSON number. Zero or more: a negative amount is a 400 invalid_request naming the field. At most 15 digits before the decimal point and at most 2 decimal places (dollars and cents); otherwise a 400 invalid_request naming the field. The engine also reads a decimal point with no digits after it ("1000.") or none before it (".50"), and leading zeros. -
dependants_under_19(integer, optional) A whole number from 0 to 99. -
dependants_disabled(integer, optional) A whole number from 0 to 99. -
bonus_method("regular", optional) Only the default, regular, is accepted here; a year projection has no bonus.
Responses
#### HTTP 200
Year projection
Body schema: YearResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
periods(array, required) -
totals(YearTotals, required) -
annual_t1(string, required) Decimal money as a lexical string. Never a JSON number. At most 15 digits before the decimal point; a larger amount is a 400 invalid_request. -
federal_tax_sum_tolerance(string, required) Decimal money as a lexical string. Never a JSON number. At most 15 digits before the decimal point; a larger amount is a 400 invalid_request. -
cpp_cap_period(integer | null, optional) -
ei_cap_period(integer | null, optional) -
cpp2_start_period(integer | null, optional) -
ympe_cross_period(integer | null, optional) -
ympe(string, optional) Decimal money as a lexical string. Never a JSON number. At most 15 digits before the decimal point; a larger amount is a 400 invalid_request.
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 400
Rejected request
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 402
Plan calculation limit reached; nothing billed
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 409
The account was deleted while this request ran; nothing was counted or returned (code invalid_request)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 415
Content-Type is not application/json (code unsupported_media_type)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 422
Jurisdiction not supported
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Header X-Usage-Limit: Monthly live calculation cap, or unlimited for np_test_ keys. Header X-Usage-Remaining: Calculations remaining this UTC month, or unlimited. Header X-Usage-Reset: Next UTC month start, when the live meter resets.
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 500
An unexpected engine failure (code engine, a generic message); no result is returned
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The usage store could not be read or written (fail closed: no result)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 402, 409, 413, 415, 422, 429, 500, 503.
GET /v1/jurisdictions
Provinces and territories. Quebec is listed as unsupported.
Authentication: none.
Responses
#### HTTP 200
Jurisdictions
Body schema: JurisdictionsResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
jurisdictions(array, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429.
GET /v1/keys
Active key prefixes for this account. Secrets are never returned.
Responses
#### HTTP 200
Prefixes only
Body schema: KeyListResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
keys(array, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 401, 429, 503.
POST /v1/keys/revoke
Revoke keys of the given kind immediately. The body must be exactly { "kind": "test" } or { "kind": "live" }; any other body is a 400 and revokes nothing. A lost key is regenerated, never retrieved. Never refused by our own limits or budgets; Cloudflare's per-IP rule (10 requests per 10 seconds) still applies.
Request body
-
kind("test" | "live", required)
Responses
#### HTTP 200
Revoked
Body schema: KeyRevokeResponse
-
kind("test" | "live", required) -
revoked(true, required)
#### HTTP 400
kind missing or not exactly test or live, or another field present. Nothing was revoked.
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 413, 503.
POST /v1/keys/rotate
Issue a new key of the given kind. The previous key is revoked immediately. The new secret is shown once. At most 10 rotations a day made with the test key and, separately, 10 made with the live key.
Request body
-
kind("test" | "live", required)
Responses
#### HTTP 200
New secret, shown once
Body schema: KeyRotateResponse
-
kind("test" | "live", required) -
key(string, required) -
message(string, required)
#### HTTP 400
Rejected request
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
The calling key kind used its 10 rotations for the UTC day, or the request limit (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 413, 429, 503.
GET /v1/rules
Embedded T4127 rule-set editions.
Authentication: none.
Responses
#### HTTP 200
Rule set list
Body schema: RulesListResponse
-
rule_set_version(string, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
rule_set_versions(array, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 429.
GET /v1/rules/diff
Field-by-field comparison of two embedded T4127 editions.
Authentication: none.
Parameters
-
from(query, required): string -
to(query, required): string
Responses
#### HTTP 200
Rule-set diff
Body schema: RulesDiffResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
from(string, required) -
to(string, required) -
changed(array, required) -
fields(object, required) -
cpp(array, required) -
ei(array, required) -
qpip(array, required)
#### HTTP 400
Missing versions
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 404
Unknown version
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 404, 429.
GET /v1/rules/{version}
One embedded rule-set edition. Unknown versions 404; never nearest-match.
Authentication: none.
Parameters
-
version(path, required): string
Responses
#### HTTP 200
Rule set
Body schema: RuleVersionResponse
-
rule_set_version(string, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
effective_from(string, required) -
effective_to(string | null, optional) -
status("enacted" | "proposed", required)
#### HTTP 404
Unknown version
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 404, 429.
POST /v1/signup
Start signup from the form at /signup/ (a browser is needed: the form carries a Cloudflare Turnstile token, checked with Cloudflare after the IP rate limit and the format checks, before any account or budget is read). Email, verify, keys. No sales call. One address gets at most one email every 10 minutes and 3 in 24 hours (counted per normalised address: lower case and no +tag; for Gmail also no dots). One network (an IPv4 address or an IPv6 /48) gets at most 30 in 24 hours (CLIENT_SIGNUPS_PER_DAY), then 429. An email the provider refuses counts toward the network budget of 30, not toward the per-address limits, and keeps no account. Every address gets the same answer, whether or not it has an account; an address that already has keys is emailed a notice instead of a link. Cross-site browser requests are refused (403).
Authentication: none.
Request body
-
email(string, required) -
turnstile_token(string, required) The token the Cloudflare Turnstile widget on /signup/ gives the form (action "signup"). Single use; checked with Cloudflare siteverify before the address is read. -
accept_terms(boolean, required) Must be true: you agree to the Terms (/terms/, /api-terms/) and have read the Privacy Policy (/privacy/). The time and the policy version are stored on the verification token and copied to the account, as a history entry, when the link is used.
Responses
#### HTTP 200
Verification email sent
Body schema: SignupResponse
-
message(string, required)
#### HTTP 400
Rejected request
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 403
Cross-site browser request, or the Turnstile check failed
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited, or the per-network daily budget is used up (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The email could not be sent or confirmed, or signup is misconfigured
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 403, 413, 429, 503.
POST /v1/signup/verify
Verify email and issue np_test_ plus np_live_ keys. POST only: the emailed page sends it when the person presses the button, which records the Terms acceptance carried by the link. The link works once and expires 24 hours after it was issued.
Authentication: none.
Request body
-
token(string, required) The token from the emailed link.
Responses
#### HTTP 200
Keys issued once
Body schema: VerifyResponse
-
test_key(string, required) -
live_key(string, required) -
message(string, required)
#### HTTP 400
Invalid, used or expired token
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 403
A cross-site browser request (refused before anything is read)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 409
The account was deleted while this request ran (code invalid_request); no keys were issued. Sign up again
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 403, 409, 413, 429, 503.
GET /v1/webhooks
Webhook endpoints for this account. Each URL is shown as its scheme and host only (https://<host>/), to every key: the whole URL is shown once, in the registration answer.
Responses
#### HTTP 200
Endpoints
Body schema: WebhookListResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
webhooks(array, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 401, 429, 503.
POST /v1/webhooks
Register an HTTPS endpoint. Needs the live key. The signing secret and the whole URL are shown once (listing shows only the scheme and host). At most 5 endpoints per account (20 on a paid plan), and 20 registrations a day.
The whsec_ secret is returned only on create. Deliveries are HMAC-SHA256 over t + "." + body as takehome-signature: t=<unix>,v1=<hex>. Receivers must reject |now - t| > 300 seconds (5 minutes).
Request body
-
url(string, required) https only, no user name or password, at most 2,048 bytes (UTF-8). One account's URLs together may be at most 16,384 bytes.
Responses
#### HTTP 200
Created
Body schema: WebhookCreateResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
id(string, required) -
url(string, required) https only, no user name or password, at most 2,048 bytes (UTF-8). -
secret(string, required) -
created_at(string, required)
#### HTTP 400
Rejected request
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 409
The account already has the most endpoints its plan allows, or its URLs would pass 16,384 bytes together. Not counted as a registration.
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
The account used its 20 registrations for the UTC day, or the request limit (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 409, 413, 429, 503.
GET /v1/webhooks/deliveries
Delivery log for this account, one page at a time, newest first: at most 20 records a page. Pass next_cursor back as cursor for the next page; it is null on the last page.
Parameters
-
limit(query, optional): integer -
cursor(query, optional): string
Responses
#### HTTP 200
Log
Body schema: WebhookDeliveriesResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
deliveries(array, required) -
next_cursor(string | null, required) Pass as ?cursor= for the next page; null on the last page.
#### HTTP 400
A limit outside 1 to 20, a cursor this API did not make, or another query parameter
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 429, 503.
POST /v1/webhooks/deliveries/{id}/replay
Re-send one signed payload. Needs the live key.
Parameters
-
id(path, required): string
Responses
#### HTTP 200
Replayed
Body schema: WebhookReplayResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
id(string, required) -
endpoint_id(string, optional) -
event(string, optional) -
status(string, required) -
attempts(integer, required) -
last_error(string | null, optional) -
replay_of(string | null, optional) -
created_at(string, optional) -
delivered_at(string | null, optional)
#### HTTP 400
A body was sent (this route takes none), or an unknown field
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 404
Unknown delivery
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 409
The endpoint or the account was deleted while this request ran, before its first attempt; nothing was sent (code invalid_request). Also when another dispatch or replay of this account is still running (one at a time per account; retry when it has answered, at most 60 seconds); nothing was sent or counted
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
The UTC clock hour's or UTC day's delivery budget is used up, or the key rate limit (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 404, 409, 413, 429, 503.
POST /v1/webhooks/dispatch
Fan out a rule_set.changed event to every endpoint on this account (at most 5; an account with more is a 409 and nothing is sent). Deliveries are sent only when this is called or a delivery is replayed, never automatically when rules change. Needs the live key. At most 100 deliveries in a UTC clock hour and 300 in a UTC day per account (one per endpoint per dispatch, plus replays); a dispatch that would pass either is refused whole (429).
Request body
-
from(string, required) -
to(string, required)
Responses
#### HTTP 200
Deliveries
Body schema: WebhookDispatchResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
from(string, required) -
to(string, required) -
changed(array, required) -
deliveries(array, required)
#### HTTP 400
Rejected request
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 404
Unknown rule set
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 409
More endpoints than one dispatch delivers to; another dispatch or replay of this account is still running (one at a time per account; retry when it has answered, at most 60 seconds); or an endpoint (or the account) was deleted before the delivery records were written; nothing was sent or counted (code invalid_request). An endpoint deleted after that gets no further attempt: its delivery is listed as failed with last_error "The endpoint was deleted; no further attempt was sent."
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
The UTC clock hour's or UTC day's delivery budget is used up, or the key rate limit (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 404, 409, 413, 429, 503.
DELETE /v1/webhooks/{id}
Delete a webhook endpoint on this account. Needs the live key. Unknown or foreign IDs are 404. Never refused by our own limits or budgets; Cloudflare's per-IP rule (10 requests per 10 seconds) still applies. Deleting an endpoint does not give back registrations or delivery budget.
Parameters
-
id(path, required): string
Responses
#### HTTP 200
Deleted
Body schema: WebhookDeleteResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
deleted(boolean, required) -
id(string, required)
#### HTTP 400
A body was sent (this route takes none), or an unknown field
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 404
Unknown webhook
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 413
The body is over its size limit (code payload_too_large)
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 400, 401, 404, 413, 503.
GET /v1/webhooks/{id}
One webhook endpoint on this account, its URL shown as its scheme and host only (https://<host>/). Unknown or foreign IDs are 404.
Parameters
-
id(path, required): string
Responses
#### HTTP 200
Endpoint
Body schema: WebhookGetResponse
-
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required) -
id(string, required) -
url(string, required) Only the scheme and host, https://<host>/ (with the port, if not 443). The path and query, where receivers often keep a token, are never shown again after the registration answer, to any key. -
created_at(string, required)
#### HTTP 401
Missing or unknown key
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 404
Unknown webhook
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 429
Rate limited (our JSON ErrorEnvelope, code rate_limited). Every API path also has Cloudflare's per-IP rule in front of it: at most 10 requests per 10 seconds from one IP address, counted before our code runs. Above it, Cloudflare answers HTTP 429 with its own page for 10 seconds: not JSON, so there is no error.code; wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all. The rule counts by IP address alone: test-key and live-key calls from one address share those 10, and so do all callers behind one shared outbound address (an office network, a CI service, or a serverless platform such as Cloudflare Workers, whose outbound requests may share addresses). To run the engine inside your own Worker with no API call, use the takehome-ca WASM package (in the repository, not yet published on npm).
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
#### HTTP 503
The database could not be read or written (code store); retry later
Body schema: ErrorEnvelope
-
error(ErrorBody, required) -
rule_set_version(string | null, required) -
engine_version(string, required) -
engine_build_sha256(string, required)
Error responses on this route
HTTP statuses that return ErrorEnvelope: 401, 404, 429, 503.
Error codes
Every error our code returns is JSON matching ErrorEnvelope: error (code, message, docs) plus rule_set_version, engine_version, and engine_build_sha256. The one exception is not ours: every API path is behind Cloudflare's per-IP rule, at most 10 requests per 10 seconds from one IP address, counted before our code runs; above it Cloudflare answers HTTP 429 with its own page for 10 seconds, which is not JSON and has no error.code. Wait 10 seconds and retry. One IP address gets at most 60 API requests a minute in all.
batch_too_large
HTTP 400.
Cause: POST /v1/deductions/batch carried more than 25 requests (the Workers Free CPU limit).
What to do: Split the pay run into calls of at most 25 calculations. Nothing was calculated or counted.
date_out_of_range
HTTP 400.
Cause: as_of falls before the first enacted rule set or after the last covered date.
What to do: Pick a supported calendar date. The API does not snap to the nearest rule set.
engine
HTTP 500.
Cause: An internal engine or health probe failure occurred.
What to do: Retry once. If it persists, contact support with rule_set_version and engine_build_sha256 from the envelope.
invalid_request
HTTP 400.
Cause: The JSON parsed but violates request rules (shape, enums, or business constraints).
What to do: Read error.message. Fix the named constraint and retry.
jurisdiction_not_supported
HTTP 422.
Cause: The province code is not calculated (for example QC).
What to do: Use GET /v1/jurisdictions. Do not treat a partial total as Quebec payroll tax.
malformed_json
HTTP 400.
Cause: The body is not valid JSON or exceeds depth or size limits.
What to do: Fix syntax. Send application/json. Keep payloads within documented limits.
method_not_allowed
HTTP 405.
Cause: The HTTP method does not match the route contract.
What to do: Use the method shown in this reference for the path.
not_found
HTTP 404.
Cause: The path, rule version, webhook id, or delivery id does not exist for this account.
What to do: Fix the URL or id. Cross-account webhook access returns 404, not 403.
payload_too_large
HTTP 413.
Cause: The request body exceeded its limit: 16 KiB (48 KiB for a batch, 8 KiB for signup and verify).
What to do: Shrink the batch or payload. Prefer batch for many employees.
plan_limit
HTTP 402.
Cause: The live key reached its UTC-month calculation cap.
What to do: Paid plans are not offered yet (POST /v1/billing/checkout answers 501 billing_unavailable), so the quota cannot be raised. It resets at X-Usage-Reset (00:00 UTC on the first of the month). A refused call counts nothing.
rate_limited
HTTP 429.
Cause: Too many HTTP requests in the rate-limit window for this route, key kind or IP, or a per-account daily budget used up. A 429 whose body is not JSON comes from Cloudflare's per-IP rule (10 requests per 10 seconds), not from our code.
What to do: Back off and retry; after a 429 without JSON, wait 10 seconds. Metering limits are separate; see rate-limits-and-billing.md.
rule
HTTP 400.
Cause: The engine rejected rule lookup or diff input.
What to do: Confirm rule set versions with GET /v1/rules. Use enacted dates only unless testing proposed sets.
store
HTTP 503.
Cause: Account, usage, or key storage failed (fail closed).
What to do: Retry later. Live metering outages do not serve unmetered calculations.
unauthorized
HTTP 401.
Cause: Missing Bearer key, wrong prefix, or revoked secret.
What to do: Check the key and its prefix: send Authorization: Bearer np_test_… or np_live_…. A lost test key can be replaced with the live key (POST /v1/keys/rotate with {"kind":"test"}). A lost live key cannot be replaced through the API: email developwith.gt@gmail.com from the account address (API Terms section 3).
unknown_field
HTTP 400.
Cause: The JSON body includes a property name the engine does not recognize.
What to do: Remove or rename the field. See request-fields.md for the allowed set.
unsupported_media_type
HTTP 415.
Cause: Content-Type was not application/json where required.
What to do: Set content-type: application/json on POST bodies.
year_too_large
HTTP 400.
Cause: POST /v1/deductions/year asked for more than 27 pay periods (a weekly, daily or hourly year; the Workers Free CPU limit).
What to do: Send those pay periods to POST /v1/deductions/batch, at most 25 a call. Nothing was calculated or counted.
Last reviewed: 2026-10-24 Engine: 0.1.0