Stripe with Purchasing Power Parity across countries for Global SaaS (World Bank data)
This post walks through implementing Purchasing Power Parity (PPP) pricing per country for a Stripe Checkout purchase flow. But you can use it for any other payment gateway, such as Paddle, PayPal, Lemon Squeezy, e.t.c, just understand the logic from high level. Live demo is available at https://agenytics.com.
The reference implementation behind this article is built in PHP (Laravel 12) and it's available in https://github.com/MuhammadQuran17/ppp_stripe, but the approach is language-agnostic. This package uses World Bank PPP data and TrustIP to detect VPN/Proxy. The goal is to understand the logic and architecture so you can reproduce it in any programming language (JS, Python, Go, Ruby etc.).
How it works (end-to-end)
Use World Bank CSV data
↓
Import into DB (ppp_data)
User clicks “Buy”
↓
Get real client IP
↓
Proxy/VPN check (if proxy → disable PPP default to US price)
↓
Country detection (Cloudflare header or fallback IP API)
↓
ISO2 to ISO3 conversion
↓
Lookup PPP factor from DB
↓
Compute adjusted unit price
↓
Create Stripe Checkout Session (dynamic price_data)
Each step is explained in details in the next sections.
Step 1 — Download fresh Purchasing Power Parity CSV data from the World Bank
World Bank provides the indicator PPP conversion factor to market exchange rate ratio as a CSV file.
Download the latest CSV file and save it somewhere private on your server (for Laravel, it should be in storage/app/private/ppp_world.csv, currently we have data from "2025-12-19" in our package).
Step 2 — Import & normalize PPP into your database
Instead of reading the CSV at runtime, import it into a table (e.g. ppp_data) so lookups are fast and reliable.
DB schema for ppp_data table
Columns:
country_code(string, unique)country_name(string)latest_ppp_value(decimal('latest_ppp_value', 10, 9))- timestamps (optional but recommended)
Please look at the ImportPPPData.php file for the implementation details in PHP Laravel and how to run it.
Step 3 — Determine the user’s country (ISO2)
Your pricing decision starts with “which country is this user in?”
In our package we use:
- Cloudflare
CF-IPCountryheader (best when available) - Fallback to IP geolocation API:
http://ipwhois.app/json/<ip>
Option A (recommended): Cloudflare header
If you use Cloudflare, use below header to get the country code:
CF-IPCountry: an ISO2 code likeUS,DE,BR
Why it’s nice:
- It’s fast (no outbound HTTP call).
- It’s consistent.
Option B: IPWhois fallback
If the Cloudflare header isn’t present or you don't use Cloudflare, call:
http://ipwhois.app/json/<ip>
Parse:
country_code(ISO2)
Important: always implement timeouts and graceful fallbacks (default to US if the call fails).
Step 4 — Detect VPN/Proxy and disable PPP (anti-abuse)
PPP discounts are frequently abused via VPNs and proxies. A common pattern is:
- If request IP is proxy/VPN → disable PPP and charge the default (e.g. US) price.
In the Laravel reference implementation, a service wraps TrustIP proxy detection:
ProxyIpDetectionService::isProxy($ip): bool
You can swap TrustIP for any other provider—what matters is the decision gate:
Step 5 — Convert ISO2 → ISO3, then compute an adjusted price
World Bank country codes in the CSV are ISO3, but geolocation providers and Cloudflare return ISO2.
So you need a conversion step:
- ISO2
UZ→ ISO3UZB - ISO2
BR→ ISO3BRA
There are ready to use ISO-3166 mapping libraries available in a lot of programming languages. For example, in PHP we have used league/iso3166 library.
Price calculation (as implemented)
This implementation calculates:
adjusted_price = base_price * latest_ppp_value_for_country
Step 6 — Create Stripe Checkout with dynamic price_data
Once you’ve computed your adjusted price on the server, you create a Stripe Checkout session.
Create a new Product in Stripe. For pricing select "Customer chooses price" pricing model with price in USD. "Unit quantity" should be 1, "custom price" should be 0.
So the main logic is to create a dynamic pricing in payment gateway, so you can send from your backend the amount in USD.
Don’t create a Stripe Price per country.
Stripe checkout shape (conceptually)
{
"line_items": [
{
"price_data": {
"currency": "usd",
"product": "prod_123",
"unit_amount": 1999
},
"quantity": 1
}
],
"success_url": "https://example.com/success",
"cancel_url": "https://example.com/cancel"
}
Currency and cents
Stripe amounts are typically in the smallest currency unit:
- USD: cents →
unit_amount = round(price, 2) * 100
