Skip to main content
ipconfig.is
Your IPIPv4Columbus, OhioAS16509 Amazon.com, Inc.Show my IP
Speed test
Contents
Developer API

The ipconfig.is API

Your IP address, the location and reputation of any address, and a set of network checks, all from one host. Plain text for terminals, JSON for code, CORS for browser apps. No key, no sign-up.

Base URL
https://ipconfig.is
Authentication
None
Formats
Plain text, JSON
CORS
Any origin
Rate limit
40 requests / 20 s

Reading from a terminal? curl ipconfig.is/api prints this reference as text, and llms.txt has it for language models.

bash - ipconfig.is
$curl ipconfig.is
216.73.216.241 # you
$curl ipconfig.is/country/8.8.8.8
United States
$curl -s ipconfig.is/json/1.1.1.1 | jq -r .asn
AS13335 Cloudflare, Inc.
$curl "ipconfig.is/dns/github.com/MX?short=1"
0 github-com.mail.protection.outlook.com

Getting started

Responses and formats

Plain text or JSON
/ and the single-field paths answer in plain text: one value and a newline. Everything else answers in JSON, pretty-printed UTF-8, where a field with no value is null rather than missing.
Browsers get the website
A request that asks for text/html gets the page; curl, wget, HTTPie, PowerShell and code (fetch, requests, Go) get the data. For JSON at /, send Accept: application/json.
CORS
Every response allows any origin, so a browser app can call the API directly. The one exception is /ports: it scans whoever calls it.
Never cached
Responses carry Cache-Control: no-store, so every answer is fresh.
Forgiving input
Pasted URLs and e-mail addresses are reduced to their host, /tls takes host:port, and path shortcuts such as /dns/github.com/MX work too.
IPv4 and IPv6
The API answers on both. curl -4 and -6 choose; v4.ipconfig.is only speaks IPv4.
Response headersGET /json/8.8.8.8
HTTP/2 200
content-type: application/json; charset=utf-8
cache-control: no-store, no-cache, must-revalidate
access-control-allow-origin: *
ratelimit-limit: 40
ratelimit-remaining: 39
ratelimit-reset: 20

Rate limits

40 requests every 20 seconds for each IP address (each /64 for IPv6), across every endpoint.

  • Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, the seconds until the window resets.
  • Over the limit you get 429 with Retry-After, and remaining in the JSON body. Plain-text endpoints answer with a one-line message instead.
  • The first block lasts 30 seconds and each repeat adds 10 more, until five quiet minutes clear the slate.
  • In scripts, curl --retry 3 --retry-all-errors waits as long as Retry-After asks.
Response429 · application/json
HTTP/2 429
retry-after: 30

{
  "error": "rate_limited",
  "code": "rate_limited",
  "remaining": 30
}

Errors

JSON endpoints answer an error with a readable error message and a stable code to branch on; plain-text endpoints print the message. The status says what kind of problem it is.

Request
curl ipconfig.is/json/10.0.0.1
const res = await fetch('https://ipconfig.is/json/10.0.0.1');
if (!res.ok) {
  const { code, error } = await res.json();
}
import requests

res = requests.get("https://ipconfig.is/json/10.0.0.1")
if not res.ok:
    print(res.json()["code"])
Response400 · application/json · example
{
  "error": "Not a public, routable IP address (private, reserved, special-use or multicast)",
  "code": "non_routable"
}
StatusCodeMeaning
400invalid_ip
non_routable
A malformed IP, or private, reserved, special-use or multicast space
400invalid_domain
unsupported_type
unsupported_port
invalid_url
missing_host
missing_url
Bad tool input; the response lists what is supported
403cross_site
tor_exit
/ports started by a page on another website, or called from a Tor exit
404unknown_endpoint
nxdomain
not_registered
No such endpoint, the domain does not exist, or it is not registered
422ipv6_unsupported/ports over IPv6: use curl -4, or v4.ipconfig.is
429rate_limitedToo many requests: wait for Retry-After (also the JSON remaining field)
502upstream
unreachable
The remote host or registry failed (TLS handshake, timeout, registry down); the message says why
503unavailable
resolver_unavailable
Our location data or resolvers are briefly unavailable

Your connection

Your IP address

GET/

The address you connect from, as plain text with a trailing newline, so $(curl -s ipconfig.is) just works in a script.

  • curl -4 and curl -6 pick the address family. v4.ipconfig.is always answers over IPv4, for tools that cannot choose.
  • Send Accept: application/json to get your full record from the same URL.
