# 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.

Canonical: https://currency-core.com/docs/guides/saas-pricing/
Updated: 2026-11-12
Published: 2026-11-11

---
## The job

Price a subscription for several markets without treating purchasing-power
parity as an exchange rate. Use [`/v1/ppp`](/docs/ppp-endpoint/) for one
country's factor, [`/v1/convert?ppp=true`](/docs/convert-endpoint/) for a
PPP-adjusted amount, and [`/v1/rates`](/docs/rates-endpoint/) 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:

```json
{
  "year": 2024,
  "countryCode": "IND",
  "pppFactor": 20.397,
  "projected": false
}
```

## The pricing pipeline

1. Start from the plan's USD price.
2. Fetch the country's PPP factor for the factor year.
3. Multiply the USD price by the factor to get a raw local price.
4. Apply a commercial floor and ceiling in local currency.
5. Round to a presentable local price.
6. 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

```ts
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`](/docs/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](/docs/rate-limits/).

## Next steps

- [PPP endpoint](/docs/ppp-endpoint/)
- [PPP analysis](/docs/ppp-analysis-endpoint/)
- [Convert endpoint](/docs/convert-endpoint/)
- [Countries](/docs/countries/)
- [E-commerce conversion](/docs/guides/ecommerce/)
- [All developer guides](/docs/guides/)
