DNS lookup API: query A, MX, and TXT records over REST
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.
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/lookupreturns 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/batchqueries multiple record types in parallel with one request. Defaults to A, AAAA, MX, TXT, and NS if you don't specify. -
/v1/dns/propagationchecks Google, Cloudflare, and Quad9 and returns a booleanconsistentflag. Use it to verify DNS changes have gone global. -
All three endpoints work anonymously at 5 req/min. Pass an
X-Api-Keyheader 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.