Skip to content
tutorial

DNS lookup API: query A, MX, and TXT records over REST

| 6 min read
Server racks in a data center
Photo by Taylor Vick on Unsplash

Your monitoring script needs to verify MX records after a DNS migration. You could install dig, parse its multi-line output with regex, and handle the dozen edge cases where the format varies between record types. Or you could send one HTTP request and get structured JSON back.

The botoi DNS API gives you three endpoints: single-type lookup, batch lookup for multiple record types at once, and a propagation checker that queries Google, Cloudflare, and Quad9 in parallel. All three return consistent JSON you can feed into any script or application without parsing gymnastics.

Look up a single record type

The /v1/dns/lookup endpoint takes a domain and an optional record type. If you omit the type, it defaults to A records. Here's an MX lookup for stripe.com:

curl -X POST https://api.botoi.com/v1/dns/lookup \
  -H "Content-Type: application/json" \
  -d '{"domain": "stripe.com", "type": "MX"}'

Response:

{
  "success": true,
  "data": {
    "domain": "stripe.com",
    "type": "MX",
    "records": [
      { "type": "MX", "value": "aspmx.l.google.com", "priority": 1, "ttl": 3600 },
      { "type": "MX", "value": "alt1.aspmx.l.google.com", "priority": 5, "ttl": 3600 },
      { "type": "MX", "value": "alt2.aspmx.l.google.com", "priority": 5, "ttl": 3600 },
      { "type": "MX", "value": "alt3.aspmx.l.google.com", "priority": 10, "ttl": 3600 },
      { "type": "MX", "value": "alt4.aspmx.l.google.com", "priority": 10, "ttl": 3600 }
    ],
    "query_time_ms": 14
  }
}

Every MX record comes back with a priority field and a ttl in seconds. No need to split strings or guess which number is the priority; the API does that for you.

TXT records for SPF and verification

TXT records hold SPF policies, domain ownership tokens, and DKIM selectors. Query them the same way:

curl -X POST https://api.botoi.com/v1/dns/lookup \
  -H "Content-Type: application/json" \
  -d '{"domain": "github.com", "type": "TXT"}'

Response:

{
  "success": true,
  "data": {
    "domain": "github.com",
    "type": "TXT",
    "records": [
      { "type": "TXT", "value": "v=spf1 ip4:192.30.252.0/22 include:_netblocks.google.com ~all", "ttl": 3600 },
      { "type": "TXT", "value": "MS=ms58704441", "ttl": 3600 },
      { "type": "TXT", "value": "docusign=087098e3-3d46-47b7-9b4e-8a23028154cd", "ttl": 3600 }
    ],
    "query_time_ms": 11
  }
}

The API supports eight record types: A, AAAA, MX, TXT, CNAME, NS, SOA, and PTR. Each type returns the same structured format with type, value, and ttl fields.

Terminal showing DNS query results
Photo by Sai Kiran Anagani on Unsplash

Query multiple record types at once

Four separate HTTP calls to get A, MX, TXT, and NS records is wasteful. The /v1/dns/batch endpoint runs all the lookups in parallel and returns the results grouped by type.

curl -X POST https://api.botoi.com/v1/dns/batch \
  -H "Content-Type: application/json" \
  -d '{"domain": "github.com", "types": ["A", "MX", "TXT", "NS"]}'

Response:

{
  "success": true,
  "data": {
    "domain": "github.com",
    "results": {
      "A": [
        { "type": "A", "value": "140.82.121.3", "ttl": 60 }
      ],
      "MX": [
        { "type": "MX", "value": "aspmx.l.google.com", "priority": 1, "ttl": 3600 },
        { "type": "MX", "value": "alt1.aspmx.l.google.com", "priority": 5, "ttl": 3600 },
        { "type": "MX", "value": "alt2.aspmx.l.google.com", "priority": 5, "ttl": 3600 }
      ],
      "TXT": [
        { "type": "TXT", "value": "v=spf1 ip4:192.30.252.0/22 include:_netblocks.google.com ~all", "ttl": 3600 },
        { "type": "TXT", "value": "MS=ms58704441", "ttl": 3600 }
      ],
      "NS": [
        { "type": "NS", "value": "dns1.p08.nsone.net", "ttl": 900 },
        { "type": "NS", "value": "dns2.p08.nsone.net", "ttl": 900 },
        { "type": "NS", "value": "dns3.p08.nsone.net", "ttl": 900 },
        { "type": "NS", "value": "dns4.p08.nsone.net", "ttl": 900 }
      ]
    }
  }
}

One request, one response, all four record types. If you omit the types array, the endpoint defaults to A, AAAA, MX, TXT, and NS. This is useful for building domain overview dashboards or running comprehensive DNS audits.

Check DNS propagation across resolvers

You updated your A record 20 minutes ago. Your local resolver shows the new IP, but customers in other regions still hit the old server. The /v1/dns/propagation endpoint queries three major public resolvers and tells you if they agree.

curl -X POST https://api.botoi.com/v1/dns/propagation \
  -H "Content-Type: application/json" \
  -d '{"domain": "stripe.com", "type": "A"}'

Response:

