Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To add IP-based geolocation to a Flask app, identify the client IP that your deployment can actually trust, validate it, and look it up using either a hosted service or a locally maintained GeoIP database. Treat the result as an estimate of a network’s likely location—not a person’s precise location or verified identity. The example below uses a hosted lookup; the guide also covers proxy configuration, a local database option, failure handling, and privacy decisions.

How IP geolocation fits into a Flask request

A browser does not hand your Python code a trustworthy location or client IP. Flask receives an HTTP request, and request.remote_addr exposes the address of the connection Flask sees. That may be the visitor’s public IP on a direct connection, but a reverse proxy or hosting platform may make the connection appear to come from the proxy instead. Flask’s deployment guide explains that a proxy can intercept external requests and forward them to the local WSGI server: Tell Flask it is Behind a Proxy.

Once you have a trustworthy address, your application can send it to a geolocation provider or query a local database. The returned country, region, city, or coordinates are inferences based on IP address data. They are not device GPS, proof of residence, or a reliable way to identify a particular person or household. MaxMind explicitly cautions against using GeoIP output to identify a particular address or household: MaxMind GeoIP2 Python repository.

Decide how to obtain the location

Approach What your app does Operational trade-offs
Hosted API Sends an IP address to a provider and receives a response over the network. Often straightforward to wire into a route, but adds vendor disclosure, network latency, rate limits, provider availability, terms, and possibly usage costs. Keep credentials server-side.
Local GeoIP database Uses a database file deployed with the application; a Python reader looks up the IP locally. A lookup does not require a live provider request, but you must review licensing, obtain and deploy the data, and manage updates and database availability.

There is no universal winner established by the cited documentation. Compare the provider’s permitted use, coverage, freshness, latency, outage behavior, update work, deployment footprint, and total cost against your application’s needs. MaxMind documents both a Python database reader and hosted web services: Python repository and web services.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Review privacy and terms before sending or retaining IPs

IP addresses and location data can be personal data. The European Data Protection Board lists both as examples and explains that obligations depend on processing context and risk: EDPB FAQ. For an EU/EEA-facing service, assess whether GDPR applies to your organization and processing, determine an appropriate lawful basis and transparency approach, and establish access and retention controls. The EDPB describes principles including purpose limitation, data minimisation, accuracy, storage limitation, integrity, and confidentiality: basic principles and legal basis. This is general guidance, not a legal conclusion for a particular deployment.

Also check the actual provider terms for your use case. For example, IP-API.com says its unauthenticated service is limited to non-commercial purpose/environment, lists a 45-requests-per-minute limit, and requires Pro for commercial use. These are that provider’s terms, not general rules for geolocation APIs; verify the current conditions before shipping: IP-API.com terms and API documentation.

Configure trusted proxy handling before lookup

Do not blindly use the first value in X-Forwarded-For. A client can supply forwarding headers unless your edge proxy removes or overwrites them, and the correct trusted count depends on the actual network path. Configure the proxy to overwrite or safely append the forwarding headers, then set Werkzeug’s ProxyFix counts to the number of trusted proxies that set each header.

from flask import Flask
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)

# Example only: use these counts only if your infrastructure has exactly
# one trusted proxy setting these values. Configure your edge proxy accordingly.
app.wsgi_app = ProxyFix(
    app.wsgi_app,
    x_for=1,
    x_proto=1,
    x_host=1,
    x_port=1,
    x_prefix=1,
)

Set a count to zero when that header is not supplied by a trusted proxy. Do not copy the example counts without checking your load balancer, ingress, CDN, and hosting setup. Flask’s middleware documentation explains the trust configuration: ProxyFix guidance. With the middleware correctly configured, Flask’s remote address reflects the forwarded client address as interpreted through those trusted proxy hops.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Implement a hosted lookup in Flask

The following example uses the IP-API.com JSON endpoint shape and the Python requests library. Confirm that the provider permits your use and that its current endpoint and terms fit production before deploying. The example deliberately rejects non-public addresses for an external lookup, uses a finite timeout, and converts provider/network failures into a controlled response rather than an unhandled exception.

import ipaddress
import os

import requests
from flask import Flask, jsonify, request
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)

# Configure from your real topology. This example assumes one trusted proxy;
# use zero for headers not supplied by a trusted proxy.
app.wsgi_app = ProxyFix(
    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1
)

GEO_API_URL = "http://ip-api.com/json/{}"


def public_client_ip():
    """Return a normalized public IP, or None when it is unsuitable."""
    value = request.remote_addr
    if not value:
        return None
    try:
        address = ipaddress.ip_address(value)
    except ValueError:
        return None
    if not address.is_global:
        return None
    return str(address)


@app.get("/where-am-i")
def where_am_i():
    ip = public_client_ip()
    if ip is None:
        return jsonify(error="No usable public client IP is available"), 400

    try:
        response = requests.get(
            GEO_API_URL.format(ip),
            params={"fields": "status,message,country,regionName,city,timezone"},
            timeout=(2, 5),  # connect timeout, then read timeout
        )
        response.raise_for_status()
        payload = response.json()
    except (requests.RequestException, ValueError):
        app.logger.warning("Geolocation provider request failed")
        return jsonify(error="Location lookup is temporarily unavailable"), 502

    if payload.get("status") != "success":
        return jsonify(error="Provider could not locate this IP"), 422

    # Return only the fields this route needs; avoid logging or storing the raw IP.
    return jsonify(
        country=payload.get("country"),
        region=payload.get("regionName"),
        city=payload.get("city"),
        timezone=payload.get("timezone"),
        precision="approximate",
    )


if __name__ == "__main__":
    app.run()

