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}'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"
}
}{
"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 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 1000 employees; that counts as 1000 calculations, not 1. One invalid row returns {error} for that index and does not abort the rest. If the remaining quota cannot cover the whole batch, the call 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.