DNS Robot API

The engines behind our DNS, domain, email and network tools as a JSON API. Every lookup is one GET request: no key, no sign-up, and CORS open to any website.

  • No API key
  • CORS on every origin
  • OpenAPI 3.1
GEThttps://dnsrobot.netBase URL/api/v2Version/mxEndpoint?domain=Parameters
200 OKRecorded 2026-10-25. Send it to run the request live.
{
  "query": {
    "domain": "github.com"
  },
  "verdict": "mail",
  "zone": "github.com",
  "answer": {
    "source": "authoritative",
    "server": "ns-421.awsdns-52.com",
    "ip": "205.251.193.165",
    "viaCloudflare": false,
    "status": "ok",
    "authoritative": true,
    "dnssecValidated": null,
    "negativeTtl": null,
    "cnameChain": []
  },
  "mx": [
    {
      "priority": 0,
      "host": "github-com.mail.protection.outlook.com",
      "ttl": 300,
      "provider": {
        "id": "microsoft-365",
        "name": "Microsoft 365",
        "kind": "mailbox"
      },
      "status": "ok",
      "cname": null,
      "addresses": [
        {
          "ip": "52.101.10.16",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "52.96.0.0/12",
            "country": "US"
          },
          "reverse": {
            "name": "mail-bn6pr21cu00500.inbound.protection.outlook.com",
            "forwardConfirmed": true
          }
        },
        {
          "ip": "52.101.9.26",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "52.96.0.0/12",
            "country": "US"
          },
          "reverse": null
        },
        {
          "ip": "52.101.194.164",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "52.96.0.0/12",
            "country": "US"
          },
          "reverse": null
        },
        {
          "ip": "52.101.194.3",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "52.96.0.0/12",
            "country": "US"
          },
          "reverse": null
        },
        {
          "ip": "2a01:111:f403:c902::7",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "2a01:111:f000::/36",
            "country": "GB"
          },
          "reverse": {
            "name": "mail-sj0pr02cu00107.inbound.protection.outlook.com",
            "forwardConfirmed": true
          }
        },
        {
          "ip": "2a01:111:f403:c922::7",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "2a01:111:f000::/36",
            "country": "GB"
          },
          "reverse": null
        },
        {
          "ip": "2a01:111:f403:f804::2",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "2a01:111:f000::/36",
            "country": "GB"
          },
          "reverse": null
        },
        {
          "ip": "2a01:111:f403:f909::6",
          "network": {
            "asn": 8075,
            "name": "Microsoft",
            "prefix": "2a01:111:f000::/36",
            "country": "GB"
          },
          "reverse": null
        }
      ],
      "dnssecSigned": false
    }
  ],
  "implicitMx": null,
  "parentZone": null,
  "provider": {
    "mailbox": {
      "id": "microsoft-365",
      "name": "Microsoft 365",
      "kind": "mailbox",
      "evidence": [
        {
          "kind": "mx",
          "value": "github-com.mail.protection.outlook.com"
        },
        {
          "kind": "spf",
          "value": "include:spf.protection.outlook.com"
        },
        {
          "kind": "autodiscover",
          "value": "autodiscover.outlook.com"
        }
      ]
    },
    "gateway": null,
    "ownServers": false,
    "multipleProviders": []
  },
  "security": {
    "spf": {
      "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,
      "all": "~all",
      "multipleRecords": false,
      "missingIncludes": []
    },
    "dmarc": {
      "record": "v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:dmarc@github.com; ruf=mailto:dmarc@github.com; fo=1",
      "policy": "quarantine",
      "subdomainPolicy": "reject",
      "effectivePolicy": "quarantine",
      "pct": 100,
      "rua": [
        "dmarc@github.com"
      ],
      "inheritedFrom": null,
      "multipleRecords": false
    },
    "mtaSts": null,
    "tlsRpt": null,
    "bimi": null,
    "dane": [
      {
        "host": "github-com.mail.protection.outlook.com",
        "tlsa": [],
        "dnssecValidated": false,
        "status": "none"
      }
    ],
    "dnssec": "unsigned",
    "ds": [],
    "autodiscover": "autodiscover.outlook.com"
  },
  "sources": [
    {
      "source": "authoritative",
      "server": "ns-421.awsdns-52.com",
      "ip": "205.251.193.165",
      "status": "ok",
      "viaCloudflare": false,
      "records": [
        {
          "priority": 0,
          "host": "github-com.mail.protection.outlook.com",
          "ttl": 300
        }
      ],
      "tookMs": 27,
      "sameAsAuthoritative": null
    },
    {
      "source": "google",
      "server": "dns.google",
      "ip": "8.8.8.8",
      "status": "ok",
      "viaCloudflare": false,
      "records": [
        {
          "priority": 0,
          "host": "github-com.mail.protection.outlook.com",
          "ttl": 205
        }
      ],
      "tookMs": 82,
      "sameAsAuthoritative": true
    },
    {
      "source": "cloudflare",
      "server": "one.one.one.one",
      "ip": "1.1.1.1",
      "status": "ok",
      "viaCloudflare": false,
      "records": [
        {
          "priority": 0,
          "host": "github-com.mail.protection.outlook.com",
          "ttl": 300
        }
      ],
      "tookMs": 2,
      "sameAsAuthoritative": true
    }
  ],
  "issues": [],
  "tookMs": 1039
}

