ipinfo.javiervalencia.net: geolocalización de IPs con una API que no pide nada a cambio
Hace unos meses que tengo rodando un pequeño servicio propio en ipinfo.javiervalencia.net. Es una API de geolocalización de IPs: le das una dirección y te devuelve ciudad, región, país, coordenadas, código postal y zona horaria. Sin registro, sin API key, sin formulario, sin cookies. La usas desde la terminal, desde un script, desde un dashboard o desde un navegador.
Este post cuenta qué hace, cómo está construida, y unos cuantos ejemplos prácticos para que te hagas una idea de cuándo puede serte útil.
Por qué existe
Llevo años usando servicios de geolocalización de IPs para cosas muy mundanas:
- Saber desde dónde viene una petición sospechosa en el log de nginx.
- Calcular la zona horaria correcta en un cron que corre en varios servidores repartidos por datacenters.
- Validar en un script de provisioning que la IP pública del servidor está realmente en el país que esperaba.
- Mostrar el país en un pequeño dashboard de métricas.
- Depurar problemas de enrutamiento de CDN.
Durante mucho tiempo tiré de ipinfo.io, ipapi.co o ip-api.com. Todos funcionan, pero todos tienen las mismas pegas: rate limits agresivos para la capa gratuita, obligación de registrarse para cualquier uso serio, o bloqueos cuando se huele que estás haciendo algo automatizado. Y si encima quieres usarlo desde varias máquinas, acabas gestionando tokens.
El caso es que los datos de geolocalización que uso en el 95% de los casos son los de la base gratuita de MaxMind GeoLite2, que se actualiza semanalmente y es más que suficiente para los usos que he listado arriba. Así que en una tarde monté el servicio yo mismo.
Qué hace, en concreto

La API tiene dos endpoints:
| Endpoint | Qué hace |
|---|---|
GET /me |
Devuelve la geolocalización de la IP que hace la petición |
GET /{ip} |
Devuelve la geolocalización de la IP indicada (IPv4 o IPv6) |
Los campos que devuelve son:
ip: la IP consultada.city: ciudad.region: región administrativa (comunidad autónoma en España, estado en EE.UU.).country: nombre del país en inglés.country_code: código ISO 3166-1 alfa-2.postal_code: código postal.latitudeylongitude: coordenadas WGS84.timezone: zona horaria en formato IANA (ej.Europe/Madrid).continent: nombre del continente.
Algunos campos pueden venir vacíos. La precisión a nivel de IP pública es desigual: MaxMind acierta el país casi siempre, la región a menudo, la ciudad con frecuencia y el código postal en ocasiones. Para IPs de servidores de proveedores como Google o Cloudflare los datos son más vagos y suelen apuntar a un punto genérico del país.
Ejemplos prácticos
Lo mejor es verlo con ejemplos. Todos los que siguen son comandos reales que puedes copiar y pegar.
Consultar tu propia IP
curl https://ipinfo.javiervalencia.net/me
Una respuesta típica, si estoy conectado desde casa:
{
"ip": "147.135.214.123",
"city": "Fuengirola",
"region": "Andalucía",
"country": "Spain",
"country_code": "ES",
"postal_code": "29640",
"latitude": 36.5394,
"longitude": -4.6239,
"timezone": "Europe/Madrid",
"continent": "Europe"
}
Consultar una IP concreta
curl https://ipinfo.javiervalencia.net/8.8.8.8
Respuesta real:
{
"ip": "8.8.8.8",
"city": "",
"region": "",
"country": "United States",
"country_code": "US",
"postal_code": "",
"latitude": 37.751,
"longitude": -97.822,
"timezone": "America/Chicago",
"continent": "North America"
}
Los campos vacíos para la DNS de Google son típicos: MaxMind no tiene datos de ciudad para muchas IPs de infraestructura.
IPv6 también funciona
curl https://ipinfo.javiervalencia.net/2001:4860:4860::8888
Devuelve el mismo tipo de respuesta para la versión IPv6 del DNS de Google. No hay diferencia de comportamiento entre v4 y v6, y los formatos de dirección de RFC 4291 se aceptan todos.
Cambiar el formato de salida

