Docs · API reference

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

Source: docs/api-reference.md in the Takehome repository, rendered as written.