Request
curl ipconfig.is
const res = await fetch('https://ipconfig.is');
const ip = (await res.text()).trim();
import requests

ip = requests.get("https://ipconfig.is").text.strip()
Response200 · text/plain · example
81.2.69.142

Your full record

GET/json

Everything we know about your address in one JSON object: location, network, flags and reverse DNS. Fields without a value are null, never missing.

  • Your own record also carries the site's visitor counters, visitor_total and visitor_country. Calling the API never counts as a visit.
Request
curl ipconfig.is/json
const res = await fetch('https://ipconfig.is/json');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/json").json()
Response200 · application/json · example
{
  "ip": "81.2.69.142",
  "city": "Ipswich",
  "district": "Suffolk",
  "region": "England",
  "region_code": "ENG",
  "country_name": "United Kingdom",
  "country_code": "GB",
  "registered_country_code": "GB",
  "continent": "Europe",
  "continent_code": "EU",
  "zip": "IP4",
  "latitude": 52.0618,
  "longitude": 1.156,
  "accuracy_radius": 200,
  "timezone": "Europe/London",
  "offset": 3600,
  "currency": "GBP",
  "org": "Andrews & Arnold Ltd",
  "asn": "AS20712 Andrews & Arnold Ltd",
  "asn_number": 20712,
  "network": "81.2.68.0/22",
  "is_mobile": false,
  "is_vpn": false,
  "is_tor": false,
  "is_hosting": false,
  "ptr_record": "142.69.2.81.in-addr.arpa",
  "source": "ipconfig.is",
  "visitor_total": 353,
  "visitor_country": 291
}

Single fields

GET/{field}

GET/{field}/{ip}

One field as plain text, no jq needed. Every JSON field name works as a path, plus short aliases such as /country, /asn and /ptr. Add an IP to ask about that address.

Path parameters

NameTypeDescription
fieldrequiredstringA JSON field name, or one of the short paths listed below.
ipstringAny public IPv4 or IPv6 address. Leave it out for your own.
  • Short paths: /ip/city/district/region/region-code/country/country-code/registered-country/continent/asn/asn-number/org/isp/network/timezone/offset/currency/ptr/lat/lon/accuracy/zip/vpn/tor/hosting/mobile
  • Booleans print true or false; a missing value prints an empty line.
  • Send Accept: application/json (or add ?format=json) to get {"country_name": "United Kingdom"} instead.
Request
curl ipconfig.is/country
const res = await fetch('https://ipconfig.is/country');
const country = (await res.text()).trim();
import requests

country = requests.get("https://ipconfig.is/country").text.strip()
Response200 · text/plain · example
United Kingdom

Request headers

GET/headers

Your HTTP request, echoed back: the method, scheme, host, path, query and every header you sent. Handy for checking what a proxy, VPN or client library really sends.

  • Headers our own proxies add (X-Forwarded-*, X-Real-IP, CF-*) are hidden, because yours could not be told apart from them. Cloudflare rewrites Accept-Encoding, so that is hidden too.
  • An Authorization header is shown as its scheme and length only.
Request
curl ipconfig.is/headers
const res = await fetch('https://ipconfig.is/headers');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/headers").json()
Response200 · application/json · example
{
  "method": "GET",
  "scheme": "https",
  "host": "ipconfig.is",
  "path": "/headers",
  "query": {},
  "client_ip": "81.2.69.142",
  "headers": {
    "Accept": "*/*",
    "Host": "ipconfig.is",
    "User-Agent": "curl/8.5.0"
  },
  "notes": [
    "Accept-Encoding is not shown: Cloudflare rewrites it before the request reaches us.",
    "Proxy headers (X-Forwarded-*, X-Real-IP, CF-*) are hidden: our proxies add them, so yours cannot be told apart."
  ]
}

Open ports

GET/ports

Which of 34 common TCP ports on your public IPv4 address the internet can reach. We connect from our server to the address the request came from; it never takes a target.

Query parameters

NameTypeDescription
portintegerCheck one port instead (1 to 65535). An open port also returns the banner its service sends, so you can see a port forward reaches the right service.
formatstringtext prints a table instead of JSON.
  • States: open answered, closed refused, filtered no answer within 3 seconds (a firewall). A full scan therefore takes about 3 seconds.
  • IPv4 only for now. Over IPv6 you get 422 ipv6_unsupported: use curl -4, or v4.ipconfig.is/ports.
  • A scan targets whoever calls it, so a page on another website cannot start one (403 cross_site). On a VPN or mobile network the response says that it scanned the provider's address.
