How to Restrict Purchases by Country Using IP Geolocation?
Restricting purchases by country is a common requirement for modern e-commerce and SaaS platforms. Whether you are navigating complex tax jurisdictions like VAT/GST, complying with international trade sanctions, or mitigating high-risk fraud regions, a robust geographic filtering system is essential.
For backend engineers, this isn't just about a simple if statement. It involves managing IP intelligence, handling edge cases like VPNs/IPv6, and ensuring the restriction logic doesn't introduce significant latency into the checkout flow.

Why Restrict Purchases by Geography?
Before diving into the implementation, it is important to categorize why these restrictions are necessary, as the "strictness" of your implementation may vary based on the goal.
- Legal & Tax Compliance: Many digital services have zero-threshold tax obligations in regions like the EU, UK, and India. If you lack the infrastructure to remit VAT, it is often safer to block sales in those regions entirely.
- Licensing & Distribution: Media companies and software vendors often hold distribution rights that are legally limited to specific territories.
- Fraud Mitigation: High-value digital goods are frequent targets for card-testing attacks originating from specific network blocks or regions with low judicial recourse.
- Sanctions (OFAC/EU): Operating in sanctioned regions can result in severe legal penalties and the termination of payment processing agreements (e.g., Stripe, Adyen).
How IP-Based Restriction Works Internally
The process typically occurs at the application or middleware level. When a request hits your server, you extract the client's IP address and query a geolocation database or API to resolve that IP into a country code (ISO 3166-1 alpha-2).
The Logic Flow
- a. Extract IP: Retrieve the remote address. If your app is behind a load balancer (like Nginx or AWS ALB), you must parse the X-Forwarded-For header.
- b. Geo-Lookup: Send the IP to an intelligence service like ip2geoapi.com.
- c. Evaluate: Compare the returned country code against your "Allowed" or "Blocked" list.
- d. Action: Return a 403 Forbidden or a localized message to the frontend to disable the "Buy" button.
Internally, IP geolocation providers maintain large datasets that map IP ranges to geographic regions. These datasets are continuously updated using data from regional internet registries (RIRs), routing tables, and network telemetry.
Practical Backend Implementation
1. The API Request
Using a specialized IP intelligence API is preferred over local MaxMind databases for most cloud-native apps to avoid the overhead of constant manual database updates.
Example Request (cURL):
curl -X GET "https://api.ip2geoapi.com/ip/8.8.8.8?key=YOUR_API_KEY"
2. The JSON Response
A reliable API should return a structured response containing the ISO country code and security metadata.
{
"success": true,
"ip": "8.8.8.8",
"version": "ipv4",
"geo": {
"city": "Chicago",
"country": "United States",
"countryCode": "US",
"region": null,
"regionCode": null,
"latitude": 37.751,
"longitude": -97.822,
"postalCode": null,
"geonameId": 6252001,
"accuracyRadius": 1000,
"metroCode": null,
"continentName": "North America",
"continentCode": "NA",
"isEuMember": false
},
"countryInfo": {
"name": "United States of America",
"alpha2Code": "US",
"alpha3Code": "USA",
"flag": "https://api.ip2geoapi.com/assets/flags/us.svg",
"callingCodes": [
"1"
],
"currencies": [
{
"code": "USD",
"name": "United States dollar",
"symbol": "$"
}
],
"languages": [
{
"iso639_1": "en",
"iso639_2": "eng",
"name": "English",
"nativeName": "English"
}
]
},
"timezoneInfo": {
"timezone": "America/Chicago",
"utcOffsetSeconds": -21600,
"utcOffsetText": "-06:00",
"utcOffsetHours": -6,
"isDst": false,
"abbreviation": "CST",
"localTime": "2026-01-18T21:44:25-06:00"
},
"network": {
"cidr": "8.8.8.8/32",
"prefixLen": 32,
"asn": 15169,
"asFormatted": "AS15169",
"asName": "GOOGLE",
"isp": "Google",
"organization": "Google",
"connectionType": "Corporate",
"mobile": {
"mcc": null,
"mnc": null
}
},
"asDetails": {
"asn": 15169,
"abuser_score": "0.001 (Low)",
"descr": "GOOGLE, US",
"country": "us",
"active": true,
"org": "Google LLC",
"domain": "google.com",
"abuse": "[email protected]",
"type": "hosting",
"created": "2000-03-30",
"updated": "2012-02-24",
"rir": "ARIN"
},
"security": {
"isHosting": true,
"isProxy": false,
"proxyType": null,
"isVpn": false,
"vpnProvider": null,
"vpnProviderUrl": null,
"isTor": false,
"isAnonymous": true,
"trustScore": 65,
"riskLevel": "medium"
}
}
3. Backend Logic (Node.js/Express Example)
In a production environment, you should implement this as a middleware that runs specifically on your /api/v1/checkout or /api/v1/payment-intent endpoints.
const axios = require('axios');
const GEO_RESTRICTED_COUNTRIES = ['CN', 'RU', 'IR', 'KP'];
async function geoBlockMiddleware(req, res, next) {
// Get client IP, accounting for proxies
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
try {
const response = await axios.get(`https://api.ip2geoapi.com/ip/${ip}?key=${process.env.GEO_API_KEY}`);
// Block if country is restricted OR if user is masking their location via VPN
if (GEO_RESTRICTED_COUNTRIES.includes(response.geo.countryCode) || response.security.isVpn) {
return res.status(403).json({
error: "Service unavailable in your region.",
code: "GEO_RESTRICT_BLOCK"
});
}
next();
} catch (error) {
// Fallback: Decide whether to fail-open or fail-closed
console.error("GeoIP Lookup failed", error);
next();
}
}
Implementing Geofencing in Python with FastAPI
import httpx
from fastapi import FastAPI, Request, HTTPException, Depends, status
app = FastAPI()
# Configuration
API_KEY = "YOUR_API_KEY"
RESTRICTED_COUNTRIES = ["CN", "RU", "IR", "KP"]
MIN_TRUST_SCORE = 70
async def get_geo_data(request: Request):
# Retrieve the client IP (handling reverse proxies)
client_ip = request.headers.get("x-forwarded-for") or request.client.host
url = f"https://api.ip2geoapi.com/ip/{client_ip}?key={API_KEY}"
async with httpx.AsyncClient() as client:
try:
response = await client.get(url, timeout=2.0)
if response.status_code != 200:
# Fail-open: allow request if API is down, or log for manual review
return None
return response.json()
except Exception as e:
print(f"Geo-check error: {e}")
return None
def verify_purchase_eligibility(geo_data: dict = Depends(get_geo_data)):
if not geo_data or not geo_data.get("success"):
return # Allow if data is missing or API failed (fail-open strategy)
# Extract relevant fields from the nested structure
country_code = geo_data["geo"]["countryCode"]
security = geo_data["security"]
# Check 1: Geographical Blocklist
if country_code in RESTRICTED_COUNTRIES:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Service not available in your region."
)
# Check 2: Proxy/VPN detection for high-risk transactions
if security["isVpn"] or security["isProxy"] or security["isTor"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="VPN or Proxy detected. Please use a direct connection."
)
# Check 3: Risk Level Check
if security["trustScore"] < MIN_TRUST_SCORE:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Transaction flagged as high risk."
)
@app.post("/api/v1/purchase", dependencies=[Depends(verify_purchase_eligibility)])
async def process_purchase():
return {"message": "Purchase authorized and processing."}
We have other flags to detect if the user is anonymous. Checkout our guide on blocking Tor, VPN or proxy traffic.
Utilizing ip2geoapi.com for Reliable Restrictions
For developers who need a production-ready solution without the high cost of enterprise-grade intelligence, ip2geoapi.com offers a specialized IP intelligence service.
It provides high-accuracy mapping for both IPv4 and IPv6 addresses. This is critical because as IPv4 address space becomes exhausted, more residential traffic moves to IPv6, which many legacy geo-databases handle poorly.
- Generous Free Tier: You can start with 100,000 free requests per month, which is more than enough for most growing SaaS platforms or e-commerce stores.
- High Performance: Low-latency response times ensure that your checkout process remains snappy.
- Security Metadata: Beyond just country codes, it provides data on VPNs and proxies, allowing you to block users attempting to bypass your geographic restrictions.
You can get a free API key here and be integrated in minutes.
Handling Edge Cases and Pitfalls
IPv4 vs. IPv6
Do not assume every request will be IPv4. If your server or load balancer is dual-stack, you will receive IPv6 addresses (e.g., 2001:4860:4860::8888). Ensure your backend logic and your API provider support both formats natively.
VPN, tor and Proxy Bypassing
A user in a restricted country can easily use a VPN to appear as if they are in the US.
The Fix: Use an API that provides a isVpn, isTor or isProxy flag. If the goal is strict legal compliance (e.g., online gambling or sanctioned goods), you should block all non-residential IP types. We offer all these 3 flags, in addition to the isHosting flag. You can see this article on blocking traffic from high risk countries.
False Positives (The "Mobile" Problem)
Users on mobile data (CGNAT) may sometimes have their IP registered in a different city or neighboring small country depending on how their ISP routes traffic.
Best Practice: Always allow users to manually select their country in the UI for shipping purposes, but use the IP data as a "hard gate" for digital-only services.
Common Implementation Mistakes
| Mistake | Why It’s a Problem |
|---|---|
| Enforcing rules only on frontend | Easily bypassed |
| Trusting unvalidated headers | Can be spoofed |
| Blocking entire countries without logging | Difficult to debug false positives |
| Assuming 100% accuracy | Leads to user friction |
| Ignoring IPv6 | Incomplete enforcement |
Performance and Security Considerations
- Caching: Do not query the API for every single page load. Cache the geo-result in the user's session or a Redis store for 24 hours based on their IP address.
- Fail-Open vs. Fail-Closed: If the API is unreachable, should you allow the purchase?
- Fail-Open: Better for UX/Revenue (allow the purchase if the system is down).
- Fail-Closed: Better for Compliance (block everything if you can't verify the location).
- Accuracy Limits: No IP geolocation is 100% accurate. Generally, country-level accuracy is ~99%, while city-level is ~70-80%. For purchase restrictions, country-level is usually sufficient.
Conclusion
Restricting purchases by country is a vital component of a secure and compliant backend architecture. By implementing a server-side check using a high-fidelity intelligence service like ip2geoapi.com, you can automate compliance, reduce fraud, and ensure your business operates within its legal boundaries.