The Tax Brief real effective rates for 111+ countries — bi-weekly, free.
API GUIDE

Salary Comparison API — Multi-Country Take-Home Pay

CountryTaxCalc Research Team · About Updated July 2026
Tax data in examples is for informational and product integration purposes only, sourced from official government tax authorities. Consumer-facing products must include appropriate disclaimers — see the API Terms of Service and Legal Disclaimer.
Quick answer: On any tier, loop POST /calculate once per country. On Pro+, use POST /compare for up to 20 countries in one call with pre-built ranking and savings analysis. On Growth+, use GET /summary for a quick ranked snapshot across all 111 countries.

Two approaches to multi-country comparison

There are two patterns depending on your plan tier:

Approach Tier required Countries per call Pre-built ranking Savings analysis
Loop POST /calculate Free+ 1 per request Build yourself Calculate yourself
POST /compare Pro+ Up to 20 per call Yes (included) Yes (annual + 10yr)
GET /summary Growth+ All 111 countries Yes (ranked) No

The loop approach is the right starting point for most products — it works on the free tier, consumes one request per country, and gives you the same underlying data as POST /compare. Upgrade to Pro for the convenience of single-call comparison and the pre-built savings analysis.

Loop approach (all tiers)

Call POST /calculate once per country, then aggregate the results. The example below fetches 10 countries concurrently using Promise.all and sorts by net income:

// server-side only — API key in environment variable
const API_KEY = process.env.COUNTRYTAXCALC_API_KEY;
const BASE_URL = "https://api.countrytaxcalc.com/api/v1";

async function compareCountries(income, countries, currency = "USD") {
  const results = await Promise.all(
    countries.map(async (country) => {
      const resp = await fetch(`${BASE_URL}/calculate`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-API-Key": API_KEY
        },
        body: JSON.stringify({ income, country, currency })
      });

      if (!resp.ok) {
        console.warn(`Failed for ${country}: ${resp.status}`);
        return null;
      }

      return resp.json();
    })
  );

  const valid = results.filter(Boolean);

  // Sort by net income descending
  valid.sort((a, b) => b.totals.net_income - a.totals.net_income);

  // Add rank and savings vs. top result
  const topNet = valid[0]?.totals.net_income ?? 0;
  return valid.map((r, i) => ({
    rank: i + 1,
    country: r.country,
    country_code: r.country_code,
    currency: r.currency,
    currency_symbol: r.currency_symbol,
    net_income: r.totals.net_income,
    monthly_net: r.totals.monthly_net_income,
    effective_rate: r.totals.effective_tax_rate,
    vs_best: r.totals.net_income - topNet,  // negative = less than best
    confidence: r.metadata.accuracy_confidence
  }));
}

// Usage
const countries = ["GB", "DE", "NL", "IE", "SE", "CH", "SG", "AE", "AU", "CA"];
const comparison = await compareCountries(100000, countries, "USD");

comparison.forEach(r => {
  const approx = r.confidence < 0.9 ? "~" : "";
  const diff = r.vs_best < 0
    ? ` (${r.currency_symbol}${Math.abs(r.vs_best).toLocaleString()} less than #1)`
    : " (best)";
  console.log(
    `#${r.rank} ${r.country}: ${approx}${r.currency_symbol}${r.net_income.toLocaleString()} net, ` +
    `${r.effective_rate}% effective rate${diff}`
  );
});

Python equivalent

import asyncio
import os
import httpx

API_KEY = os.environ.get("COUNTRYTAXCALC_API_KEY")

async def compare_countries(income: float, countries: list, currency: str = "USD") -> list:
    semaphore = asyncio.Semaphore(5)  # limit concurrency

    async def fetch_one(client, country):
        async with semaphore:
            resp = await client.post(
                "https://api.countrytaxcalc.com/api/v1/calculate",
                headers={"Content-Type": "application/json", "X-API-Key": API_KEY},
                json={"income": income, "country": country, "currency": currency},
                timeout=10
            )
            return resp.json() if resp.status_code == 200 else None

    async with httpx.AsyncClient() as client:
        results = await asyncio.gather(*[fetch_one(client, c) for c in countries])

    valid = [r for r in results if r]
    valid.sort(key=lambda r: r["totals"]["net_income"], reverse=True)
    return valid