Request
curl -4 ipconfig.is/ports
// Node.js or another server-side runtime: a page on another website gets 403 cross_site
const res = await fetch('https://ipconfig.is/ports');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/ports").json()
Response200 · application/json · example
{
  "ip": "81.2.69.142",
  "scanned_at": 1791686400,
  "duration_ms": 3012,
  "open_count": 1,
  "ports": [
    {
      "port": 21,
      "service": "FTP",
      "state": "closed",
      "risky": true
    },
    {
      "port": 22,
      "service": "SSH",
      "state": "open",
      "risky": false
    },
    {
      "port": 23,
      "service": "Telnet",
      "state": "filtered",
      "risky": true
    },
    … 31 more
  ]
}

Any public IP

Look up an IP

GET/json/{ip}

The full record for any public IPv4 or IPv6 address: the same fields as your own, without the visitor counters.

Path parameters

NameTypeDescription
iprequiredstringA public IPv4 or IPv6 address. Brackets and percent-encoding are fine, and ::ffff: mapped addresses become IPv4. Also accepted as ?ip=, or as a bare path for IPv4 (/8.8.8.8).
  • Private, reserved, special-use and multicast space is refused with 400 non_routable.
  • 6to4, Teredo and NAT64 addresses add address_type and embedded_ipv4; the location is the relay's.
Request
curl ipconfig.is/json/8.8.8.8
const res = await fetch('https://ipconfig.is/json/8.8.8.8');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/json/8.8.8.8").json()
Response200 · application/json · example
{
  "ip": "8.8.8.8",
  "city": null,
  "district": null,
  "region": null,
  "region_code": null,
  "country_name": "United States",
  "country_code": "US",
  "registered_country_code": "US",
  "continent": "North America",
  "continent_code": "NA",
  "zip": null,
  "latitude": 37.751,
  "longitude": -97.822,
  "accuracy_radius": 1000,
  "timezone": "America/Chicago",
  "offset": -18000,
  "currency": "USD",
  "org": "Google LLC",
  "asn": "AS15169 Google LLC",
  "asn_number": 15169,
  "network": "8.8.8.0/24",
  "is_mobile": false,
  "is_vpn": false,
  "is_tor": false,
  "is_hosting": true,
  "ptr_record": "dns.google",
  "source": "ipconfig.is"
}

Reputation

GET/threat/{ip}

GET/threat

