Docs · The manual

API docs

Get a key, make one authenticated call, see 800.79. Test keys run the same engine as live keys and are not metered.

First call

Get a key: email, verify, copy the np_test_ key. No sales call. Then run this. Ontario weekly $1000.00 on 2026-01-15, claim code 1. employee.net_pay is 800.79.

curl -sS -X POST https://takehome.gautamkhosla.com/v1/deductions \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer np_test_YOUR_KEY' \
  -d '{"as_of":"2026-01-15","province":"ON","pay_period":52,"gross_pay":"1000.00","federal_claim_code":1,"provincial_claim_code":1}'

Request

POST /v1/deductions with content-type: application/json and authorization: Bearer. Replace np_test_YOUR_KEY with the test key from verify.

Response

Trimmed body for the first call. Cents are strings, never binary floats.

{
  "rule_set_version": "2026-01-01",
  "employee": {
    "federal_tax": "81.61",
    "provincial_tax": "45.80",
    "cpp": "55.50",
    "cpp2": "0.00",
    "ei": "16.30",
    "total_deductions": "199.21",
    "net_pay": "800.79"
  }
}

Year projection

POST /v1/deductions/year runs every cheque in the year with YTD CPP, CPP2, EI, and pensionable earnings carried forward. That is how the annual maxima bind and how CPP2 waits for YMPE (spec ยง21.5).

The body is checked exactly like a single calculation. Fields that apply to every cheque (claim codes or amounts, RPP, union dues, taxable benefits and the others listed in the year projection guide) are applied to every period; year-to-date totals, bonuses and the other one-off fields are refused by name. Every pay date falls in one calendar year, and a projection runs at most 27 pay periods.

The sum of per-period federal tax is not required to equal annual T1. Each cheque rounds (T1)/P to the cent (T4127 Chapter 2). That per-period rounding is why the gap exists; the documented tolerance is one cent per pay period (federal_tax_sum_tolerance, 0.26 on a 26-biweekly year).

Authentication

Live keys count calculations, not HTTP requests, and stop at the plan limit. A pay run is POST /v1/deductions/batch with up to 25 employees a call; that counts as 25 calculations, not 1. One invalid row returns {error} for that index and does not abort the rest; it still counts as a calculation. A year projection counts one calculation per pay period (26 for biweekly), at most 27 pay periods; a weekly, daily or hourly year is refused with year_too_large. If the remaining quota cannot cover the whole call, it is 402 and usage is unchanged. Prices are on pricing.

Webhooks

POST /v1/webhooks registers an HTTPS URL. The whsec_ signing secret is in that create response exactly once; GET will not return it. Deliveries send rule_set.changed with header takehome-signature: t=<unix>,v1=<hex>. v1 is HMAC-SHA256 over the timestamp and body together (t + . + raw JSON), never the body alone.

Receivers must reject a signature whose t is more than 5 minutes off their clock (300 seconds either side). That is the replay window. Accepting an old t with a valid v1 lets an attacker replay a captured delivery.

Public endpoints

Public listings (/v1/rules, /v1/jurisdictions, /openapi.json) do not need a key.

Embed

Job boards can skip the API: one embed.js tag runs the same WASM engine in the listing page. No cookie, no salary posted back. Pin the versioned path and the SRI hash so a host page cannot be swapped onto a different build.

<script src="https://takehome.gautamkhosla.com/embed/v0.1.0/embed.js" integrity="sha384-ksYDBpXyjnJ+bOzEcPnvc6q7/noZxoOc6MX4fWhbQN+WTQIv22ztzZ8v5u5v4pW4" crossorigin="anonymous" data-province="ON"
        data-gross="75000"></script>

Integrity sha384-ksYDBpXyjnJ+bOzEcPnvc6q7/noZxoOc6MX4fWhbQN+WTQIv22ztzZ8v5u5v4pW4 with crossorigin="anonymous".

Privacy

The API does not store calculation inputs. Request bodies are not written to D1 and are not logged. The in-browser calculator never sends a salary figure to the Worker.