{
  "success": true,
  "data": {
    "domain": "stripe.com",
    "type": "A",
    "resolvers": {
      "google": {
        "records": ["185.166.143.28", "185.166.143.29"],
        "response_time_ms": 18
      },
      "cloudflare": {
        "records": ["185.166.143.28", "185.166.143.29"],
        "response_time_ms": 9
      },
      "quad9": {
        "records": ["185.166.143.28", "185.166.143.29"],
        "response_time_ms": 22
      }
    },
    "consistent": true
  }
}

The consistent field is true when all three resolvers return the same sorted set of records. When it's false, the resolvers object shows you exactly which resolver is still serving stale data and how long each query took.

Practical example: verify MX records before accepting signups

A common use case: someone enters user@typo-domain.cm in your signup form. Syntax validation passes because the format is correct, but the domain has no mail servers. You'll discover this three days later when the welcome email bounces.

This Node.js example checks MX records at signup time and rejects email addresses pointing to domains with no mail infrastructure:

import express from "express";

const app = express();
app.use(express.json());

async function lookupMx(domain) {
  const res = await fetch("https://api.botoi.com/v1/dns/lookup", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": process.env.BOTOI_API_KEY,
    },
    body: JSON.stringify({ domain, type: "MX" }),
  });
  return res.json();
}

app.post("/signup", async (req, res) => {
  const { email } = req.body;
  const domain = email.split("@")[1];

  // Check if the domain has MX records before accepting the signup
  const { data } = await lookupMx(domain);

  if (!data.records || data.records.length === 0) {
    return res.status(422).json({
      error: `The domain \${domain} has no mail servers. Check the email address and try again.`,
    });
  }

  // MX records exist; proceed with signup
  await createUser({ email });
  res.status(201).json({ created: true });
});

app.listen(3000);

The check adds 10-30ms to the signup request. That's a small price to prevent bounce-related damage to your sender reputation. You can also cache MX results per domain for 30 minutes to avoid repeated lookups for the same domain.

Monitor DNS propagation after a migration

After changing DNS records, you want to know the moment all major resolvers serve the new values. This script polls the propagation endpoint every 30 seconds and logs the state of each resolver:

async function checkPropagation(domain, type) {
  const res = await fetch("https://api.botoi.com/v1/dns/propagation", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Api-Key": process.env.BOTOI_API_KEY,
    },
    body: JSON.stringify({ domain, type }),
  });
  return res.json();
}

// After updating DNS records, poll until all resolvers agree
async function waitForPropagation(domain, type, intervalMs = 30000) {
  let attempts = 0;

  while (attempts < 60) {
    const { data } = await checkPropagation(domain, type);

    console.log(
      `Attempt \${attempts + 1}: consistent=\${data.consistent}`
    );

    for (const [name, info] of Object.entries(data.resolvers)) {
      console.log(`  \${name}: \${JSON.stringify(info.records)}`);
    }

    if (data.consistent) {
      console.log("Propagation complete.");
      return data;
    }

    attempts++;
    await new Promise((r) => setTimeout(r, intervalMs));
  }

  throw new Error("Propagation did not complete within 30 minutes.");
}

// Usage
waitForPropagation("yourdomain.com", "A");

Run this as a background job after making DNS changes. It logs the records from each resolver on every attempt and exits when all three agree. The 30-second interval keeps you under the free tier's rate limit (5 requests per minute).

Key points

  • /v1/dns/lookup returns structured JSON for any of the eight supported record types. MX records include priority; SOA records include serial, refresh, retry, expire, and minimum fields.
  • /v1/dns/batch queries multiple record types in parallel with one request. Defaults to A, AAAA, MX, TXT, and NS if you don't specify.
  • /v1/dns/propagation checks Google, Cloudflare, and Quad9 and returns a boolean consistent flag. Use it to verify DNS changes have gone global.
  • All three endpoints work anonymously at 5 req/min. Pass an X-Api-Key header for higher limits.
  • The API runs on Cloudflare's edge network. Queries resolve through DNS-over-HTTPS, so you get the same reliability as querying Cloudflare or Google DNS directly, wrapped in a developer-friendly JSON format.

Frequently asked questions

How do I look up DNS records programmatically?
Send a POST request to https://api.botoi.com/v1/dns/lookup with a JSON body containing the domain and record type. The API returns structured JSON with all matching records, TTLs, and query time. No dig installation or output parsing required.
What DNS record types does the API support?
The API supports eight record types: A, AAAA, MX, TXT, CNAME, NS, SOA, and PTR. If you omit the type parameter, it defaults to A records.
How do I check if DNS changes have propagated?
Use the /v1/dns/propagation endpoint. It queries Google DNS, Cloudflare DNS, and Quad9 in parallel and returns a "consistent" boolean indicating whether all three resolvers return the same records. If consistent is false, propagation is still in progress.
Can I query multiple DNS record types in one request?
Yes. The /v1/dns/batch endpoint accepts a types array (e.g., ["A", "MX", "TXT"]) and queries all of them in parallel. The response groups records by type. If you omit the types parameter, it defaults to A, AAAA, MX, TXT, and NS.
Is the DNS lookup API free?
Anonymous access is available at 5 requests per minute and 100 requests per day with IP-based rate limiting. No API key or account required. For higher throughput, paid plans start at $9/month with a single API key covering all 150+ endpoints.

Try this API

DNS Lookup API — interactive playground and code examples

More tutorial posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.