Quick start

Ask for github.com's mail servers. There is nothing to install and nothing to sign up for:

Every answer is JSON with the normalised query you asked about, the result, and tookMs. The same call works from a browser page, a shell script, a serverless function or a CI job.

curl "https://dnsrobot.net/api/v2/mx?domain=github.com"
const params = new URLSearchParams({ domain: "github.com" });
const res = await fetch(`https://dnsrobot.net/api/v2/mx?${params}`);
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data.mx);
import requests

r = requests.get(
    "https://dnsrobot.net/api/v2/mx",
    params={"domain": "github.com"},
    timeout=30,
)
data = r.json()
if not r.ok:
    raise RuntimeError(data["error"]["message"])
print(data["mx"])
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

func main() {
	q := url.Values{"domain": {"github.com"}}
	res, err := http.Get("https://dnsrobot.net/api/v2/mx?" + q.Encode())
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	var data map[string]any
	if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
		panic(err)
	}
	if res.StatusCode != http.StatusOK {
		panic(data["error"])
	}
	fmt.Println(data["mx"])
}
<?php
$query = http_build_query(["domain" => "github.com"]);
$context = stream_context_create(["http" => ["ignore_errors" => true, "timeout" => 30]]);
$data = json_decode(file_get_contents("https://dnsrobot.net/api/v2/mx?" . $query, false, $context), true);
if (isset($data["error"])) {
    throw new RuntimeException($data["error"]["message"]);
}
print_r($data["mx"]);
require "net/http"
require "json"

uri = URI("https://dnsrobot.net/api/v2/mx")
uri.query = URI.encode_www_form(domain: "github.com")
data = JSON.parse(Net::HTTP.get(uri))
raise data["error"]["message"] if data["error"]
puts data["mx"]
// Cargo.toml: reqwest = { version = "0.13", features = ["blocking", "json"] }, serde_json = "1"
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = reqwest::blocking::get("https://dnsrobot.net/api/v2/mx?domain=github.com")?;
    let ok = res.status().is_success();
    let data: serde_json::Value = res.json()?;
    if !ok {
        return Err(data["error"]["message"].to_string().into());
    }
    println!("{}", data["mx"]);
    Ok(())
}
using System.Text.Json;

using var http = new HttpClient();
var res = await http.GetAsync("https://dnsrobot.net/api/v2/mx?domain=github.com");
var data = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!res.IsSuccessStatusCode)
    throw new Exception(data.GetProperty("error").GetProperty("message").GetString());