Is the address a known bad actor? Checked live against six DNS blocklists (at each list's own nameservers), the Tor exit list and abuse.ch botnet C2 feeds, with a verdict and a score.

Parameters

NameTypeDescription
ipstringAny public address, in the path or as ?ip=. Leave it out for your own.
freshstring1 skips our cache and re-checks every list now.
  • threat_level is clean, suspicious, malicious or unknown; score runs from 0 to 100.
  • Each entry in lists is listed, clean, unavailable (a list we could not reach is never reported clean) or skipped (IPv4-only lists for an IPv6 address).
  • mail_policy names policy listings such as Spamhaus PBL: normal for home broadband, not a reputation problem.
Request
curl ipconfig.is/threat/8.8.8.8
const res = await fetch('https://ipconfig.is/threat/8.8.8.8');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/threat/8.8.8.8").json()
Response200 · application/json · example
{
  "ip": "8.8.8.8",
  "threat_level": "clean",
  "score": 10,
  "reasons": [
    "Hosting / datacenter network (non-residential)"
  ],
  "is_tor": false,
  "is_vpn": false,
  "is_proxy": false,
  "is_hosting": true,
  "is_c2": false,
  "blocklists": [],
  "blocklist_count": 0,
  "lists": [
    {
      "name": "Spamhaus ZEN",
      "zone": "zen.spamhaus.org",
      "status": "clean",
      "codes": [],
      "lookup": "https://check.spamhaus.org/results/?query=8.8.8.8"
    },
    {
      "name": "DroneBL",
      "zone": "dnsbl.dronebl.org",
      "status": "clean",
      "codes": [],
      "lookup": "https://dronebl.org/lookup?ip=8.8.8.8"
    },
    … 4 more
  ],
  "checked_lists": [
    "Spamhaus ZEN",
    "DroneBL",
    "blocklist.de",
    "SpamCop",
    "GBUdb Truncate",
    "s5h.net"
  ],
  "unavailable_lists": [],
  "skipped_lists": [],
  "mail_policy": [],
  "ipv6_skipped": false,
  "checked_at": "2026-10-11T02:32:27+00:00",
  "cached": false
}

IP WHOIS

GET/whois/{ip}

GET/whois

Who the address block is registered to: the range, network name, organisation, abuse contact and allocation dates, from the regional registry.

Parameters

NameTypeDescription
ipstringAny public address, in the path or as ?ip=. Leave it out for your own.
  • RDAP first, through the IANA bootstrap, so blocks transferred between registries are found at their current home; port-43 WHOIS is the fallback.
  • If no registry answers you get 502 upstream; try again later.
Request
curl ipconfig.is/whois/8.8.8.8
const res = await fetch('https://ipconfig.is/whois/8.8.8.8');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/whois/8.8.8.8").json()
Response200 · application/json · example
{
  "ip": "8.8.8.8",
  "range": "8.8.8.0 - 8.8.8.255",
  "cidr": "8.8.8.0/24",
  "netname": "GOGL",
  "handle": "NET-8-8-8-0-2",
  "type": "DIRECT ALLOCATION",
  "org": "Google LLC",
  "country": "US",
  "abuse_email": "[email protected]",
  "allocated": "2023-12-28",
  "updated": "2023-12-28",
  "parent": "NET-8-0-0-0-0",
  "rir": "ARIN",
  "source": "rdap",
  "rdap_url": "https://rdap.arin.net/registry/ip/8.8.8.8",
  "cached": false
}

Network tools

DNS lookup

GET/dns?name={name}&type={type}

Any record type through validating resolvers, with the DNSSEC status, the CNAME chain and TTLs.

Query parameters

NameTypeDescription
namerequiredstringA domain name. A pasted URL or e-mail address is reduced to its host. For PTR, an IP address.
typestringThe record type, A by default. One of A, AAAA, CNAME, MX, TXT, NS, SOA, CAA, PTR, SRV, HTTPS, SVCB, DS, DNSKEY, CDS, CDNSKEY, TLSA, SSHFP, NAPTR, DNAME.
shortstring1 prints one record per line, like dig +short. format=text does the same.
  • dnssec is secure, insecure or bogus; a bogus answer carries the resolver's extended error in ede.
  • Path form: /dns/github.com or /dns/github.com/MX.

Try it in the toolbox

Request
curl "ipconfig.is/dns?name=github.com&type=MX"
const res = await fetch('https://ipconfig.is/dns?name=github.com&type=MX');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/dns?name=github.com&type=MX").json()
Response200 · application/json · example
{
  "status": "NOERROR",
  "authenticated": false,
  "dnssec": "insecure",
  "name": "github.com",
  "type": "MX",
  "resolver": "1.1.1.1",
  "records": [
    {
      "name": "github.com",
      "type": "MX",
      "ttl": 183,
      "priority": 0,
      "value": "github-com.mail.protection.outlook.com",
      "data": "0 github-com.mail.protection.outlook.com"
    }
  ]
}

DNS propagation

GET/propagation?name={name}&type={type}

The same record from 12 public resolvers around the world (including China and Russia) and from every authoritative nameserver of the zone, so you can see whether a change has gone live.

Query parameters

NameTypeDescription
namerequiredstringA domain name.
typestringAny type DNS lookup takes; A by default.
  • consistent is true when every resolver agrees. Resolvers show what they have cached until the TTL runs out; the authoritative servers show what is published now, with their SOA serials.
  • CDN and GeoDNS names legitimately answer differently in different places.

Try it in the toolbox

Request
curl "ipconfig.is/propagation?name=github.com&type=A"
const res = await fetch('https://ipconfig.is/propagation?name=github.com&type=A');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/propagation?name=github.com&type=A").json()
Response200 · application/json · example
{
  "name": "github.com",
  "type": "A",
  "consistent": false,
  "resolvers": [
    {
      "resolver": "Cloudflare",
      "note": "1.1.1.1",
      "status": "NOERROR",
      "values": [
        "140.82.121.4"
      ],
      "ok": true,
      "authenticated": false,
      "ttl": 48,
      "differs": true
    },
    {
      "resolver": "Quad9",
      "note": "unfiltered, no DNSSEC validation",
      "status": "NOERROR",
      "values": [
        "140.82.121.3"
      ],
      "ok": true,
      "authenticated": false,
      "ttl": 34,
      "differs": false
    },
    {
      "resolver": "AliDNS",
      "note": "China",
      "status": "NOERROR",
      "values": [
        "20.200.245.247"
      ],
      "ok": true,
      "authenticated": false,
      "ttl": 4,
      "differs": true
    },
    … 9 more
  ],
  "authoritative": {
    "zone": "github.com",
    "servers": [
      {
        "ns": "dns1.p08.nsone.net",
        "ip": "198.51.44.8",
        "status": "NOERROR",
        "aa": true,
        "values": [
          "140.82.121.4"
        ],
        "ttl": 60,
        "serial": 1656468023
      },
      {
        "ns": "ns-421.awsdns-52.com",
        "ip": "205.251.193.165",
        "status": "NOERROR",
        "aa": true,
        "values": [
          "140.82.121.3"
        ],
        "ttl": 60,
        "serial": 1
      },
      … 6 more
    ],
    "consistent": false,
    "serials": [
      1656468023,
      1
    ]
  }
}

TLS certificate

GET/tls?host={host}

The certificate a server presents and how it is served: validity and days remaining, the chain (including missing intermediates), revocation, the TLS versions it accepts, the key and the SHA-256 fingerprint.

Query parameters

NameTypeDescription
hostrequiredstringA host name or IP, optionally with a port: github.com, smtp.gmail.com:587, [2606:4700::1111]:853 or a pasted URL.
portintegerThe port, if not in host. Standard TLS ports only (443, 465, 636, 853, 993, 995, 8443 and others); the error lists them.
starttlsstringsmtp, imap, pop3 or ftp. Set automatically on 25, 587, 2525, 143, 110 and 21.
  • valid is the verdict; expired, name_matches, trusted and chain_issue (incomplete, self_signed, untrusted_root) say why.

Try it in the toolbox

Request
curl "ipconfig.is/tls?host=github.com"
const res = await fetch('https://ipconfig.is/tls?host=github.com');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/tls?host=github.com").json()
Response200 · application/json · example
{
  "host": "github.com",
  "ip": "140.82.121.4",
  "port": 443,
  "protocol": "TLSv1.3",
  "cipher": "TLS_AES_128_GCM_SHA256",
  "legacy_protocol": false,
  "valid": true,
  "name_matches": true,
  "trusted": true,
  "chain_issue": null,
  "expired": false,
  "not_yet_valid": false,
  "days_remaining": 49,
  "revocation": {
    "status": "unknown",
    "note": "The CA publishes revocation via OCSP only, which is not queried"
  },
  "protocols": {
    "TLSv1": false,
    "TLSv1.1": false,
    "TLSv1.2": true,
    "TLSv1.3": true
  },
  "cert": {
    "subject_cn": "github.com",
    "subject_o": null,
    "issuer_cn": "Sectigo Public Server Authentication CA DV E36",
    "issuer_o": "Sectigo Limited",
    "serial": "A59EBDB596751DB7F5C095079613953C",
    "sig_alg": "ecdsa-with-SHA256",
    "key_type": "EC",
    "key_bits": 256,
    "key_curve": "prime256v1",
    "fingerprint_sha256": "46:B6:01:EE:08:B4:18:CF:8A:3A:1E:BF:E6:70:BA:5C:E4:3B:B0:5A:91:7F:A8:B2:DD:08:7A:30:47:1C:FC:63",
    "valid_from": "2026-09-01T00:00:00+00:00",
    "valid_to": "2026-11-29T23:59:59+00:00",
    "sans": [
      "github.com",
      "www.github.com"
    ],
    "ca_issuers_url": "http://crt.sectigo.com/SectigoPublicServerAuthenticationCADVE36.crt",
    "ocsp_url": "http://ocsp.sectigo.com",
    "crl_urls": []
  },
  "chain": [
    {
      "subject": "github.com",
      "issuer": "Sectigo Public Server Authentication CA DV E36",
      "valid_to": "2026-11-29T23:59:59+00:00"
    },
    … 2 more
  ],
  "chain_notes": []
}

Email authentication

GET/mail?domain={domain}

Whether a domain's email is set up right, graded: SPF evaluated the way a receiver does it (including the 10-lookup limit), DMARC on the organisational domain, DKIM at common selectors, MX health with forward-confirmed reverse DNS, MTA-STS, TLS-RPT and BIMI.

Query parameters

NameTypeDescription
domainrequiredstringThe domain. An e-mail address or URL is reduced to it.
selectorstringYour DKIM selectors, comma-separated (up to 5), probed alongside the common ones.
  • Each entry in checks has a status (pass, warn, fail, info) and a one-line note; summary holds the grade and findings.

Try it in the toolbox

Request
curl "ipconfig.is/mail?domain=github.com"
const res = await fetch('https://ipconfig.is/mail?domain=github.com');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/mail?domain=github.com").json()
Response200 · application/json · example
{
  "domain": "github.com",
  "mx": [
    {
      "priority": 0,
      "host": "github-com.mail.protection.outlook.com",
      "issues": [],
      "ips": [
        "52.101.194.158",
        "52.101.40.56",
        … 6 more
      ],
      "ptr": "mail-ch3pr04cu00106.inbound.protection.outlook.com",
      "fcrdns": true
    }
  ],
  "null_mx": false,
  "implicit_mx": false,
  "spf": {
    "found": true,
    "record": "v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com include:_netblocks.google.com include:_netblocks2.google.com include:mail.zendesk.com include:_spf.salesforce.com include:servers.mcsv.net include:mktomail.com include:sendgrid.net ip4:62.253.227.114 ip4:166.78.69.169 ip4:166.78.69.170 ip4:166.78.71.131 ~all",
    "lookups": 10,
    "void_lookups": 0,
    "tree": [
      … 8 more
    ],
    "errors": [],
    "warnings": [],
    "policy": "softfail",
    "qualifier": "~all",
    "note": "Soft fail - unauthorised senders are marked (DMARC decides)"
  },
  "dmarc": {
    "found": true,
    "record": "v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:[email protected]; ruf=mailto:[email protected]; fo=1",
    "policy": "quarantine",
    "sp": "reject",
    "pct": 100
  },
  "dkim": {
    "found": true,
    "selectors": [
      "google",
      "selector1",
      … 6 more
    ],
    "keys": [
      {
        "selector": "google",
        "key_type": "rsa",
        "key_bits": 2048,
        "testing": false
      },
      … 7 more
    ]
  },
  "mta_sts": {
    "found": false
  },
  "tls_rpt": {
    "found": false
  },
  "bimi": {
    "found": false
  },
  "checks": {
    "mx": {
      "status": "pass",
      "note": "1 MX host(s), all resolving"
    },
    "spf": {
      "status": "pass",
      "note": "Soft fail - unauthorised senders are marked (DMARC decides) (10/10 lookups)"
    },
    "dmarc": {
      "status": "pass",
      "note": "p=quarantine"
    },
    "dkim": {
      "status": "pass",
      "note": "Keys at: google, selector1, s1, s2, k1, k2, k3, cm"
    },
    "mta_sts": {
      "status": "info",
      "note": "Not published (optional)"
    },
    "tls_rpt": {
      "status": "info",
      "note": "Not published (optional)"
    },
    "bimi": {
      "status": "info",
      "note": "Not published (optional)"
    }
  },
  "summary": {
    "score": 100,
    "grade": "A",
    "findings": [
      "MX, SPF and DMARC are correctly set up."
    ],
    "no_mail": false
  }
}

HTTP trace

GET/trace?url={url}

Follows a URL's redirects hop by hop, with the status, HTTP version, timings and headers of each, then grades the final page's security headers and cookie flags.

Query parameters

NameTypeDescription
urlrequiredstringAn http or https URL. A bare host is treated as https://.
  • Every hop is a real GET, up to 10 hops within 15 seconds; each redirect target is checked again before it is fetched.
  • A trace that fails part-way (a loop, a timeout) still returns 200 with the hops it walked and an error.

Try it in the toolbox

Request
curl "ipconfig.is/trace?url=http://github.com"
const res = await fetch('https://ipconfig.is/trace?url=http://github.com');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/trace?url=http://github.com").json()
Response200 · application/json · example
{
  "url": "http://github.com",
  "final_url": "https://github.com/",
  "redirects": 1,
  "hops": [
    {
      "url": "http://github.com",
      "ip": "140.82.121.4",
      "status": 301,
      "http_version": "HTTP/1.1",
      "server": null,
      "timing_ms": {
        "connect": 4,
        "tls": null,
        "first_byte": 9,
        "total": 9
      },
      "headers": {
        "content-length": "0",
        "location": "https://github.com/"
      },
      "location": "https://github.com/"
    },
    {
      "url": "https://github.com/",
      "ip": "140.82.121.4",
      "status": 200,
      "http_version": "HTTP/2",
      "server": "github.com",
      "timing_ms": {
        "connect": 4,
        "tls": 18,
        "first_byte": 24,
        "total": 28
      },
      "headers": {
        "content-type": "text/html; charset=utf-8",
        "strict-transport-security": "max-age=31536000; includeSubdomains; preload",
        "x-frame-options": "deny",
        … 14 more
      },
      "security": {
        "score": 14,
        "max": 15,
        "grade": "A",
        "headers": {
          "strict-transport-security": {
            "status": "ok",
            "value": "max-age=31536000; includeSubdomains; preload",
            "note": "Preload-ready"
          },
          "permissions-policy": {
            "status": "missing",
            "value": null,
            "note": "Not set"
          },
          … 4 more
        },
        "present": [
          "strict-transport-security",
          "content-security-policy",
          … 3 more
        ],
        "missing": [
          "permissions-policy"
        ]
      },
      "cookies": [
        {
          "name": "_gh_sess",
          "secure": true,
          "httponly": true,
          "samesite": "Lax"
        },
        … 2 more
      ]
    }
  ]
}

Domain security audit

GET/audit?domain={domain}

One graded report on a domain's security posture, built from the other tools: TLS, the HTTPS redirect and security headers, email authentication, CAA and DNSSEC.

Query parameters

NameTypeDescription
domainrequiredstringThe domain to audit.
  • Scoring: the essentials are worth 70 points (TLS 35, email 35) and hardening 30 (security headers 15, CAA 8, DNSSEC 7). Grades: A from 85, B from 70, C from 50, D from 30.
  • issues are real problems (an expired or mismatched certificate, no HTTPS); recommendations are hardening steps.

Try it in the toolbox

Request
curl "ipconfig.is/audit?domain=github.com"
const res = await fetch('https://ipconfig.is/audit?domain=github.com');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/audit?domain=github.com").json()
Response200 · application/json · example
{
  "domain": "github.com",
  "score": 92,
  "grade": "A",
  "categories": {
    "tls": {
      "status": "pass",
      "host": "github.com",
      "valid": true,
      "protocol": "TLSv1.3",
      "protocols": {
        "TLSv1": false,
        "TLSv1.1": false,
        "TLSv1.2": true,
        "TLSv1.3": true
      },
      "days_remaining": 49,
      "issuer": "Sectigo Public Server Authentication CA DV E36",
      "chain_issue": null
    },
    "email": {
      "status": "pass",
      "grade": "A",
      "score": 100,
      "no_mail": false,
      "checks": {
        "mx": "pass",
        "spf": "pass",
        "dmarc": "pass",
        "dkim": "pass",
        "mta_sts": "info",
        "tls_rpt": "info",
        "bimi": "info"
      },
      "spf": "softfail",
      "dmarc": "quarantine",
      "dkim": true,
      "mx": 1,
      "null_mx": false
    },
    "headers": {
      "status": "pass",
      "grade": "A",
      "score": 14,
      "url": "https://github.com/",
      "https_redirect": true,
      "hsts_preload_ready": true,
      "present": [
        "strict-transport-security",
        "content-security-policy",
        "x-content-type-options",
        "x-frame-options",
        "referrer-policy"
      ],
      "missing": [
        "permissions-policy"
      ]
    },
    "caa": {
      "status": "pass",
      "count": 7,
      "found_at": "github.com"
    },
    "dnssec": {
      "status": "info",
      "signed": false,
      "bogus": false
    }
  },
  "issues": [],
  "recommendations": [
    "Security headers to add or fix: permissions-policy.",
    "DNSSEC is not enabled (optional) - signing protects DNS answers from tampering."
  ]
}

Domain WHOIS

GET/whois?domain={domain}

A domain's registration, from the registry itself: the registrar, when it was registered and when it expires, its nameservers, status codes and DNSSEC.

Query parameters

NameTypeDescription
domainrequiredstringThe domain. A subdomain is reduced to its registered domain, and the response says so in queried.
  • RDAP through the IANA bootstrap; registry WHOIS (port 43) for the TLDs that have no RDAP. A domain nobody has registered returns 404 not_registered.

Try it in the toolbox

Request
curl "ipconfig.is/whois?domain=github.com"
const res = await fetch('https://ipconfig.is/whois?domain=github.com');
const data = await res.json();
import requests

data = requests.get("https://ipconfig.is/whois?domain=github.com").json()
Response200 · application/json · example
{
  "domain": "github.com",
  "registrar": "MarkMonitor Inc.",
  "abuse_email": "[email protected]",
  "created": "2007-10-09T18:20:50Z",
  "updated": "2026-09-07T09:22:52Z",
  "expires": "2028-10-09T18:20:50Z",
  "age_days": 6941,
  "expires_in_days": 729,
  "nameservers": [
    "dns1.p08.nsone.net",
    "dns2.p08.nsone.net",
    "dns3.p08.nsone.net",
    "dns4.p08.nsone.net",
    "ns-1283.awsdns-32.org",
    "ns-1707.awsdns-21.co.uk",
    "ns-421.awsdns-52.com",
    "ns-520.awsdns-01.net"
  ],
  "status": [
    "client delete prohibited",
    "client transfer prohibited",
    "client update prohibited"
  ],
  "dnssec": false,
  "source": "rdap",
  "rdap_url": "https://rdap.verisign.com/com/v1/domain/github.com"
}

Reference

Response fields

The record returned by /json and /json/{ip}. Every field is always present; a value that is unknown for the address is null.

FieldTypeDescription
Location
citystringCity name
districtstringSecond-level subdivision, such as a county or province
regionstringFirst-level subdivision: a state, province or constituent country
region_codestringISO 3166-2 code of the region, such as CA or SCT
country_namestringFull country name
country_codestringISO 3166-1 alpha-2 code, such as US
registered_country_codestringCountry the block is registered in: set even for anycast space with no location
continentstringContinent name
continent_codestringTwo-letter continent code
zipstringPostal or ZIP code
latitudefloatApproximate latitude
longitudefloatApproximate longitude
accuracy_radiusintegerRadius in kilometres around the coordinates where the address likely is
timezonestringIANA time zone, such as Asia/Bangkok
offsetintegerCurrent UTC offset in seconds, daylight saving included
currencystringISO 4217 currency code
Network
ipstringPublic IP address (IPv4 or IPv6), canonical form
orgstringOrganisation that operates the network (the ISP); /isp prints it too
asnstringAutonomous system number and name, such as AS15169 Google LLC
asn_numberintegerThe AS number alone
networkstringThe network block the address belongs to, in CIDR notation
ptr_recordstringReverse-DNS hostname
address_typestring6to4, teredo or nat64 for IPv6 transition addresses; absent otherwise
embedded_ipv4stringThe IPv4 address inside a transition address
Flags
is_mobilebooleantrue for a mobile or cellular network (positive evidence only)
is_vpnbooleantrue for a VPN, proxy or Tor exit
is_torbooleantrue for a current Tor exit node
is_hostingbooleantrue for a datacenter, hosting or cloud network
Record
sourcestringData source: ipconfig.is
visitor_totalintegerAll-time page visits; your own record only
visitor_countryintegerPage visits from your country; your own record only

Plain-text paths: /ip/city/district/region/region-code/country/country-code/registered-country/continent/asn/asn-number/org/isp/network/timezone/offset/currency/ptr/lat/lon/accuracy/zip/vpn/tor/hosting/mobile, plus every JSON field name.

Recipes

A few patterns that come up often: pulling fields out with jq, checking a blocklist, watching a certificate, and backing off politely.

  • One value, no jq: every field has its own path, such as curl ipconfig.is/country.
  • Several values: jq '{ip, city, country_code, org}' picks them from the record.
  • Monitoring: /tls reports days_remaining, and /whois?domain= reports expires_in_days.
jq
# One field
curl -s ipconfig.is/json | jq -r '.country_code'

# Several fields
curl -s ipconfig.is/json | jq '{ip, city, country_code, org}'

# Is this address on a blocklist?
curl -s ipconfig.is/threat/1.2.3.4 | jq -r '.threat_level, .blocklists[]'
Shell script
#!/bin/sh
# Warn when a certificate has under 14 days left
DAYS=$(curl -s "https://ipconfig.is/tls?host=example.com" | jq -r '.days_remaining')
[ "$DAYS" -lt 14 ] && echo "example.com: certificate expires in $DAYS days"

# Retry politely on 429 (Retry-After is honoured)
curl -s --retry 3 --retry-all-errors ipconfig.is/json