Integrations
Local Pricing for SaaS Plans
Turn a USD plan price into a defensible local price with IMF PPP factors, explicit floors and ceilings, and a documented rounding rule.
The job
Price a subscription for several markets without treating purchasing-power
parity as an exchange rate. Use /v1/ppp for one
country’s factor, /v1/convert?ppp=true for a
PPP-adjusted amount, and /v1/rates when you need a
nominal market conversion.
The goal is a local price you can defend, not a mathematically pure number. PPP is an input to the pricing decision, not the decision itself.
Two different numbers
| Number | What it answers | Endpoint |
|---|---|---|
| Market conversion | What the local currency is worth at the FX rate | /v1/convert |
| PPP conversion | What a basket of goods costs at local price levels | /v1/convert?ppp=true |
| PPP factor | Local currency units per international dollar | /v1/ppp |
A PPP factor of 20.397 for India means goods costing one international dollar
in the United States would cost about 20.4 units of Indian currency in India at
PPP. It is a price level, not a quote. The market FX rate can be very different.
/v1/ppp returns:
{
"year": 2024,
"countryCode": "IND",
"pppFactor": 20.397,
"projected": false
}
The pricing pipeline
- Start from the plan’s USD price.
- Fetch the country’s PPP factor for the factor year.
- Multiply the USD price by the factor to get a raw local price.
- Apply a commercial floor and ceiling in local currency.
- Round to a presentable local price.
- Record the country, factor, factor year, raw price and posted price.
Steps 4 and 5 are business decisions. Document them, because a support agent will eventually need to explain why the Indian price is not exactly 20.4 times the US price.
A complete pricing function
type PppResponse = {
year: number;
countryCode: string;
pppFactor: number;
projected: boolean;
};
type LocalPrice = {
country: string;
currency: string;
factorYear: number;
pppFactor: number;
projected: boolean;
basePriceUsd: number;
rawLocalPrice: number;
postedLocalPrice: number;
};
const ZERO_DECIMAL = new Set(["JPY", "KRW", "VND", "CLP", "ISK"]);
async function fetchPpp(country: string, year = 2024): Promise<PppResponse> {
const url = new URL("https://api.currency-core.com/v1/ppp");
url.searchParams.set("country", country);
url.searchParams.set("year", String(year));
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.CURRENCECORE_API_KEY}` },
signal: AbortSignal.timeout(8000),
});
const body = await res.json();
if (!res.ok) {
throw Object.assign(new Error(body.error?.message ?? `HTTP ${res.status}`), {
status: res.status,
code: body.error?.code,
});
}
return body as PppResponse;
}
function roundLocal(value: number, currency: string) {
// Replace this with your own charm-price rule. This one rounds to .99 and
// uses whole units for zero-decimal currencies.
if (ZERO_DECIMAL.has(currency)) return Math.max(1, Math.round(value / 10) * 10 - 1);
return Math.max(0.99, Math.round(value) - 0.01);
}
export async function localPrice(input: {
basePriceUsd: number;
country: string;
currency: string;
year?: number;
floor?: number;
ceiling?: number;
}): Promise<LocalPrice> {
const ppp = await fetchPpp(input.country, input.year ?? 2024);
const raw = input.basePriceUsd * ppp.pppFactor;
const bounded = Math.min(
Math.max(raw, input.floor ?? raw),
input.ceiling ?? raw,
);
return {
country: ppp.countryCode,
currency: input.currency,
factorYear: ppp.year,
pppFactor: ppp.pppFactor,
projected: ppp.projected,
basePriceUsd: input.basePriceUsd,
rawLocalPrice: raw,
postedLocalPrice: roundLocal(bounded, input.currency),
};
}
The currency code is not part of the /v1/ppp response. Read it from
/v1/countries and pass it in, or keep a small country to
currency map for the markets you price.
Worked example
A $29 monthly plan, using the 2024 PPP factors published in the endpoint examples and the site dataset. Factors are rounded for display; the API is the source of truth.
| Market | Country | Currency | Factor | Raw local | Posted local |
|---|---|---|---|---|---|
| United States | USA | USD | 1.000 | 29.00 | 29.00 |
| India | IND | INR | 20.397 | 591.51 | 599.00 |
| Germany | DEU | EUR | 0.722 | 20.94 | 20.99 |
| France | FRA | EUR | 0.644 | 18.68 | 18.99 |
| United Kingdom | GBR | GBP | 0.665 | 19.29 | 19.99 |
The posted column uses the charm-price rule in the code above. It is an example, not a recommendation. A different rule will produce different prices from the same raw values.
Floors, ceilings and unit economics
A floor is not “20 percent below the raw price.” It is the lowest price that still covers support, payment fees and tax in that market. Set it in local currency and store it with the price.
Examples:
| Market | Raw | Floor | Ceiling | Posted |
|---|---|---|---|---|
| India | 591.51 | 499 | 899 | 599 |
| United Kingdom | 19.29 | 15 | 25 | 19.99 |
If the raw value is below the floor, charge the floor. If it is above the ceiling, charge the ceiling. A ceiling is useful when a high PPP factor would make the local price look implausible next to competitors or next to the purchasing power of the audience.
Do not use the market FX rate to set the floor. Use your cost to serve and the price you are willing to accept.
Actual years versus projections
/v1/ppp can return projected: true for an IMF forecast year. For a live price,
prefer actual data. The configured factor year is 2024, and
/v1/convert?ppp=true uses actual years only, not projections.
If you price with a projected year, label it as a scenario in your internal review. Do not present a forecast factor as observed history in a customer-facing explanation.
What not to do
- Do not treat a PPP factor as an exchange rate.
- Do not take a PPP price and then route it through a market conversion as if it were an FX charge. Charge the local price you set.
- Do not use one global multiplier for every market. The factor is country-level for a reason.
- Do not ignore tax, payment fees and minimum charge amounts. PPP covers price levels, not your cost structure.
- Do not compare a 2024 factor with a 2026 factor without checking the year.
Failure handling
| Status | What to do |
|---|---|
400 invalid_input | Check the alpha-3 country code and the four-digit year |
401 unauthorized | Check the server-side key |
429 rate_limited | Honor Retry-After; PPP factors change annually, so cache aggressively |
5xx | Retry with backoff, or use the last known factor with a visible date |
/v1/ppp requires a key but has no Growth gate. It is metered like any other
keyed request. See rate limits.