Endpoints
| Endpoint | Content type | Returns |
|---|---|---|
/ip |
text/plain |
The address plus \n, nothing else |
/json |
application/json |
The full record. ?full=1 adds hostname and privacy flags |
/ip.md |
text/markdown |
A short Markdown block for agents and LLM tooling |
/ |
negotiated | Plain text for curl, wget, HTTPie, PowerShell, python-requests, and Go-http-client user agents. HTML for browsers |
All four send Access-Control-Allow-Origin: *. That is why curl whatsmyip.fyi with no path works in a shell and still renders a page in a browser.
Bash
curl -s https://whatsmyip.fyi/ip
With the flags that belong in anything unattended:
ip=$(curl -fsS --max-time 5 --retry 2 --retry-delay 1 https://whatsmyip.fyi/ip) || {
echo "lookup failed" >&2
exit 1
}
echo "$ip"
-f makes curl exit non-zero on a 4xx or 5xx instead of printing the error body into your variable. -sS hides the progress meter but keeps real errors. --max-time bounds the whole call so a hung connection cannot stall a cron job.
Pin the address family:
curl -4 -s https://v4.whatsmyip.fyi/ip
curl -6 -s https://v6.whatsmyip.fyi/ip
Structured output with jq:
curl -s https://whatsmyip.fyi/json | jq -r '.ip, .as_name, .country'
Python
Standard library only, no dependency:
import urllib.request
def public_ip(timeout=5):
req = urllib.request.Request(
"https://whatsmyip.fyi/ip",
headers={"User-Agent": "my-tool/1.0"},
)
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.read().decode().strip()
With requests, and the JSON record:
import requests
r = requests.get("https://whatsmyip.fyi/json", timeout=5)
r.raise_for_status()
data = r.json()
print(data["ip"], data["as_name"], data["country"])
Always pass timeout. requests has no default, so a call without it can block forever.
Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
"time"
)
func publicIP() (string, error) {
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://whatsmyip.fyi/ip")
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("status %d", resp.StatusCode)
}
b, err := io.ReadAll(io.LimitReader(resp.Body, 64))
if err != nil {
return "", err
}
return strings.TrimSpace(string(b)), nil
}
http.DefaultClient has no timeout. Construct a client with one, and bound the read: the response is at most 45 bytes, so a limit reader costs nothing and removes a class of failure.
Node
const res = await fetch("https://whatsmyip.fyi/json", {
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`status ${res.status}`);
const { ip, as_name, country } = await res.json();
console.log(ip, as_name, country);
fetch is built in from Node 18. AbortSignal.timeout is available from Node 17.3.
PowerShell
$ip = Invoke-RestMethod -Uri https://whatsmyip.fyi/ip -TimeoutSec 5
$ip.Trim()
The JSON record deserialises into an object:
$r = Invoke-RestMethod -Uri https://whatsmyip.fyi/json -TimeoutSec 5
"{0} on {1} (AS{2})" -f $r.ip, $r.isp, $r.asn
In Windows PowerShell, curl is an alias for Invoke-WebRequest and will reject curl’s flags. Call curl.exe if you want the real binary, which ships with Windows 10 build 1803 and later.
The JSON record
{
"ip": "203.0.113.45",
"version": 4,
"city": "Tallinn",
"region": "Harju",
"country": "EE",
"country_name": "Estonia",
"loc": "59.4370,24.7536",
"timezone": "Europe/Tallinn",
"isp": "Telia Eesti",
"asn": 3249,
"as_name": "Telia Eesti AS",
"rir": "RIPE",
"connection_type": "residential",
"geo_accuracy": "approximate",
"checked_at": "2026-09-04T14:02:11Z"
}
| Field | Type | Notes |
|---|---|---|
ip |
string | The address the request arrived on |
version |
number | 4 or 6 |
hostname |
string or null | Reverse DNS. Null unless ?full=1 |
city, region, country, country_name |
string | Country is an ISO 3166-1 alpha-2 code |
loc |
string | lat,lon, the centroid of an area, never a street |
isp, asn, as_name, as_domain |
string / number | Routing origin for the prefix |
rir |
string | ARIN, RIPE, APNIC, LACNIC, or AFRINIC |
connection_type |
string | residential, business, mobile, or hosting |
privacy |
object or null | VPN, proxy, Tor, hosting, relay flags. Null unless ?full=1 |
network |
object | Protocol, TLS version, edge RTT |
geo_accuracy |
string | Always approximate or country-only |
checked_at |
string | RFC 3339 timestamp |
Treat city and loc as a hint. They come from registration records and routing inference, not from the device, and they are frequently wrong at suburb level. The what an IP reveals guide covers why.
Rate limits and behaving well
Keyless, per source IP: 1,000 requests a day and 60 a minute for /ip, /json, /ip.md, and /api/v1/ip. Lookup-class endpoints such as /api/v1/lookup allow 200 a day. Exceeding either returns 429 with a Retry-After header in seconds.
Rules that keep a script inside the budget:
- Cache the result. Your address changes when your lease renews or you switch networks, not every second. Poll once at start-up and again every few minutes at most.
- Honour
Retry-After. Sleep for the value given. Do not tighten the loop on a 429. - Back off exponentially on 5xx and on connection errors, with jitter. Two or three attempts is enough for a health check.
- Send a real User-Agent identifying your tool. It makes abuse investigation land on the right party, which is you if something is wrong.
- Never put the call in a hot path. A per-request lookup in a web handler turns one outage here into an outage in your service.
Do not hardcode the result
The address you get today belongs to a DHCP lease from your ISP, and most residential leases change on reconnection, on router reboot, or after a maintenance window. Baking it into a firewall rule, an allowlist, or a config file produces a failure that surfaces weeks later, at the worst moment, with no obvious cause.
Resolve it at run time, cache it in memory with a short expiry, and fail closed with a clear error rather than falling back to a stale value. If a remote system genuinely needs a fixed source address, ask your ISP for a static allocation or route through a host that has one. If your WAN address is in 100.64.0.0/10, you are behind carrier-grade NAT under RFC 6598 and the address is shared with other subscribers, so an allowlist entry for it grants access to strangers as well.
Full endpoint documentation, the OpenAPI schema, and the changelog live on /api. More one-liners are collected in the curl cookbook.
Questions people ask
- What is the rate limit without an API key?
- 1,000 requests per day and 60 per minute per source IP for /ip, /json, /ip.md, and /api/v1/ip. Lookup-class endpoints allow 200 per day. Over the limit you get HTTP 429 with a Retry-After header.
- How do I force IPv4 or IPv6?
- Use the single-family hostnames. v4.whatsmyip.fyi publishes A records only and v6.whatsmyip.fyi publishes AAAA records only, so curl -4 and curl -6 against them return a definite answer instead of whatever the resolver preferred.
- Can I call the endpoint from browser JavaScript?
- Yes. /ip, /json, /ip.md, and /api/v1/* send Access-Control-Allow-Origin: *, so fetch from a page works without a proxy.
- Why does my script get a different IP than my browser?
- Usually because the browser is using a proxy the script is not, such as iCloud Private Relay or a browser-level VPN extension. Protocol choice matters too: your browser may prefer IPv6 while your HTTP client falls back to IPv4.
Related
Last reviewed 2026-09-04. Reviewed quarterly, or sooner when a vendor changes something.