How the Indonesia Gaji bersih (net salary) calculator is verified

Inputs

InputFieldType
Gross monthly pay (IDR)grossMonthlynumber
Bonus and THR for the year (IDR)annualBonusnumber
Marital status on 1 JanuarymaritalStatusstring
Dependants you fully support (at most 3 count)dependantsnumber
Enrolled in BPJS Kesehatan and BPJS KetenagakerjaanbpjsEnrolledboolean
Work-accident (JKK) risk class of your workplacejkkRiskstring
2026 government-borne PPh 21 (PMK 105/2025)dtpEligibleboolean
Regular pay in January 2026 (IDR), if differentjanuaryRegularPaynumber

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 valueFieldFormula (JSONata)
Jkk RatejkkRatejkkRisk = 'veryHigh' ? $const.jkkRateVeryHigh : (jkkRisk = 'high' ? $const.jkkRateHigh : (jkkRisk = 'medium' ? $const.jkkRateMedium : (jkkRisk = 'low' ? $const.jkkRateLow : $const.jkkRateVeryLow)))
Health BasehealthBasebpjsEnrolled ? $clampFloorCap($n(grossMonthly), null, $const.healthWageCap) : 0
BPJS Kesehatan 1% / monthhealthMonthly$round(healthBase * $const.healthEmployeeRate, 2)
Old-age (JHT) 2% / monthjhtMonthlybpjsEnrolled ? $round($n(grossMonthly) * $const.jhtEmployeeRate, 2) : 0
Pension (JP) 1% / monthjpMonthlybpjsEnrolled ? $round($clampFloorCap($n(grossMonthly), null, $const.jpWageCap) * $const.jpEmployeeRate, 2) : 0
Contributions MonthlycontributionsMonthly$round(healthMonthly + jhtMonthly + jpMonthly, 2)
Employer premiums / month (taxable)employerPremiumsMonthlybpjsEnrolled ? $round($n(grossMonthly) * jkkRate + $n(grossMonthly) * $const.jkmRate + healthBase * $const.healthEmployerRate, 2) : 0
Gross for PPh 21 / yeargrossTaxableAnnual$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, yearpensionDeduction$round((jhtMonthly + jpMonthly) * 12, 2)
PTKP (non-taxable income)ptkpmaritalStatus = '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 yearpph21Annual$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 governmentgovernmentBorneTax($j := $n(januaryRegularPay); dtpEligible and ($j > 0 ? $j : $n(grossMonthly)) <= $const.dtpRegularGrossCap) ? pph21Annual : 0
Net / yearnetAnnual$round($max([0, $n(grossMonthly) * 12 + $n(annualBonus) - contributionsMonthly * 12 - pph21Annual + governmentBorneTax]), 2)
Net / monthnetMonthly$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.

ConstantValue
ptkpSelf54000000
ptkpMarried4500000
ptkpPerDependant4500000
maxDependants3
pitBand160000000
pitBand2250000000
pitBand3500000000
pitBand45000000000
pitRate10.05
pitRate20.15
pitRate30.25
pitRate40.3
pitRate50.35
occupationalCostRate0.05
occupationalCostCapAnnual6000000
jhtEmployeeRate0.02
jpEmployeeRate0.01
jpWageCap10547400
jkkRateVeryLow0.0024
jkkRateLow0.0054
jkkRateMedium0.0089
jkkRateHigh0.0127
jkkRateVeryHigh0.0174
jkmRate0.003
healthEmployeeRate0.01
healthEmployerRate0.04
healthWageCap12000000
dtpRegularGrossCap10000000

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 grossMonthly15000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleno
given januaryRegularPay0
expect grossTaxableAnnual186732000
expect occupationalCost6000000
expect pensionDeduction4865688
expect taxableIncome121866000
expect pph21Annual12279900
expect netMonthly13451201

Case 2 — N1b Rp15 million: own contributions and taxable employer premiums

given grossMonthly15000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleno
given januaryRegularPay0
expect healthMonthly120000
expect jhtMonthly300000
expect jpMonthly105474
expect employerPremiumsMonthly561000
expect netAnnual161414412

Case 3 — N2 PMK 105/2025 example 1 (TK/0): Rp1,860,000, all borne

given grossMonthly8000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledno
given jkkRiskveryLow
given dtpEligibleyes
given januaryRegularPay0
expect grossTaxableAnnual96000000
expect occupationalCost4800000
expect ptkp54000000
expect taxableIncome37200000
expect pph21Annual1860000
expect governmentBorneTax1860000
expect netAnnual96000000

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 grossMonthly10500000
given annualBonus10000000
given maritalStatusmarried
given dependants1
given bpjsEnrolledno
given jkkRiskveryLow
given dtpEligibleyes
given januaryRegularPay10000000
expect grossTaxableAnnual136000000
expect occupationalCost6000000
expect ptkp63000000
expect taxableIncome67000000
expect pph21Annual4050000
expect governmentBorneTax4050000
expect netAnnual136000000

Case 5 — N4 Rp30 million a month: health and JP at their caps

given grossMonthly30000000
given annualBonus0
given maritalStatusmarried
given dependants2
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleno
given januaryRegularPay0
expect healthBase12000000
expect healthMonthly120000
expect jhtMonthly600000
expect jpMonthly105474
expect employerPremiumsMonthly642000
expect ptkp67500000

Case 6 — N5 three dependants: K/3 = Rp72,000,000

given grossMonthly15000000
given annualBonus0
given maritalStatusmarried
given dependants3
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleno
given januaryRegularPay0
expect ptkp72000000

Case 7 — N6 Rp4 million a month: under PTKP, no PPh 21

given grossMonthly4000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleno
given januaryRegularPay0
expect taxableIncome0
expect pph21Annual0

Case 8 — N7 the government-borne relief needs January regular pay of at most Rp10 million

given grossMonthly12000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleyes
given januaryRegularPay0
expect governmentBorneTax0

Case 9 — N8 a very-high-risk workplace raises the taxable employer premium

given grossMonthly10000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledyes
given jkkRiskveryHigh
given dtpEligibleno
given januaryRegularPay0
expect jkkRate0.0174
expect employerPremiumsMonthly604000

Case 10 — N9 a married woman whose husband earns: own PTKP only

given grossMonthly15000000
given annualBonus0
given maritalStatusmarriedWoman
given dependants2
given bpjsEnrolledyes
given jkkRiskveryLow
given dtpEligibleno
given januaryRegularPay0
expect ptkp54000000
expect pph21Annual12279900

Case 11 — N10 a raise to Rp12m after January keeps the relief when January's regular pay was Rp10m

given grossMonthly12000000
given annualBonus0
given maritalStatussingle
given dependants0
given bpjsEnrolledno
given jkkRiskveryLow
given dtpEligibleyes
given januaryRegularPay10000000
expect taxableIncome84000000
expect pph21Annual6600000
expect governmentBorneTax6600000
expect netAnnual144000000

Sources

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.