Por defecto, la API responde en JSON si detecta un cliente programático (curl, wget, requests...) y en HTML si es un navegador. Pero puedes forzar el formato de tres maneras.
Con la extensión en la URL:
curl https://ipinfo.javiervalencia.net/8.8.8.8.xml
curl https://ipinfo.javiervalencia.net/8.8.8.8.yaml
curl https://ipinfo.javiervalencia.net/8.8.8.8.csv
curl https://ipinfo.javiervalencia.net/8.8.8.8.ini
curl https://ipinfo.javiervalencia.net/8.8.8.8.txt
Con la cabecera Accept:
curl -H "Accept: application/xml" https://ipinfo.javiervalencia.net/8.8.8.8
curl -H "Accept: application/yaml" https://ipinfo.javiervalencia.net/8.8.8.8
curl -H "Accept: text/csv" https://ipinfo.javiervalencia.net/8.8.8.8
La prioridad es: extensión en la URL > cabecera Accept > detección del User-Agent. Es la aplicación práctica de la negociación de contenido de HTTP, definida en RFC 9110.
El formato CSV es útil para tuberías que acaban en una hoja de cálculo o en un awk. El INI, para scripts viejos que no quieren depender de jq. El YAML, para pipelines de Ansible que ingieren configuración. El HTML es lo que ves si abres la URL en el navegador: una tabla formateada con un mapa estático.
Usarlo en scripts
Un caso típico: un script de provisioning que quiere saber en qué país está el servidor antes de seguir.
#!/usr/bin/env bash
set -euo pipefail
COUNTRY=$(curl -fsS https://ipinfo.javiervalencia.net/me | jq -r '.country_code')
if [[ "$COUNTRY" != "ES" ]]; then
echo "Error: este script espera un servidor en España, detectado $COUNTRY" >&2
exit 1
fi
echo "Servidor en España, continuando..."
El jq -r '.country_code' extrae el código de país del JSON. Con -fsS, curl falla si el HTTP status no es 2xx, se queda callado en éxito y muestra errores en stderr. Es el combo que uso siempre en scripts.
Configurar la zona horaria desde la IP pública
TZ=$(curl -fsS https://ipinfo.javiervalencia.net/me | jq -r '.timezone')
echo "$TZ" | sudo tee /etc/timezone > /dev/null
sudo timedatectl set-timezone "$TZ"
Esto configura la zona horaria del servidor a partir de su IP pública. Útil en provisioning automatizado cuando no sabes a priori en qué datacenter va a acabar la máquina: un droplet de Digital Ocean que aparece en Frankfurt, un VPS de OVH que aparece en Estrasburgo, una instancia de Hetzner en Helsinki.
Mostrar la ubicación en el prompt de la shell
Uno que uso en el .bashrc de máquinas remotas para saber dónde demonios estoy cuando abro una sesión SSH:
server_loc() {
curl -fsS https://ipinfo.javiervalencia.net/me 2>/dev/null \
| jq -r '.city + ", " + .country_code'
}
PS1="[$(server_loc)] \u@\h:\w\$ "
No es rápido (añade una petición HTTP al abrir la shell), pero es útil la primera vez que entras a una máquina desconocida y no te acuerdas de en qué zona horaria vive.
Desde Python
import urllib.request
import json
with urllib.request.urlopen("https://ipinfo.javiervalencia.net/me") as r:
data = json.loads(r.read())
print(f"Conectado desde {data['city']}, {data['country']}")
Sin dependencias externas: urllib y json vienen con Python 3.x estándar.
Desde Go
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Info struct {
IP string `json:"ip"`
City string `json:"city"`
Country string `json:"country"`
}
func main() {
resp, err := http.Get("https://ipinfo.javiervalencia.net/me")
if err != nil {
panic(err)
}
defer resp.Body.Close()
var info Info
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
panic(err)
}
fmt.Printf("%s desde %s (%s)\n", info.IP, info.City, info.Country)
}
También sin dependencias: net/http y encoding/json son stdlib.
Desde un pipeline de nginx y awk
Si tienes un access.log con IPs y quieres sacar los países de los últimos 100 visitantes únicos:
awk '{print $1}' /var/log/nginx/access.log \
| sort -u \
| tail -n 100 \
| while read ip; do
curl -fsS "https://ipinfo.javiervalencia.net/$ip" \
| jq -r "[\"$ip\", .country_code] | @csv"
done > visitors.csv
El rate limit te permite unas 30 peticiones por minuto, así que para lotes grandes conviene espaciar o usar xargs -P con cuidado.
Geofencing sencillo en un script
Imagina que tienes un backup nocturno que solo debe ejecutarse si el servidor sigue estando físicamente en el continente que esperas. Un script de paranoia contra movidas raras:
#!/usr/bin/env bash
set -euo pipefail
EXPECTED_CONTINENT="Europe"
CONTINENT=$(curl -fsS https://ipinfo.javiervalencia.net/me | jq -r '.continent')
if [[ "$CONTINENT" != "$EXPECTED_CONTINENT" ]]; then
echo "Aborto: servidor detectado en $CONTINENT, esperaba $EXPECTED_CONTINENT" >&2
logger -t backup "anomaly: continent $CONTINENT"
exit 1
fi
rsync -a --delete /data/ backup@remote:/backups/
No es una medida de seguridad seria (la geolocalización por IP es manipulable con VPN), pero sí un detector barato de "alguien ha redirigido mi DNS o me ha movido el servidor sin avisarme".
Convertir IPs a CSV para una hoja de cálculo
for ip in 1.1.1.1 8.8.8.8 9.9.9.9 208.67.222.222; do
curl -fsS "https://ipinfo.javiervalencia.net/$ip.csv" | tail -n +2
done > resolvers.csv
El tail -n +2 salta la línea de cabeceras CSV en todas menos la primera, dejándote un CSV limpio listo para pegar en LibreOffice Calc o en Google Sheets.
Validar una lista de IPs sospechosas
Si tienes un fichero sospechosas.txt con una IP por línea y quieres agruparlas por país:
while read ip; do
cc=$(curl -fsS "https://ipinfo.javiervalencia.net/$ip" 2>/dev/null | jq -r '.country_code // "?"')
echo "$cc $ip"
done < sospechosas.txt | sort | uniq -c | sort -rn
Sale un histograma por país de tus atacantes. Un patrón que he visto demasiadas veces es que el 80% de las IPs agresivas de un día concreto vienen de dos países; con ese dato, un bloqueo temporal por país en el firewall resuelve el día.
Cómo está construida por dentro

El servicio es un único binario en Go, compilado estáticamente, que corre detrás de nginx. No tiene base de datos, no tiene cache externa, no depende de ningún otro proceso. Las piezas principales:
-
Base de datos GeoLite2-City de MaxMind: un fichero
.mmdbde unos 70 MB que se descarga una vez a la semana vía cron. Es la fuente de los datos. MaxMind lo ofrece gratis con un registro mínimo, y su licencia permite usarlo incluso comercialmente. Al arrancar el servicio, el fichero semmapea en memoria para que las consultas sean de microsegundos. -
Librería
oschwald/geoip2-golang: wrapper en Go del formato MaxMind DB. Resuelve IPs a registros en tiempo constante usando el árbol binario propio de MaxMind. Maneja IPv4 e IPv6 con el mismo método. -
net/httpde la stdlib de Go: el router y el servidor HTTP. Desde Go 1.22 el mux de la stdlib soporta path variables (/{ip}) y verbos en el patrón (GET /me), con lo cual no necesitochi,gin,echoni ninguna otra librería de routing externa. -
Negociación de contenido manual: un middleware pequeño que mira la extensión de la URL, la cabecera
Accepty el User-Agent (para detectar navegadores), y elige el renderer adecuado. Cada formato (JSON, XML, YAML, CSV, INI, HTML, texto plano) tiene su función que escribe en elhttp.ResponseWriter. El XML sale conencoding/xmlde la stdlib, el YAML congopkg.in/yaml.v3, el CSV conencoding/csv. Solo una dependencia externa. -
Rate limiting basado en IP: un
sync.Mapcon un contador por IP que se reinicia por ventana temporal deslizante. Devuelve429 Too Many Requestscuando superas 30 peticiones por minuto. Esto basta para parar scrapers tontos sin molestar a nadie que use el servicio de forma razonable. -
Zona horaria: la librería
timede Go ya entiende el formato IANA, así que devolverEurope/Madridcomo string es suficiente. Si alguna vez lo necesito localmente,time.LoadLocation(data.Timezone)me lo da en un segundo.
Todo esto cabe en menos de 500 líneas de código Go, incluyendo los tests. Un proyecto intencionadamente pequeño.
Privacidad
El servicio no guarda logs de consultas. Nginx está configurado para no loguear las peticiones a estos endpoints, y el servicio Go tampoco persiste nada. Lo único que se registra son métricas agregadas: número total de peticiones por hora, número de rate limits aplicados. Nada que pueda atar una IP concreta a una consulta concreta.
Tampoco usa cookies ni JavaScript de tracking. La versión HTML para navegadores carga un mapa estático generado con OpenStreetMap sin embebidos de terceros.
Esto es deliberado: una de las razones por las que monté esto es que no me gustaba la política de datos de los servicios equivalentes comerciales.
Cuándo no usarlo
Un par de advertencias honestas:
-
La GeoLite2 no es tan precisa como la versión comercial. Si tu negocio depende de saber con precisión en qué ciudad está cada cliente (por ejemplo, para fraud detection serio), contrata la base completa de MaxMind o una de sus competidoras. Para el 95% de usos técnicos, la gratuita es suficiente.
-
Es un servicio personal, no tiene SLA. Lo aloja un servidor mío. Si se cae, se cae. No pongas tu facturación por encima.
-
No hace reverse DNS ni ASN lookup. Solo geolocalización. Si necesitas saber el AS de una IP, mira Team Cymru IP-to-ASN o la API de ip-api.com.
-
No intenta detectar VPN, proxy o Tor. Eso se haría con una base distinta (IP2Location PX, MaxMind Anonymous IP). Aquí no aplica.
Troubleshooting común
Algunos tropiezos que me han reportado:
-
Devuelve el país pero no la ciudad. Normal para IPs de infraestructura (Google, Cloudflare, AWS), VPNs comerciales y pools de operadoras móviles. MaxMind prioriza la precisión: si no está segura, deja el campo vacío antes que adivinar.
-
"Mi IP sale mal por 200 km". La GeoLite2 tiene un error típico de 30-60 km en España, mayor en zonas rurales. Es una base agregada, no un GPS. Para fraud detection que dependa de bloques pequeños, no la uses.
-
IPv6 desde móvil sale en otro país. Algunas operadoras asignan prefijos IPv6 globalmente que MaxMind etiqueta en la sede central del operador, no en donde estás tú. Aquí no hay solución: es un problema de la asignación, no del servicio.
-
Recibo
429constantemente. Estás superando las 30 peticiones por minuto desde una misma IP. Espera un minuto y espacia las peticiones consleep 2o usaxargs -P1 -I{} curl ...con una pausa controlada. -
curl: (6) Could not resolve host. Si tu red tiene DNS capado, el nombreipinfo.javiervalencia.netno resuelve. Puedes probar por IP directa concurl --resolve ipinfo.javiervalencia.net:443:147.135.214.123 https://ipinfo.javiervalencia.net/me.
Límites y consideraciones
- 30 peticiones por minuto por IP. Suficiente para un humano o un script; insuficiente para scraping masivo.
- Tamaño de respuesta: unos 200 bytes en JSON. Ignorable en cualquier red moderna.
- Latencia típica: 5-15 ms desde Europa. El lookup en la GeoLite2 es inferior a 1 ms; el resto es red.
- Soporta HTTP/2 y HTTP/3 vía nginx.
- Sin CORS. El servicio responde con
Access-Control-Allow-Origin: *, así que puedes llamarlo desde unfetch()en cualquier frontend sin montar un proxy. - Sin autenticación. Cualquiera puede consultar cualquier IP pública. Las IPs privadas (
10.0.0.0/8,192.168.0.0/16,172.16.0.0/12,127.0.0.0/8) devuelven422 Unprocessable Entitycon un mensaje explicando que no tienen geolocalización posible. - Siempre HTTPS. El servidor redirige
80a443y fuerza HSTS, así que si usashttp://acabas igualmente en HTTPS con una ida y vuelta extra. - Versiones estables de la base. Me guardo en cold storage la base MaxMind de cada semana durante un año, por si alguna consulta histórica necesita reproducirse con los datos vigentes en una fecha concreta. No está expuesto en el API, pero si alguna vez lo necesito, el dato está.
Conclusión
Si necesitas geolocalizar IPs de forma ocasional, sin registrar nada ni gestionar tokens, y te basta con la precisión de MaxMind GeoLite2, ipinfo.javiervalencia.net es una herramienta que funciona. Si necesitas algo más serio (mayor precisión, SLA, detección de VPN), ve directo al proveedor comercial correspondiente.
El código es corto por diseño. El servicio va a seguir ahí mientras lo siga usando yo, que es básicamente lo único que puedo prometer. Si te es útil, úsalo. Si rompes el rate limit, respira hondo y monta uno tú: la receta está en este post.