The IP Narwhal API
Your public address as JSON or plain text. No key, no signup, no logging, and one useful trick most IP APIs cannot do.
The short version
$ curl https://api4.ipnarwhal.com/iptool/ip.txt 203.0.113.42
That is the whole thing for most uses. There is no key to request, no account, and no rate limit you are likely to notice. We log nothing: your address is read from the request, put in the response, and forgotten.
Two hostnames, and why that matters
This is the part worth reading, because it is the thing most IP APIs get wrong by accident.
| Host | DNS | Answers over |
|---|---|---|
api4.ipnarwhal.com | A record only | IPv4 only |
api6.ipnarwhal.com | AAAA record only | IPv6 only |
A host with a single-family DNS record cannot be reached over the other family. So api4 is guaranteed to report your IPv4 address and api6 is guaranteed to report your IPv6 address, without depending on your client honoring a -4 or -6 flag and without any chance of the answer quietly coming back over the wrong stack.
An API on a dual-stacked hostname cannot promise that. It reports whichever family your client happened to prefer that second, which is usually IPv6 when it is available, and you have no way to tell from the response which one you got.
If api6 does not resolve or does not connect, that is the finding: this connection has no working IPv6. It is not a fault to retry. Our own front page uses exactly this to decide whether to print an address or "Not detected", and it gives up after five seconds.
Endpoints
| Path | Returns | Content type |
|---|---|---|
/iptool/ip | JSON, address plus approximate location | application/json |
/iptool/ip.txt | The bare address and a newline | text/plain |
Both exist on both hostnames. Only GET is supported.
The JSON response
$ curl -s https://api4.ipnarwhal.com/iptool/ip
{
"ip": "203.0.113.42",
"version": "IPv4",
"geo": {
"country_code": "US",
"country": "United States",
"region": "Pennsylvania",
"city": "Easton",
"postal_code": "18045",
"timezone": "America/New_York",
"latitude": "40.69410",
"longitude": "-75.29040",
"asn": "6079"
}
}
| Field | Notes |
|---|---|
ip | The address we saw the request come from |
version | IPv4 or IPv6 |
geo.country_code | Two letter code. The most reliable field here by a wide margin |
geo.region, geo.city, geo.postal_code | Approximate. See the accuracy note below |
geo.latitude, geo.longitude | Strings, not numbers. The center of a coverage area, never a street |
geo.asn | The autonomous system holding the address, usually your provider |
Any geo field can be absent when the edge did not supply it, so read defensively rather than assuming every key is present.
Examples
A shell variable
MY_IP=$(curl -s https://api4.ipnarwhal.com/iptool/ip.txt)
Both families at once
v4=$(curl -s --max-time 5 https://api4.ipnarwhal.com/iptool/ip.txt)
v6=$(curl -s --max-time 5 https://api6.ipnarwhal.com/iptool/ip.txt)
echo "v4=${v4:-none} v6=${v6:-none}"
Just the country
curl -s https://api4.ipnarwhal.com/iptool/ip | jq -r .geo.country_code
Python
import urllib.request, json
with urllib.request.urlopen("https://api4.ipnarwhal.com/iptool/ip") as r:
data = json.load(r)
print(data["ip"], data["geo"].get("country_code"))
Limits, stated plainly
- No browser access from other sites. Cross-origin requests are allowed only from our own pages, so this is for scripts, shells and servers rather than for JavaScript running on your domain. Call it from your backend and pass the result down.
- No SLA. It is a small free service on a small budget. It is reliable in practice and you should not build something load bearing on it without a fallback.
- Be reasonable. There is no published rate limit and no key to revoke. Please do not make one necessary.
- Location is a guess. It comes from the CloudFront edge that served your request, and nothing in an address encodes a place. Country is dependable, city often is not. Why that is.
- We store nothing. No logs of your address, no analytics on these endpoints, no cookies. See the privacy policy.