countries = ["GB", "DE", "NL", "IE", "SE", "CH", "SG", "AE", "AU", "CA"]
results = asyncio.run(compare_countries(100000, countries, "USD"))

for i, r in enumerate(results, 1):
    sym = r["currency_symbol"]
    net = r["totals"]["net_income"]
    rate = r["totals"]["effective_tax_rate"]
    print(f"#{i} {r['country']}: {sym}{net:,.0f} net ({rate}% effective)")

POST /compare (Pro and Enterprise only)

Pro and Enterprise only. POST /compare is not available on Free, Starter, or Growth plans. On those tiers, use the loop approach above — the result is equivalent, it just consumes one request per country.

POST /compare accepts up to 20 country entries in a single request and returns a full comparison with pre-built ranking and savings analysis:

POST /api/v1/compare
{
  "income": 100000,
  "currency": "USD",
  "countries": [
    { "country": "GB" },
    { "country": "DE" },
    { "country": "NL" },
    { "country": "US", "jurisdiction": "CA" },
    { "country": "AE" },
    { "country": "SG" },
    { "country": "CH" },
    { "country": "IE" }
  ]
}

Response shape:

{
  "income": 100000,
  "currency": "USD",
  "results": [
    {
      "rank": 1,
      "country": "United Arab Emirates",
      "country_code": "AE",
      "net_income": 100000,
      "effective_tax_rate": 0,
      "take_home_percentage": 100
    },
    {
      "rank": 2,
      "country": "Singapore",
      "country_code": "SG",
      "net_income": 86420,
      "effective_tax_rate": 13.58,
      "take_home_percentage": 86.42
    }
    // ... remaining countries ranked by net income
  ],
  "summary": {
    "best_for_net_income": {
      "country": "United Arab Emirates",
      "country_code": "AE",
      "net_income": 100000,
      "effective_tax_rate": 0
    },
    "best_for_low_tax": {
      "country": "United Arab Emirates",
      "country_code": "AE",
      "effective_tax_rate": 0
    },
    "worst_for_net_income": {
      "country": "Belgium",
      "country_code": "BE",
      "net_income": 53420,
      "effective_tax_rate": 46.58
    },
    "savings_potential": {
      "annual": 46580,
      "ten_year": 465800
    }
  }
}

The summary.savings_potential.annual field — the difference between the best and worst jurisdiction's net income — is a compelling display figure for relocation and job-offer comparison tools. It answers the question: "how much more could I take home per year?"

Currency baseline for meaningful comparisons

When comparing countries, always use a common currency baseline. Without one, you're comparing Swiss francs to British pounds to US dollars — the effective rates are still comparable, but the net income figures are meaningless side-by-side.

Pass "currency": "USD" (or any ISO 4217 code) in every request. The API will:

  1. Convert your USD income to the country's local currency using ECB exchange rates
  2. Calculate tax using local rates on the converted income
  3. Return the net income in local currency (e.g. GBP for UK, EUR for Germany)
  4. Include the exchange rate used in currency_conversion metadata

The effective rate is always comparable across currencies — 24% in the UK means 24% regardless of whether the income was specified in GBP or USD. The net income figures will be in each country's local currency, so display those with their currency_symbol.

To display all net incomes in a single currency for easier comparison, convert locally:

// Convert all results to USD for display
const exchangeRates = {}; // cache as you fetch

results.forEach(r => {
  if (r.currency_conversion) {
    // The API tells you the exchange rate it used
    const rate = r.currency_conversion.exchange_rate;
    const usdNet = r.totals.net_income / rate;
    r.net_income_usd = Math.round(usdNet);
  } else {
    // Already in USD (for US results)
    r.net_income_usd = r.totals.net_income;
  }
});