Console.WriteLine(data.GetProperty("mx"));

Requests and responses

Base URL
https://dnsrobot.net/api/v2. Every endpoint is a GET with query parameters, and GET /api/v2 lists them all.
Input
Parameters are read the way our tool pages read them: a full URL, upper case, a trailing dot or an internationalised domain all work, and the normalised value comes back in query.
Output
JSON with camelCase keys. Durations end in Ms, DNS TTLs are seconds in ttl, dates are ISO 8601 in UTC. A value we couldn't determine is null, never a made-up 0 or false.
Findings are not errors
A domain that doesn't exist, a closed port or an address on a blocklist is a 200 answer with that finding in the body. Problems in a record come back in issues, each with a severity, a stable code and a plain-English message.
Partial answers
When one source fails or times out, the answer still comes back with what the others found, plus incomplete: true and warnings saying what is missing.
Versioning
New fields and new issue codes can appear at any time, so ignore keys you don't know. Renaming or removing a field would mean a new version at /api/v3.

Errors

An error is about the request or our side, never about the target. Its body always has the same shape, and code is stable for your code to branch on:

StatusCodeWhen
400missing_parameterA required parameter is absent or empty; param names it.
400invalid_parameterA parameter can't be read; param names it.
404not_foundNo such endpoint.
405method_not_allowedThe method isn't GET or OPTIONS.
422target_not_allowedA private, reserved or refused target (RFC 1918, loopback, documentation ranges).
429rate_limitedOver the endpoint's limit a minute; wait Retry-After seconds.
500internal_errorA bug on our side. Retrying rarely helps; tell us the requestId.
503unavailableEvery source the lookup needs failed. Retry after a short wait.
504timeoutThe lookup ran out of time before any source answered.
400 Bad Request
{
  "error": {
    "status": 400,
    "code": "invalid_parameter",
    "message": "domain must be a domain name like example.com.",
    "param": "domain",
    "requestId": "8d2f6c41a9e3b7f0",
    "docs": "https://dnsrobot.net/api-docs#errors"
  }
}

Rate limits and fair use

Limits count calls a minute per IP address (an IPv6 caller per /64, or per /56 where noted). Every answer carries RateLimit-Policy: <calls>;w=60; past the limit you get a 429 with Retry-After. Our tool pages have their own budget, so using the API never slows them down for you.

