Show "net per month" on job ads in Python
Turn the annual gross salary range of German and UK job ads into a net-per-month range with one API call per four ads, using the official BMF and HMRC algorithms.
The use case
You run a job board or a salary-comparison site. Candidates don't think in gross annual salary: they want to know what lands in their account each month. A range like "65,000–80,000 EUR in Munich" or "50,000–62,000 GBP in Edinburgh" only becomes comparable once German wage tax and social insurance, or UK PAYE (with Scottish rates) and National Insurance, are applied.
POST /compare takes up to 8 scenarios per call and costs 1 credit, so one request covers both ends of the range for four ads. For a job ad you don't know the candidate's personal situation, so the example uses a standard profile: single, no children, no church tax, the fund's average health add-on rate in Germany, and the standard tax code in the UK. Say so next to the figure on your site.
Step 1: Get an API key
Buy credits at https://syntropicapi.com/pricing and export the key:
export SYNTROPIC_API_KEY="your-key"
pip install requests
If you use RapidAPI instead, replace the URL and headers with the ones in the RapidAPI code snippets.
Step 2: Map each ad to scenarios
A scenario is the same body as POST /net: country, year, gross, period and a profile. For Germany, state is the Bundesland of the workplace. It sets the church-tax rate (if church is true) and the Saxony care-insurance split (Leipzig is in SN). For the UK, region selects Scottish or Welsh income tax rates, and the API picks the matching tax code (S1257L for Scotland).
Step 3: Call the API and print the ranges
Save this as job_board_net.py:
import os
import requests
API = "https://syntropicapi.com/v1/salary-net"
HEADERS = {"X-API-Key": os.environ["SYNTROPIC_API_KEY"]}
# Job ads with an annual salary range, as your job board stores them.
jobs = [
{"title": "Backend Engineer, Munich", "country": "DE", "min": 65000, "max": 80000, "state": "BY"},
{"title": "Data Analyst, Leipzig", "country": "DE", "min": 48000, "max": 56000, "state": "SN"},
{"title": "Product Manager, London", "country": "UK", "min": 60000, "max": 75000, "region": "england"},
{"title": "Software Engineer, Edinburgh", "country": "UK", "min": 50000, "max": 62000, "region": "scotland"},
]
def scenario(job, gross):
"""One /compare scenario: the annual gross for a single employee without children (a standard profile)."""
s = {"country": job["country"], "year": 2026, "gross": gross, "period": "year"}
if job["country"] == "DE":
s["de"] = {"tax_class": 1, "state": job["state"]}
else:
s["uk"] = {"region": job["region"]}
return s
# Both ends of the range for two ads per request: 4 ads x 2 = 8 scenarios, the /compare maximum.
scenarios = [scenario(job, job[end]) for job in jobs for end in ("min", "max")]
r = requests.post(f"{API}/compare", json={"scenarios": scenarios}, headers=HEADERS, timeout=30)
r.raise_for_status()
rows = r.json()["rows"]
for i, job in enumerate(jobs):
lo, hi = rows[2 * i], rows[2 * i + 1]
cur = "EUR" if lo["currency"] == "EUR" else "GBP"
print(f"{job['title']}: {job['min']:,}-{job['max']:,} {cur} gross/year")
print(f" ≈ {lo['annual_net'] / 12:,.0f}-{hi['annual_net'] / 12:,.0f} {cur} net per month "
f"({lo['deduction_rate']}-{hi['deduction_rate']} % deductions, {lo['label']})")
print("Credits left:", r.headers.get("X-Credits-Remaining"))
Run it:
python job_board_net.py
Output (run on 29 September 2026 against the live API):
Backend Engineer, Munich: 65,000-80,000 EUR gross/year
≈ 3,341-4,004 EUR net per month (38.32-39.94 % deductions, DE 2026, tax class 1)
Data Analyst, Leipzig: 48,000-56,000 EUR gross/year
≈ 2,592-2,942 EUR net per month (35.21-36.95 % deductions, DE 2026, tax class 1)
Product Manager, London: 60,000-75,000 GBP gross/year
≈ 3,780-4,505 GBP net per month (24.4-27.92 % deductions, UK 2026-27, 1257L)
Software Engineer, Edinburgh: 50,000-62,000 GBP gross/year
≈ 3,169-3,728 GBP net per month (23.95-27.85 % deductions, UK 2026-27, S1257L)
Credits left: 192
The whole run cost 1 credit.
Step 4: Put it on the page
- Cache the result per ad and tax year. The figures only change when the ad's salary changes or a new tax year starts (January in Germany, April in the UK), so you don't need to call the API on every page view.
- Show the assumptions next to the figure, e.g. "Estimate for a single employee without children, 2026 rules". The response's
disclaimeris a good base for the fine print. - Let candidates refine it. If a visitor enters their tax class, children or church membership, call
POST /netwith those fields indeand show the full breakdown fromlines. - Monthly gross in Germany: send
"period": "month"and the monthly amount. German gross pay up to 2,000 EUR per month (Midijob range) is rejected with a 400 error, so for mini-jobs and part-time ads show the gross figure only.
What to know
- Amounts stay in the country's currency. Don't compare EUR and GBP figures without converting them yourself;
deduction_ratecompares directly. - Results are estimates, not a payroll run and not tax advice. Pension contributions (UK auto-enrolment) and student loans are not deducted.
- The full reference is in the documentation, including every profile field.