For developers

Get your public IP from a script

GET https://whatsmyip.fyi/ip returns your public IP as plain text with a trailing newline and no authentication. Use https://whatsmyip.fyi/json for the structured record with ISP, ASN, and approximate location. Force a family with v4.whatsmyip.fyi or v6.whatsmyip.fyi. The keyless limit is 1,000 requests a day and 60 a minute.

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.

Last reviewed 2026-09-04. Reviewed quarterly, or sooner when a vendor changes something.