EndpointCalls a minuteAnswers kept
DNS lookup /api/v2/dns60Not kept
DNS propagation /api/v2/dns/propagation20Not kept
MX lookup /api/v2/mx20Kept for the MX records' TTL, 30 seconds to 5 minutes (30 seconds when a part failed), shared with the MX Lookup page. A kept answer carries an `Age` header, and the resolvers' TTLs are counted down.
NS lookup /api/v2/ns20Kept for the shortest NS TTL seen, 30 seconds to 5 minutes (30 seconds when a part failed), shared with the NS Lookup page. A kept answer carries an `Age` header.
CNAME lookup /api/v2/cname20Kept for the chain's lowest TTL, 30 seconds to 5 minutes (30 seconds when a part failed), shared with the CNAME Lookup page. A kept answer carries an `Age` header, and the resolvers' TTLs are counted down.
Reverse DNS /api/v2/reverse-dns20Kept for the PTR record's TTL, 30 seconds to 5 minutes (30 seconds when a part failed), shared with the Reverse DNS page. A kept answer carries an `Age` header, and the resolvers' TTLs are counted down.
WHOIS lookup /api/v2/whois20A found record is kept an hour and a not-registered answer ten minutes, shared with the WHOIS Lookup page; `Cache-Control: max-age` says how long this answer has left.
Hosting check /api/v2/hosting10Kept an hour (a minute when a source didn't answer), shared with the Hosting Checker page; `Cache-Control: max-age` says how long this answer has left.
Domain to IP /api/v2/domain-ip30Not kept
SPF check /api/v2/spf20Not kept
DKIM check /api/v2/dkim20Not kept
DMARC check /api/v2/dmarc20Not kept
BIMI check /api/v2/bimi20Not kept
SSL checker /api/v2/ssl10A finished check is kept 10 minutes (1 minute when a part didn't finish or the server didn't answer), shared with the SSL Checker page; `checkedAt` says when it ran. Answers for names on Cloudflare's network aren't kept.
HTTP security headers /api/v2/http-headers20 (/56)The same run serves both endpoints and the HTTP Header Checker and Redirect Checker pages: a finished check is kept 2 minutes (30 seconds when a part ran out of time) per URL and user agent. A run that ended on a bot wall or whose first URL got no answer isn't kept.
Redirect chain /api/v2/redirects20 (/56)The same run serves both endpoints and the HTTP Header Checker and Redirect Checker pages: a finished check is kept 2 minutes (30 seconds when a part ran out of time) per URL and user agent. A run that ended on a bot wall or whose first URL got no answer isn't kept.
IP lookup /api/v2/ip60Not kept
What is my IP /api/v2/me60Not kept
ASN lookup /api/v2/asn301 hour (`public, max-age=3600`): the tables change with each data refresh, Team Cymru's routing every 4 hours. Answers with `incomplete` aren't cached.
Port check /api/v2/port12Not kept
IP blacklist check /api/v2/blacklist2010 minutes, 1 minute when a list refused or didn't answer (`public, max-age` is the time left).
MAC lookup /api/v2/mac1201 day (`public, max-age=86400`): the registry snapshot changes with our monthly sync.
MAC vendor search /api/v2/mac/vendors1201 day (`public, max-age=86400`): the registry snapshot changes with our monthly sync.
User agent parser /api/v2/user-agent120 (/56)Not kept

The API is free and comes without an uptime guarantee. Cache answers for their TTL, back off on a 429, and don't use it to scan networks you don't run. If you build something public on it, a link to dnsrobot.net is appreciated. Use is subject to the terms of service.

Calling it from a browser

Every endpoint sends Access-Control-Allow-Origin: *, so a page on any website can call it with fetch() and read X-Request-Id, RateLimit-Policy and Retry-After. No cookies or credentials are read, and nothing you look up is stored beyond the short answer caches listed above.

OpenAPI and client generators

The whole API is described by an OpenAPI 3.1 document at /api/v2/openapi.json, with every parameter, response field and error. Import it into Postman, Insomnia or Bruno, or generate a typed client with openapi-generator or Kiota.

Open openapi.json

SDKs and command-line tool

Official clients wrap the API in your language's idioms. They call the original API today; releases for v2 are being prepared, and the raw HTTP samples on this page work in the meantime.

Endpoints

Every endpoint runs on the same engine as one of our tools. Each page has its parameters, every response field, code in eight languages and real recorded answers.

DNS

Records from any resolver, propagation across 30+ resolvers, MX, NS, CNAME chains and reverse DNS.

Domains

Registration data, the host behind a site and every address a domain points to.

Email security

SPF, DKIM, DMARC and BIMI records, parsed and checked against their RFCs.

Web & SSL

Certificates and TLS, HTTP security headers and redirect chains.

IP & network

IP and ASN details, your own connection, open ports, blocklists and MAC vendors.

The original API

The first DNS Robot API (POST requests to /api/<tool>, the one the published SDK versions call) keeps answering with the same request and response shapes. It gets no new features: move to the v2 endpoint beside each one.

Original endpointUse instead
POST /api/dns-queryGET /api/v2/dns
POST /api/whoisGET /api/v2/whois
POST /api/ssl-certificateGET /api/v2/ssl
POST /api/spf-checkerGET /api/v2/spf
POST /api/dkim-checkerGET /api/v2/dkim
POST /api/dmarc-checkerGET /api/v2/dmarc
POST /api/mx-lookupGET /api/v2/mx
POST /api/ns-lookupGET /api/v2/ns
POST /api/ip-infoGET /api/v2/ip
POST /api/http-headersGET /api/v2/http-headers
GET /api/port-checkGET /api/v2/port
POST /api/cname-lookupGET /api/v2/cname
POST /api/reverse-dnsGET /api/v2/reverse-dns
POST /api/domain-ipGET /api/v2/domain-ip
POST /api/bimi-checkerGET /api/v2/bimi
GET /api/ip-blacklistGET /api/v2/blacklist

Changelog

  • API v2: GET endpoints on the tools' current engines, one error format, rate-limit headers and an OpenAPI 3.1 document.

Questions about the API

Is the DNS Robot API free?

Yes. Every endpoint is free to call, with no API key, no account and no credit card. The only limits are per-minute fair-use limits per IP address, listed under Rate limits, which keep the service fast for everyone. If you need more than they allow for a legitimate project, write to hello@dnsrobot.net and tell us what you are building.

Do I need an API key?

No. Send a plain GET request and read the JSON. There is no key to request, rotate or leak, and no header to set. Limits are counted per IP address instead, so a server, a laptop and a CI runner each get their own budget. Because nothing identifies you, the same request from a browser page works exactly like one from a script.

Is there a free WHOIS API?

Yes: GET /api/v2/whois?q=example.com returns a domain's registrar, creation and expiry dates, status codes, nameservers and DNSSEC as JSON. It asks the registry's RDAP service first and falls back to port-43 WHOIS for the extensions without RDAP, so country-code domains work too. The same parameter takes an IP address or an AS number.

Is there a free IP lookup API?

Yes. GET /api/v2/ip?ip=8.8.8.8 returns the network that announces an address, its AS number and holder, the registry, the reverse DNS name and an approximate location from the free DB-IP database. GET /api/v2/me answers the same for the caller's own address, and ?format=text returns just the IP for shell scripts.

Which DNS resolver does the DNS lookup API use?

Google Public DNS unless you choose another. Pass resolver=cloudflare, quad9, opendns, adguard or any public IPv4 resolver to ask that one instead. For the same record across more than 30 resolvers worldwide, use the propagation endpoint, which is the engine behind our DNS propagation checker.

Can I call the API from JavaScript in the browser?

Yes. Every endpoint answers with Access-Control-Allow-Origin: *, so fetch() works from a page on any website without a proxy. Remember that limits count per visitor IP, which suits a tool where each visitor runs their own lookups. For many lookups from one server, cache answers for their TTL.

What happens when I hit the rate limit?

You get HTTP 429 with the error code rate_limited and a Retry-After header in seconds. Wait that long and send the request again; the limits reset every minute. Each endpoint's limit is in the table above and in the RateLimit-Policy header of every answer, so a client can pace itself before it ever sees a 429.

Why does a domain that doesn't exist return 200?

Because the lookup worked: the answer is that the name doesn't exist. Errors are kept for problems with the request or on our side, such as a malformed parameter or a timeout. So NXDOMAIN, an unregistered domain, a closed port or a missing DMARC record all come back as 200 with the finding in the body, which keeps client code simple.

Is my data stored when I use the API?

No. We keep no log of who looked up what: rate limits count calls per IP for one minute, in Cloudflare's memory only. Answers are cached briefly, from a minute to a few hours depending on the endpoint, so repeated questions skip the network. The services a lookup asks, such as DNS resolvers, registries and certificate logs, see the name and our server's address, never yours.

How is this different from the original DNS Robot API?

The original API used POST requests with a different body for each tool. Version 2 is all GET requests with one error format, richer results such as issues, providers and grades, and an OpenAPI description. The original endpoints keep working for the published SDK versions, and each one is listed above beside its v2 replacement.