Net-to-gross salary offers with employer cost in JavaScript
Work out the gross salary that gives a candidate the net pay they asked for, with every deduction and your total employer cost, in Node.js.
The use case
You build an offer-letter tool, an employer-of-record calculator or a hiring budget planner. Candidates often negotiate in net terms: "I need 3,500 EUR net per month." To make an offer you need the gross salary that produces exactly that net pay under the candidate's personal situation, and finance needs the employer's total cost including the employer's social contributions.
POST /gross runs the same engine as POST /net in reverse and returns the smallest gross pay, to the cent, whose net pay reaches the target. The response includes the full breakdown and employer_cost.
Step 1: Get an API key
Buy credits at https://syntropicapi.com/pricing and export the key. The example uses the built-in fetch of Node.js 18 or later, so there is nothing to install.
export SYNTROPIC_API_KEY="your-key"
Step 2: Describe the candidate
The candidate will work in Hamburg (state is the Bundesland of the workplace), is married with one child and has chosen tax class III. Their health insurance fund charges an add-on rate (Zusatzbeitrag) of 2.5 %. If you leave that out, the API uses the official average for the year and says so in notes.
{"country": "DE", "year": 2026, "net": 3500, "period": "month", "de": {"tax_class": 3, "state": "HH", "children": 1, "health_insurance_addon_rate": 2.5}}
With one child, child_allowances defaults to 1 and parent to true, so no childless surcharge is added to long-term care insurance.
Step 3: Call the API
Save this as offer.mjs:
const API = "https://syntropicapi.com/v1/salary-net";
const headers = { "X-API-Key": process.env.SYNTROPIC_API_KEY, "Content-Type": "application/json" };
async function call(path, body) {
const res = await fetch(`${API}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
const data = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${JSON.stringify(data.detail)}`);
return data;
}
// The candidate asks for 3,500 EUR net per month. They will work in Hamburg, are married with one child
// (tax class 3) and their health fund charges a 2.5 % add-on rate.
const offer = await call("/gross", {
country: "DE", year: 2026, net: 3500, period: "month",
de: { tax_class: 3, state: "HH", children: 1, health_insurance_addon_rate: 2.5 },
});
console.log(`Gross needed: ${offer.gross} ${offer.currency}/month (net ${offer.net}, target ${offer.target_net})`);
console.log(`Employer cost: ${offer.employer_cost} ${offer.currency}/month, ${offer.annual.employer_cost} per year`);
console.log("\nEmployee deductions:");
for (const l of offer.lines.filter((l) => l.payer === "employee")) {
console.log(` ${l.label.padEnd(42)} ${l.amount.toFixed(2).padStart(9)} ${l.rule}`);
}
console.log("Employer contributions:");
for (const l of offer.lines.filter((l) => l.payer === "employer")) {
console.log(` ${l.label.padEnd(42)} ${l.amount.toFixed(2).padStart(9)}`);
}
console.log("\nNotes:", offer.notes.join(" "));
// The same candidate could also join the London office, where they ask for 3,000 GBP net per month.
const london = await call("/gross", { country: "UK", year: 2026, net: 3000, period: "month", uk: { region: "england" } });
console.log(`\nLondon: gross ${london.gross} ${london.currency}/month, employer cost ${london.employer_cost} ` +
`(${london.annual.employer_cost} per year, tax code ${london.profile.tax_code})`);
Run it:
node offer.mjs
Output (run on 29 September 2026 against the live API):
Gross needed: 4930.63 EUR/month (net 3500, target 3500)
Employer cost: 5971 EUR/month, 71652 per year
Employee deductions:
Lohnsteuer (wage tax) 397.66 PAP 2026: tax class 3, 1 child allowances, pay period month
Solidaritätszuschlag 0.00 PAP: 5.5 % of the wage tax with child allowances, zero below the exemption limit and phased in above it
Krankenversicherung (health) 421.57 14.6 % general rate + 2.5 % add-on, split equally
Pflegeversicherung (long-term care) 88.75 3.6 % total
Rentenversicherung (pension) 458.55 18.6 %, split equally
Arbeitslosenversicherung (unemployment) 64.10 2.6 %, split equally
Employer contributions:
Krankenversicherung (health) 421.57
Pflegeversicherung (long-term care) 88.75
Rentenversicherung (pension) 458.55
Arbeitslosenversicherung (unemployment) 64.10
Insolvenzgeldumlage (insolvency levy) 7.40
Notes: Employer cost excludes the fund-specific U1/U2 levies and statutory accident insurance (BG), which depend on the employer's health fund and industry.
London: gross 3758.87 GBP/month, employer cost 4260.15 (51121.8 per year, tax code 1257L)
The offer is 4,930.63 EUR gross per month, which gives exactly 3,500.00 EUR net. The company's cost is 5,971.00 EUR per month before the U1/U2 levies and accident insurance, which depend on the employer and are listed in notes as not included. If the candidate joins the London office instead, 3,000 GBP net needs 3,758.87 GBP gross and costs 4,260.15 GBP a month including employer's National Insurance.
Step 4: Use it in your product
- Show
linesin the offer letter or the approval form. Every line carries the rule applied (e.g. "18.6 %, split equally") and its official source, which helps when a candidate or finance asks where a number comes from. differencein the response isnet - target_net. It is 0 whenever the target can be reached to the cent; otherwise the API returns the next cent above.- Budget a whole year with
annual.employer_cost, or send"period": "year"with an annual net target. - For a German net target that needs a gross pay in the Midijob range (up to 2,000 EUR per month), the API returns 400
unsupported_profileinstead of an approximation. Handle that case in your UI. - Tax class IV with the factor procedure: send
"tax_class": 4, "factor": 0.912. A tax allowance on the tax card goes intoannual_tax_allowance.
Results are estimates for offers and budgeting, not a payroll run and not tax advice. See the documentation for every field and error.