Install the dependency with python -m pip install Flask requests. The route is a synchronous example: the outbound lookup occupies a Flask worker until it returns or times out. In a higher-throughput application, consider moving lookup work to a background task or using a suitable asynchronous architecture rather than allowing slow provider responses to tie up request workers.

Adapt the endpoint and response handling deliberately

The code illustrates validation and failure boundaries; it is not a provider-neutral API contract. Providers differ in URL, authentication, response fields, error codes, rate limits, and commercial permissions. If the chosen service requires an API key, read it from deployment secrets or an environment variable, never from browser JavaScript or source control. Limit the fields requested and returned to what the feature actually uses.

Private, loopback, reserved, missing, malformed, and unrecognized addresses should have an explicit policy. The example declines non-global inputs rather than asking a public lookup service about local addresses. Some providers return null or incomplete locations for private or unrecognized inputs. Avoid fabricating a city or treating missing fields as a definitive negative result.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use a local database when it fits your operations

A local database reader changes the lookup boundary: the application reads an installed GeoIP database instead of sending each IP to a hosted lookup endpoint. That can remove the per-request external API round trip, but it does not remove the need to protect the incoming IP, review data licensing, or keep the database current. MaxMind’s Python repository documents its reader/client and installation details: GeoIP2 for Python.

import ipaddress
import geoip2.database
from flask import jsonify, request

reader = geoip2.database.Reader("/path/to/GeoLite2-City.mmdb")

@app.get("/where-am-i-local")
def where_am_i_local():
    value = request.remote_addr
    try:
        address = ipaddress.ip_address(value or "")
    except ValueError:
        return jsonify(error="No usable client IP is available"), 400
    if not address.is_global:
        return jsonify(error="No public IP location is available"), 422

    try:
        result = reader.city(str(address))
    except geoip2.errors.AddressNotFoundError:
        return jsonify(error="No location record for this IP"), 404

    return jsonify(
        country=result.country.name,
        region=(result.subdivisions.most_specific.name
                if result.subdivisions else None),
        city=result.city.name,
        precision="approximate",
    )

Use the database product and license appropriate to your deployment; the sample path is a deployment-specific file location, not a download instruction. Arrange safe reader lifecycle management for your app server and update the database through the vendor’s permitted process. A local lookup can still be incomplete or inaccurate; it is not a substitute for verified user-provided information.

Handle accuracy, caching, and reliability

  • Use broad outputs for broad decisions. Country or region is often more defensible than precise-looking coordinates. MaxMind warns against identifying a particular address or household from its data.
  • Do not make high-stakes decisions from location alone. IP geolocation should not by itself decide identity, fraud, access entitlement, or a user’s physical presence. Use an appropriate independent verification mechanism where those decisions matter.
  • Cache only when justified. Caching can reduce repeat provider calls and latency, but choose a lifetime consistent with database freshness, provider terms, and your retention policy. Do not store raw IPs or detailed location indefinitely by default.
  • Make failure non-catastrophic where possible. Decide whether the feature can degrade to “unknown” if a provider is unavailable. Record operational errors without putting credentials or unnecessary personal data in logs.
  • Keep response contracts tolerant. Providers may omit city, region, or timezone for some IP ranges. Handle absent fields as null and do not assume every successful response contains every location attribute.

One vendor-authored tutorial from ip-api.io publishes accuracy claims of 99.8% for country, 85–95% for city, and an approximately 50 km median coordinate accuracy radius. Those are the vendor’s claims on its tutorial page, not an independently verified or general industry benchmark; the page does not provide an independent methodology in the evidence available here: ip-api.io Python tutorial. Do not apply those figures to another provider or treat them as a guarantee for an individual lookup.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot common failures

  • The result is your proxy or server’s location: Flask is seeing the immediate proxy connection. Verify the proxy overwrites forwarding headers, then configure ProxyFix with the exact trusted hop count. Do not fix this by trusting arbitrary client headers.
  • Every IP is rejected as private or invalid: inspect request.remote_addr in a controlled development environment and verify your WSGI/proxy path. A local container or test client may genuinely use loopback/private addresses; do not send those to a public lookup expecting a visitor location.
  • The provider returns an error or empty fields: check endpoint, requested fields, rate limit, address validity, and provider response status. Treat absent city data as unknown rather than an application crash.
  • The Flask route hangs or fails during provider outage: add finite connect and read timeouts, catch request/JSON errors, and return a controlled degraded response. Consider a background queue for work that need not delay the web request.
  • A lookup works locally but not in production: check outbound network policy, DNS, TLS/HTTP requirements, environment-specific credentials, and whether the provider plan permits production or commercial use.
  • Local lookup raises an address-not-found error: the database may not contain that network block, or the input may be unsuitable. Return “unknown” or a not-found response; do not infer a location.

Or skip the browser setup

ScreenshotNeo is a website screenshot API, not an IP-geolocation provider; use the Flask approaches above for location lookup. If your adjacent task is capturing a page from Python, one GET request can return a screenshot. See the ScreenshotNeo API documentation for options and response handling.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

ScreenshotNeo says it removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed; and its MCP server lets AI agents take screenshots. Its Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Get started with the free ScreenshotNeo sign-up.

Frequently Asked Questions

Can Flask identify someone from an IP geolocation result?

No. A lookup estimates a network location and does not verify a person’s identity, residence, or exact physical position.

Should I use geolocation to enforce a legal or licensing boundary?

IP results can be incomplete or inaccurate. Treat them as one signal only, and obtain legal and product review for any consequential boundary decision.

Does a local GeoIP database eliminate privacy obligations?

No. It changes whether each lookup is sent to an external API; processing the IP and resulting location still requires an appropriate privacy and retention assessment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.