How the Indonesia Gaji bersih (net salary) calculator is verified
Inputs
| Input | Field | Type |
|---|---|---|
| Gross monthly pay (IDR) | grossMonthly | number |
| Bonus and THR for the year (IDR) | annualBonus | number |
| Marital status on 1 January | maritalStatus | string |
| Dependants you fully support (at most 3 count) | dependants | number |
| Enrolled in BPJS Kesehatan and BPJS Ketenagakerjaan | bpjsEnrolled | boolean |
| Work-accident (JKK) risk class of your workplace | jkkRisk | string |
| 2026 government-borne PPh 21 (PMK 105/2025) | dtpEligible | boolean |
| Regular pay in January 2026 (IDR), if different | januaryRegularPay | number |
The formula
Each derived value below is computed by the Valem engine from the expression shown, in this order. Nothing else runs — there is no hidden code path.
| Derived value | Field | Formula (JSONata) |
|---|---|---|
| Jkk Rate | jkkRate | jkkRisk = 'veryHigh' ? $const.jkkRateVeryHigh : (jkkRisk = 'high' ? $const.jkkRateHigh : (jkkRisk = 'medium' ? $const.jkkRateMedium : (jkkRisk = 'low' ? $const.jkkRateLow : $const.jkkRateVeryLow))) |
| Health Base | healthBase | bpjsEnrolled ? $clampFloorCap($n(grossMonthly), null, $const.healthWageCap) : 0 |
| BPJS Kesehatan 1% / month | healthMonthly | $round(healthBase * $const.healthEmployeeRate, 2) |
| Old-age (JHT) 2% / month | jhtMonthly | bpjsEnrolled ? $round($n(grossMonthly) * $const.jhtEmployeeRate, 2) : 0 |
| Pension (JP) 1% / month | jpMonthly | bpjsEnrolled ? $round($clampFloorCap($n(grossMonthly), null, $const.jpWageCap) * $const.jpEmployeeRate, 2) : 0 |
| Contributions Monthly | contributionsMonthly | $round(healthMonthly + jhtMonthly + jpMonthly, 2) |
| Employer premiums / month (taxable) | employerPremiumsMonthly | bpjsEnrolled ? $round($n(grossMonthly) * jkkRate + $n(grossMonthly) * $const.jkmRate + healthBase * $const.healthEmployerRate, 2) : 0 |
| Gross for PPh 21 / year | grossTaxableAnnual | $round(($n(grossMonthly) + employerPremiumsMonthly) * 12 + $n(annualBonus), 2) |
| Occupational cost (biaya jabatan) | occupationalCost | $round($clampFloorCap(grossTaxableAnnual * $const.occupationalCostRate, null, $const.occupationalCostCapAnnual), 2) |
| JHT and JP you paid, year | pensionDeduction | $round((jhtMonthly + jpMonthly) * 12, 2) |
| PTKP (non-taxable income) | ptkp | maritalStatus = 'marriedWoman' ? $const.ptkpSelf : $const.ptkpSelf + (maritalStatus = 'married' ? $const.ptkpMarried : 0) + ($d := $floor($n(dependants)); ($d > $const.maxDependants ? $const.maxDependants : $d)) * $const.ptkpPerDependant |
| Taxable income (PKP) | taxableIncome | ($v := grossTaxableAnnual - occupationalCost - pensionDeduction - ptkp; $v > 0 ? $floor($v / 1000) * 1000 : 0) |
| PPh 21 for the year | pph21Annual | $round($bandedCharge(taxableIncome, [{"upTo": $const.pitBand1, "rate": $const.pitRate1}, {"upTo": $const.pitBand2, "rate": $const.pitRate2}, {"upTo": $const.pitBand3, "rate": $const.pitRate3}, {"upTo": $const.pitBand4, "rate": $const.pitRate4}, {"upTo": null, "rate": $const.pitRate5}]), 2) |
| PPh 21 borne by the government | governmentBorneTax | ($j := $n(januaryRegularPay); dtpEligible and ($j > 0 ? $j : $n(grossMonthly)) <= $const.dtpRegularGrossCap) ? pph21Annual : 0 |
| Net / year | netAnnual | $round($max([0, $n(grossMonthly) * 12 + $n(annualBonus) - contributionsMonthly * 12 - pph21Annual + governmentBorneTax]), 2) |
| Net / month | netMonthly | $round(netAnnual / 12, 2) |
Rates and thresholds
The expressions above read these as $const. They are the figures the annual refresh replaces, so they live as data rather than being written into the formulas — which is what makes a year-on-year change a one-line diff instead of an edit to arithmetic.
| Constant | Value |
|---|---|
ptkpSelf | 54000000 |
ptkpMarried | 4500000 |
ptkpPerDependant | 4500000 |
maxDependants | 3 |
pitBand1 | 60000000 |
pitBand2 | 250000000 |
pitBand3 | 500000000 |
pitBand4 | 5000000000 |
pitRate1 | 0.05 |
pitRate2 | 0.15 |
pitRate3 | 0.25 |
pitRate4 | 0.3 |
pitRate5 | 0.35 |
occupationalCostRate | 0.05 |
occupationalCostCapAnnual | 6000000 |
jhtEmployeeRate | 0.02 |
jpEmployeeRate | 0.01 |
jpWageCap | 10547400 |
jkkRateVeryLow | 0.0024 |
jkkRateLow | 0.0054 |
jkkRateMedium | 0.0089 |
jkkRateHigh | 0.0127 |
jkkRateVeryHigh | 0.0174 |
jkmRate | 0.003 |
healthEmployeeRate | 0.01 |
healthEmployerRate | 0.04 |
healthWageCap | 12000000 |
dtpRegularGrossCap | 10000000 |
Test vectors (11)
Every case is executed against this model on each build, by the Java engine and independently by a JavaScript one. A mismatch of a single cent fails the build, so the page cannot ship advertising a figure the model no longer produces.
Case 1 — N1 Rp15 million a month, single, BPJS at the lowest JKK class
given grossMonthly | 15000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect grossTaxableAnnual | 186732000 |
expect occupationalCost | 6000000 |
expect pensionDeduction | 4865688 |
expect taxableIncome | 121866000 |
expect pph21Annual | 12279900 |
expect netMonthly | 13451201 |
Case 2 — N1b Rp15 million: own contributions and taxable employer premiums
given grossMonthly | 15000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect healthMonthly | 120000 |
expect jhtMonthly | 300000 |
expect jpMonthly | 105474 |
expect employerPremiumsMonthly | 561000 |
expect netAnnual | 161414412 |
Case 3 — N2 PMK 105/2025 example 1 (TK/0): Rp1,860,000, all borne
given grossMonthly | 8000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | no |
given jkkRisk | veryLow |
given dtpEligible | yes |
given januaryRegularPay | 0 |
expect grossTaxableAnnual | 96000000 |
expect occupationalCost | 4800000 |
expect ptkp | 54000000 |
expect taxableIncome | 37200000 |
expect pph21Annual | 1860000 |
expect governmentBorneTax | 1860000 |
expect netAnnual | 96000000 |
Case 4 — N3 PMK 105/2025 example 2, annualised (Tuan B, K/1, promoted from Rp10m in October): PPh 21 Rp4,050,000, all borne
given grossMonthly | 10500000 |
|---|---|
given annualBonus | 10000000 |
given maritalStatus | married |
given dependants | 1 |
given bpjsEnrolled | no |
given jkkRisk | veryLow |
given dtpEligible | yes |
given januaryRegularPay | 10000000 |
expect grossTaxableAnnual | 136000000 |
expect occupationalCost | 6000000 |
expect ptkp | 63000000 |
expect taxableIncome | 67000000 |
expect pph21Annual | 4050000 |
expect governmentBorneTax | 4050000 |
expect netAnnual | 136000000 |
Case 5 — N4 Rp30 million a month: health and JP at their caps
given grossMonthly | 30000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | married |
given dependants | 2 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect healthBase | 12000000 |
expect healthMonthly | 120000 |
expect jhtMonthly | 600000 |
expect jpMonthly | 105474 |
expect employerPremiumsMonthly | 642000 |
expect ptkp | 67500000 |
Case 6 — N5 three dependants: K/3 = Rp72,000,000
given grossMonthly | 15000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | married |
given dependants | 3 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect ptkp | 72000000 |
Case 7 — N6 Rp4 million a month: under PTKP, no PPh 21
given grossMonthly | 4000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect taxableIncome | 0 |
expect pph21Annual | 0 |
Case 8 — N7 the government-borne relief needs January regular pay of at most Rp10 million
given grossMonthly | 12000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | yes |
given januaryRegularPay | 0 |
expect governmentBorneTax | 0 |
Case 9 — N8 a very-high-risk workplace raises the taxable employer premium
given grossMonthly | 10000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | yes |
given jkkRisk | veryHigh |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect jkkRate | 0.0174 |
expect employerPremiumsMonthly | 604000 |
Case 10 — N9 a married woman whose husband earns: own PTKP only
given grossMonthly | 15000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | marriedWoman |
given dependants | 2 |
given bpjsEnrolled | yes |
given jkkRisk | veryLow |
given dtpEligible | no |
given januaryRegularPay | 0 |
expect ptkp | 54000000 |
expect pph21Annual | 12279900 |
Case 11 — N10 a raise to Rp12m after January keeps the relief when January's regular pay was Rp10m
given grossMonthly | 12000000 |
|---|---|
given annualBonus | 0 |
given maritalStatus | single |
given dependants | 0 |
given bpjsEnrolled | no |
given jkkRisk | veryLow |
given dtpEligible | yes |
given januaryRegularPay | 10000000 |
expect taxableIncome | 84000000 |
expect pph21Annual | 6600000 |
expect governmentBorneTax | 6600000 |
expect netAnnual | 144000000 |
Sources
- UU 7/2021 (HPP): Income Tax Law arts. 7, 17 · checked 2026-10-31
- PMK 168/2023 arts. 9–10: PTKP of married women, biaya jabatan · checked 2026-10-31
- PMK 105/2025: 2026 government-borne PPh 21 · checked 2026-10-31
- BPJS Ketenagakerjaan: JHT 2%, JP 1%, JP ceiling · checked 2026-10-31
- PP 44/2015 arts. 16, 18: JKK, JKM · checked 2026-10-31
- Perpres 64/2020 arts. 30, 32: BPJS Kesehatan · checked 2026-10-31
Check it yourself — and reuse it
The whole model as JSON — inputs, outputs, formulas, vectors and sources in one re-runnable document.
This model is published under CC BY 4.0: use it, adapt it, build on it, with attribution. The licence covers the model — the expression of each rule and the arrangement of the catalog. The rates and thresholds are law, and nobody licenses those; the sources above are where they come from.
← Back to the Indonesia Gaji bersih (net salary) calculator
Built with Valem — a live, verifiable calculator you can fork and embed.