GET /summary (Growth and above)

For tools that want to show all 111 countries ranked at once, GET /api/v1/summary returns a simplified snapshot in a single request — one request instead of 111. Available on Growth, Pro, and Enterprise plans.

GET /api/v1/summary?income=100000¤cy=USD

// Response
{
  "income": 100000,
  "currency": "USD",
  "countries": [
    {
      "rank": 1,
      "country_code": "AE",
      "country": "United Arab Emirates",
      "net_income": 100000,
      "effective_tax_rate": 0,
      "currency": "AED"
    },
    // ... all 111 countries ranked
  ],
  "total": 111
}

The summary endpoint returns simplified data — net income, effective rate, and rank. For full breakdowns (deductions, brackets, social contributions), use the loop approach with POST /calculate for the specific countries you want to drill into.

Display patterns for comparison results

Common UI patterns for salary comparison tools:

Ranked table

The most common pattern — a sortable table with country, net income, effective rate, and monthly equivalent. Highlight the top result. Use the confidence field to add an asterisk to estimated results.

Bar chart

Horizontal bars showing net income for each country, with effective rate as a secondary label. Visually intuitive — users immediately see the spread between highest and lowest take-home.

Savings highlight

Pull out the gap between the user's current country and the best alternative: "Moving from Germany to Singapore at €100,000 income could increase your take-home by ~€18,000/year." This is the savings_potential.annual value from POST /compare, or a calculation you can derive from the loop approach.

Compliance note

Required disclaimer: All consumer-facing comparison tools must include language stating results are approximate and for informational purposes only. Relocating countries for tax purposes involves legal, visa, residency, and personal circumstances the API cannot model. Users should consult a qualified tax professional and immigration adviser before making decisions.

Frequently asked questions

Q: What's the difference between POST /compare and looping POST /calculate?

POST /compare (Pro+) accepts up to 20 countries in one request and returns a pre-built ranked comparison with summary.best_for_net_income, summary.best_for_low_tax, and summary.savings_potential — calculated for you. Looping POST /calculate returns the same underlying data but requires you to aggregate and rank the results yourself. Both approaches produce equivalent net income and effective rate numbers — the difference is convenience and request count.

Q: How many countries can I compare in one POST /compare call?

Up to 20 countries per request on Pro and Enterprise plans. If you need to compare all 111 countries, use GET /api/v1/summary (Growth+) which returns a ranked snapshot across all countries at a given income level in a single call.

Q: Can I compare a US state against an international country?

Yes. For US states, pass {"country": "US", "jurisdiction": "CA"} in the countries array. The API returns a combined federal + state breakdown, which you can rank alongside international results. The comparison is most meaningful when you use a common currency baseline (pass currency: "USD" to all entries).

Q: What currency should I use for the comparison?

Use a single common currency for all countries in the comparison — typically USD or the user's home currency. Pass "currency": "USD" in every request and the API converts each country's local income to USD before calculating, then returns results in local currency with full conversion metadata. This makes the effective rate comparison meaningful: everyone's rate is calculated on the same USD-equivalent income.

Q: How do I handle jurisdictions with zero income tax?

The API returns valid responses for zero-tax jurisdictions (UAE, Saudi Arabia, Bahrain, Cayman Islands, etc.) with totals.total_tax: 0 and totals.net_income equal to gross income (after currency conversion). These will always sort to the top of a net income ranking — which is intentional and correct for relocation or expat comparison tools.

Q: What does savings_potential show in POST /compare?

summary.savings_potential.annual is the difference in net income between the best and worst jurisdictions in the comparison. summary.savings_potential.ten_year is the 10-year equivalent (no compounding — simply annual × 10). These fields are only available in the POST /compare response, not from looping POST /calculate.

Ready to integrate? 1,000 requests/month free. No credit card required.
Get your free API key →