Contents
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.
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 isnullrather than missing.- Browsers get the website
- A request that asks for
text/htmlgets the page; curl, wget, HTTPie, PowerShell and code (fetch, requests, Go) get the data. For JSON at/, sendAccept: 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,
/tlstakeshost:port, and path shortcuts such as/dns/github.com/MXwork too. - IPv4 and IPv6
- The API answers on both.
curl -4and-6choose;v4.ipconfig.isonly speaks IPv4.
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: 20Rate limits
40 requests every 20 seconds for each IP address (each /64 for IPv6), across every endpoint.
- Every response carries
RateLimit-Limit,RateLimit-RemainingandRateLimit-Reset, the seconds until the window resets. - Over the limit you get
429withRetry-After, andremainingin 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-errorswaits as long asRetry-Afterasks.
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.
curl ipconfig.is/json/10.0.0.1const 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"]){
"error": "Not a public, routable IP address (private, reserved, special-use or multicast)",
"code": "non_routable"
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_ip non_routable | A malformed IP, or private, reserved, special-use or multicast space |
| 400 | invalid_domain unsupported_type unsupported_port invalid_url missing_host missing_url | Bad tool input; the response lists what is supported |
| 403 | cross_site tor_exit | /ports started by a page on another website, or called from a Tor exit |
| 404 | unknown_endpoint nxdomain not_registered | No such endpoint, the domain does not exist, or it is not registered |
| 422 | ipv6_unsupported | /ports over IPv6: use curl -4, or v4.ipconfig.is |
| 429 | rate_limited | Too many requests: wait for Retry-After (also the JSON remaining field) |
| 502 | upstream unreachable | The remote host or registry failed (TLS handshake, timeout, registry down); the message says why |
| 503 | unavailable 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 -4andcurl -6pick the address family.v4.ipconfig.isalways answers over IPv4, for tools that cannot choose.- Send
Accept: application/jsonto get your full record from the same URL.
curl ipconfig.isconst res = await fetch('https://ipconfig.is');
const ip = (await res.text()).trim();import requests
ip = requests.get("https://ipconfig.is").text.strip()81.2.69.142Your 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_totalandvisitor_country. Calling the API never counts as a visit.
curl ipconfig.is/jsonconst res = await fetch('https://ipconfig.is/json');
const data = await res.json();import requests
data = requests.get("https://ipconfig.is/json").json(){
"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
| Name | Type | Description |
|---|---|---|
fieldrequired | string | A JSON field name, or one of the short paths listed below. |
ip | string | Any 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
trueorfalse; a missing value prints an empty line. - Send
Accept: application/json(or add?format=json) to get{"country_name": "United Kingdom"}instead.
curl ipconfig.is/countryconst res = await fetch('https://ipconfig.is/country');
const country = (await res.text()).trim();import requests
country = requests.get("https://ipconfig.is/country").text.strip()United KingdomRequest 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 rewritesAccept-Encoding, so that is hidden too. - An
Authorizationheader is shown as its scheme and length only.
curl ipconfig.is/headersconst res = await fetch('https://ipconfig.is/headers');
const data = await res.json();import requests
data = requests.get("https://ipconfig.is/headers").json(){
"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
| Name | Type | Description |
|---|---|---|
port | integer | Check 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. |
format | string | text prints a table instead of JSON. |
- States:
openanswered,closedrefused,filteredno 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: usecurl -4, orv4.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.
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(){
"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
| Name | Type | Description |
|---|---|---|
iprequired | string | A 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_typeandembedded_ipv4; the location is the relay's.
curl ipconfig.is/json/8.8.8.8const 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(){
"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
| Name | Type | Description |
|---|---|---|
ip | string | Any public address, in the path or as ?ip=. Leave it out for your own. |
fresh | string | 1 skips our cache and re-checks every list now. |
threat_levelisclean,suspicious,maliciousorunknown;scoreruns from 0 to 100.- Each entry in
listsislisted,clean,unavailable(a list we could not reach is never reported clean) orskipped(IPv4-only lists for an IPv6 address). mail_policynames policy listings such as Spamhaus PBL: normal for home broadband, not a reputation problem.
curl ipconfig.is/threat/8.8.8.8const 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(){
"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
| Name | Type | Description |
|---|---|---|
ip | string | Any 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.
curl ipconfig.is/whois/8.8.8.8const 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(){
"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
| Name | Type | Description |
|---|---|---|
namerequired | string | A domain name. A pasted URL or e-mail address is reduced to its host. For PTR, an IP address. |
type | string | The 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. |
short | string | 1 prints one record per line, like dig +short. format=text does the same. |
dnssecissecure,insecureorbogus; a bogus answer carries the resolver's extended error inede.- Path form:
/dns/github.comor/dns/github.com/MX.
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(){
"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
| Name | Type | Description |
|---|---|---|
namerequired | string | A domain name. |
type | string | Any type DNS lookup takes; A by default. |
consistentistruewhen 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.
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(){
"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
| Name | Type | Description |
|---|---|---|
hostrequired | string | A host name or IP, optionally with a port: github.com, smtp.gmail.com:587, [2606:4700::1111]:853 or a pasted URL. |
port | integer | The port, if not in host. Standard TLS ports only (443, 465, 636, 853, 993, 995, 8443 and others); the error lists them. |
starttls | string | smtp, imap, pop3 or ftp. Set automatically on 25, 587, 2525, 143, 110 and 21. |
validis the verdict;expired,name_matches,trustedandchain_issue(incomplete,self_signed,untrusted_root) say why.
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(){
"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
| Name | Type | Description |
|---|---|---|
domainrequired | string | The domain. An e-mail address or URL is reduced to it. |
selector | string | Your DKIM selectors, comma-separated (up to 5), probed alongside the common ones. |
- Each entry in
checkshas astatus(pass,warn,fail,info) and a one-linenote;summaryholds the grade and findings.
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(){
"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
| Name | Type | Description |
|---|---|---|
urlrequired | string | An 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.
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(){
"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
| Name | Type | Description |
|---|---|---|
domainrequired | string | The 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.
issuesare real problems (an expired or mismatched certificate, no HTTPS);recommendationsare hardening steps.
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(){
"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
| Name | Type | Description |
|---|---|---|
domainrequired | string | The 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.
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(){
"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.
| Field | Type | Description |
|---|---|---|
| Location | ||
city | string | City name |
district | string | Second-level subdivision, such as a county or province |
region | string | First-level subdivision: a state, province or constituent country |
region_code | string | ISO 3166-2 code of the region, such as CA or SCT |
country_name | string | Full country name |
country_code | string | ISO 3166-1 alpha-2 code, such as US |
registered_country_code | string | Country the block is registered in: set even for anycast space with no location |
continent | string | Continent name |
continent_code | string | Two-letter continent code |
zip | string | Postal or ZIP code |
latitude | float | Approximate latitude |
longitude | float | Approximate longitude |
accuracy_radius | integer | Radius in kilometres around the coordinates where the address likely is |
timezone | string | IANA time zone, such as Asia/Bangkok |
offset | integer | Current UTC offset in seconds, daylight saving included |
currency | string | ISO 4217 currency code |
| Network | ||
ip | string | Public IP address (IPv4 or IPv6), canonical form |
org | string | Organisation that operates the network (the ISP); /isp prints it too |
asn | string | Autonomous system number and name, such as AS15169 Google LLC |
asn_number | integer | The AS number alone |
network | string | The network block the address belongs to, in CIDR notation |
ptr_record | string | Reverse-DNS hostname |
address_type | string | 6to4, teredo or nat64 for IPv6 transition addresses; absent otherwise |
embedded_ipv4 | string | The IPv4 address inside a transition address |
| Flags | ||
is_mobile | boolean | true for a mobile or cellular network (positive evidence only) |
is_vpn | boolean | true for a VPN, proxy or Tor exit |
is_tor | boolean | true for a current Tor exit node |
is_hosting | boolean | true for a datacenter, hosting or cloud network |
| Record | ||
source | string | Data source: ipconfig.is |
visitor_total | integer | All-time page visits; your own record only |
visitor_country | integer | Page 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:
/tlsreportsdays_remaining, and/whois?domain=reportsexpires_in_days.
# 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[]'#!/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