Documentation
echo shows you exactly what the internet sees when you connect: your IP address, location, ISP and more. This page covers general usage, the public API, and running it yourself.
Introduction
echo is a server-side IP and geo lookup tool. When you open the site it detects your public IP, looks it up against a bundled geo database, and shows location, country, coordinates, ISP/ASN, timezone and hostname. You can also look up any IP address, resolve DNS records, and query WHOIS registration data — all without a client-side geo call.
What echo does
- Shows your public IP and geo information from a bundled database
- Looks up any IP via
?ip=on the home page or the API - Resolves forward DNS records (A, AAAA, CNAME, MX, NS, TXT, SOA, SRV, CAA)
- Queries WHOIS/RDAP registration and ASN data on demand
- Runs an optional IPv4/IPv6 connectivity diagnostic from your browser
- Logs aggregate lookup statistics — never raw visitor IPs publicly
Lookup any IP
Add ?ip=8.8.8.8 to the home page URL to look up any public IP:
https://echo.johansen.foo/?ip=8.8.8.8Both IPv4 and IPv6 are supported. The page shows the lookup result instead of your own details and the copy and share actions work for the looked-up address.
Share links
Use the copy link button to get a shareable ?ip= URL. It uses the configured APP_URL when set, otherwise the address you are viewing from. Lookup pages set noindex metadata so search engines do not index query-specific URLs.
Map and copy
The coordinates card opens a Leaflet map centred on the approximate city-level location. Copy and copy-as-JSON buttons put the IP or the full lookup payload on your clipboard. Clipboard failures are reported honestly.
Connectivity diagnostics
When CONNECTIVITY_IPV4_URL or CONNECTIVITY_IPV6_URL are configured, a connectivity section lets your browser probe each endpoint and report IPv4/IPv6 reachability with latency. This measures browser reachability only — it never changes the IP recorded by the server. The section is hidden when no probe URL is configured.
Privacy model
The public site never exposes raw visitor IPs. Recent lookups are shown as aggregate totals and top countries. Exact IPs are visible only in the token-protected admin dashboard. Lookup rows are pruned after LOOKUP_RETENTION_DAYS (default 90 days). No cookies are used except the optional admin session cookie.
Overview
All public endpoints are GET. JSON endpoints return Access-Control-Allow-Origin: * and are rate limited per visitor IP — /api/ip returns plain text without CORS, and /api/stats and /api/health omit CORS (health is unthrottled). The visitor IP is read from X-Real-IP first, then X-Forwarded-For; your reverse proxy must overwrite these headers with the verified client address.
JSON errors use a stable shape:
{ "error": "human readable message", "code": "stable_code" }Rate limiting and errors
Each public endpoint has its own fixed-window budget (defaults below). A response carries x-ratelimit-limit and x-ratelimit-remaining. When a budget is exhausted the API returns HTTP 429 with a retry-after header in seconds.
| Endpoint | Default budget (per 60s) |
|---|---|
| /api/ip | 60 |
| /api/json | 30 |
| /api/history | 30 |
| /api/whois | 10 |
| /api/dns | 10 |
GET /api/ip
Returns the visitor IP as plain text, or a specific IP when ?ip= is provided.
curl https://echo.johansen.foo/api/ip203.0.113.7curl "https://echo.johansen.foo/api/ip?ip=8.8.8.8"8.8.8.8GET /api/json
Returns the full normalized lookup payload for the visitor or for ?ip=.
curl "https://echo.johansen.foo/api/json?ip=8.8.8.8"{
"ip": "8.8.8.8",
"city": "Mountain View",
"region": "California",
"country": "US",
"countryName": "United States",
"flag": "🇺🇸",
"org": "Google LLC",
"asn": "AS15169",
"timezone": "America/Los_Angeles",
"utcOffset": "-07:00",
"latitude": 37.422,
"longitude": -122.085,
"hostname": null,
"isPrivate": false
}GET /api/whois
Returns WHOIS/RDAP registration and ASN data for ?ip= (on demand, cached).
curl "https://echo.johansen.foo/api/whois?ip=8.8.8.8"{
"ip": {
"handle": "NET-8-8-8-0-2",
"name": "GOGL",
"startAddress": "8.8.8.0",
"endAddress": "8.8.8.255",
"country": "US",
"cidr": "8.8.8.0/24",
"organization": "Google LLC",
"registrant": "Google LLC",
"abuse": null
},
"asn": {
"handle": "AS15169",
"name": "GOOGLE",
"country": "US",
"organization": "Google LLC"
}
}GET /api/dns
Resolves A, AAAA, CNAME, MX, NS, TXT, SOA, SRV and CAA records for ?name= with cache metadata.
curl "https://echo.johansen.foo/api/dns?name=example.com"{
"name": "example.com",
"records": {
"a": ["93.184.216.34"],
"aaaa": [],
"mx": [],
"ns": ["a.iana-servers.net"],
"txt": [],
"soa": ["a.iana-servers.net hostmaster.iana.org"]
},
"cache": "miss",
"resolvedAt": "2026-08-20T10:00:00.000Z",
"durationMs": 42,
"partial": false
}GET /api/history
Returns aggregate lookup statistics: totals and top countries, never raw IPs.
curl https://echo.johansen.foo/api/history{
"total": 1234,
"last24h": 56,
"topCountries": [
{ "iso": "US", "count": 42 },
{ "iso": "DE", "count": 7 }
]
}GET /api/stats
Private owner analytics guarded by STATS_TOKEN. Pass ?token= or an Authorization: Bearer header.
curl -H "Authorization: Bearer $STATS_TOKEN" https://echo.johansen.foo/api/stats{
"total": 1234,
"last24h": 56,
"topCountries": [],
"topIps": [{ "ip": "203.0.113.7", "count": 12 }],
"daily": [{ "day": "2026-08-20", "count": 56 }]
}GET /api/health
Public liveness check returning {status:ok}. Readiness detail requires the HEALTH_TOKEN.
curl https://echo.johansen.foo/api/health{ "status": "ok" }Environment variables
| Variable | Default | Description |
|---|---|---|
| APP_URL | https://echo.johansen.foo | Public origin used for metadata, sitemap and share links |
| TZ | Europe/Copenhagen | Container timezone for logs and admin timestamps |
| RATE_LIMIT_MAX | 30 | Legacy global fallback for request budget per window |
| RATE_LIMIT_WINDOW_MS | 60000 | Legacy global fallback window |
| RATE_LIMIT_<ENDPOINT>_MAX | ip 60, json 30, history 30, whois 10, dns 10, stats-auth 5 | Per-endpoint budget |
| RATE_LIMIT_<ENDPOINT>_WINDOW_MS | 60000 | Per-endpoint window |
| DNS_TIMEOUT_MS | 6000 | Overall DNS resolution deadline |
| DNS_MAX_CONCURRENCY | 2 | Concurrent resolver jobs |
| DNS_CACHE_TTL_MS | 30000 | Successful DNS cache lifetime |
| DNS_FAILURE_TTL_MS | 5000 | Failed DNS cache lifetime |
| DNS_CACHE_MAX | 100 | Maximum cached hostnames |
| LOOKUP_RETENTION_DAYS | 90 | How long lookup rows are kept |
| STATS_TOKEN | (unset) | Secret protecting /api/stats; endpoint disabled when unset |
| HEALTH_TOKEN | (unset) | Secret protecting authenticated readiness detail |
| ADMIN_TOKEN | (unset) | Secret enabling the /admin dashboard; hidden when unset |
| ADMIN_SESSION_TTL_SECONDS | 28800 | Admin session lifetime in seconds |
| CONNECTIVITY_IPV4_URL | (unset) | IPv4 probe endpoint used by the browser diagnostic |
| CONNECTIVITY_IPV6_URL | (unset) | IPv6 probe endpoint used by the browser diagnostic |
| ECHO_IMAGE_SIZE_BYTES | (unset) | Optional deployed image size shown in admin resources |
| UMAMI_SCRIPT_URL | https://umami.johansen.foo/script.js | Umami script URL; analytics only when set |
| UMAMI_WEBSITE_ID | (unset) | Umami website id; analytics only when set |
| CARTO_BASEMAP_KEY | (unset) | API key for CARTO basemap tiles; avoids the "API key required" watermark |
Deployment
Images are published to GitHub Container Registry. Copy docker-compose.yml to your host, configure a .env next to it, then:
docker compose pull
docker compose up -dThe service listens on port 3100. Keep the host firewall closed to direct access if a reverse proxy is the only intended entry point; TLS and HSTS are owned by the external TLS proxy. Data persists in the echo-data volume.
Nginx Proxy Manager
NPM 2.x Proxy Hosts automatically overwrite X-Real-IP, X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, so a single-hop setup needs no manual configuration. echo trusts X-Real-IP first.
With Cloudflare in front, open the Advanced tab and add: set_real_ip_from for each Cloudflare range, then real_ip_header CF-Connecting-IP;. For an intermediate proxy, use set_real_ip_from with that proxy's range and real_ip_header X-Forwarded-For; with real_ip_recursive on;.
Connectivity probes
The connectivity diagnostic needs two lightweight endpoints reachable from browsers: one over IPv4 only and one over IPv6 only. Configure CONNECTIVITY_IPV4_URL and CONNECTIVITY_IPV6_URL with those URLs. The probe responses should be CORS-enabled and return a minimal payload; they are never used for lookups or logged as visitor activity.
Private admin dashboard
Set ADMIN_TOKEN to enable /admin. The dashboard shows exact visitor activity (type, channel, actor, country, IP, target), aggregate breakdowns, and resource sampling (CPU, memory, storage). Sessions are signed HttpOnly cookies; logout revokes the session server-side. When ADMIN_TOKEN is unset the whole area returns 404.
Releasing
Releases use npm run release:patch, npm run release:minor or npm run release:major. The script bumps the version, moves the Unreleased changelog into a dated section, commits and tags. Pushing the tag triggers CI to build and publish the image to GitHub Container Registry.
Data and attribution
Geo data comes from db-ip and is licensed under CC BY 4.0. Attribution is shown in the site footer.