Skip to content
tutorial

WHOIS API: structured domain lookups via RDAP in one POST

| 6 min read
Terminal window showing domain lookup results
Photo by Sai Kiran Anagani on Unsplash

You're building a domain monitoring tool. You need the registrar, expiry date, and nameservers for every domain in your portfolio. The old WHOIS protocol gives you unstructured text. Every registrar formats it differently. Parsing it means writing fragile regexes that break when a registrar changes their output format.

RDAP (Registration Data Access Protocol) solves this by returning structured JSON. But each TLD has a different RDAP server, and you have to query the IANA bootstrap registry to find it. Then you still need to normalize the response because RDAP implementations vary across registries.

The botoi /v1/whois endpoint handles all of this. One POST request, one JSON response, every TLD.

The endpoint

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

Response:

{
  "success": true,
  "data": {
    "domain": "stripe.com",
    "registrar": "SafeNames Ltd.",
    "status": [
      "client delete prohibited",
      "client transfer prohibited",
      "client update prohibited",
      "server delete prohibited",
      "server transfer prohibited",
      "server update prohibited"
    ],
    "created": "1995-09-12T04:00:00Z",
    "updated": "2024-06-18T10:22:31Z",
    "expires": "2032-09-11T04:00:00Z",
    "nameservers": [
      "ns-cloud-d1.googledomains.com",
      "ns-cloud-d2.googledomains.com",
      "ns-cloud-d3.googledomains.com",
      "ns-cloud-d4.googledomains.com"
    ]
  }
}

Six fields cover 90% of what you need from WHOIS: the registrar name, domain status codes, creation date, last update, expiry date, and nameservers. All dates are ISO 8601. All nameservers are lowercase. No parsing required.

Terminal window with command output
Photo by Sai Kiran Anagani on Unsplash

Raw WHOIS text vs. structured API response

Here's what the raw WHOIS protocol returns for the same domain. This is the text you'd get from a whois stripe.com command:

Domain Name: STRIPE.COM
Registry Domain ID: 609783_DOMAIN_COM-VRSN
Registrar WHOIS Server: whois.safenames.net
Registrar URL: http://www.safenames.net
Updated Date: 2024-06-18T10:22:31Z
Creation Date: 1995-09-12T04:00:00Z
Registry Expiry Date: 2032-09-11T04:00:00Z
Registrar: SafeNames Ltd.
Registrar IANA ID: 447
Registrar Abuse Contact Email: abuse@safenames.net
Registrar Abuse Contact Phone: +44.1onal234567
Domain Status: clientDeleteProhibited
Domain Status: clientTransferProhibited
Domain Status: clientUpdateProhibited
Domain Status: serverDeleteProhibited
Domain Status: serverTransferProhibited
Domain Status: serverUpdateProhibited
Name Server: NS-CLOUD-D1.GOOGLEDOMAINS.COM
Name Server: NS-CLOUD-D2.GOOGLEDOMAINS.COM
Name Server: NS-CLOUD-D3.GOOGLEDOMAINS.COM
Name Server: NS-CLOUD-D4.GOOGLEDOMAINS.COM
DNSSEC: unsigned
URL of the ICANN Whois Inaccuracy Complaint Form:
   https://www.icann.org/wicf/
>>> Last update of whois database: 2026-03-29T10:00:00Z <<<

That's a wall of text with no standard format. The field names, spacing, and order change between registrars. Verisign formats dates one way. Nominet formats them another. Some registrars include the registrant's name and address. Others redact everything under GDPR privacy shields.

The API response gives you the same data in a predictable structure. You access data.expires instead of writing a regex for "Registry Expiry Date:". You iterate data.nameservers instead of scanning for lines starting with "Name Server:".

Build a domain expiry monitor

Losing a domain because someone forgot to renew it is expensive and embarrassing. This Node.js script checks a list of domains and flags any that expire within 30 days. Run it daily with a cron job or GitHub Actions scheduled workflow.

import Botoi from "botoi";

const botoi = new Botoi({ apiKey: process.env.BOTOI_API_KEY });

const domains = [
  "stripe.com",
  "github.com",
  "vercel.com",
  "cloudflare.com",
  "mycompany.io",
];

async function checkExpiry(domain) {
  const res = await fetch("https://api.botoi.com/v1/whois", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.BOTOI_API_KEY}`,
    },
    body: JSON.stringify({ domain }),
  });
  return res.json();
}

async function monitorDomains() {
  const now = new Date();
  const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
  const alerts = [];

  for (const domain of domains) {
    const { success, data } = await checkExpiry(domain);

    if (!success || !data.expires) {
      console.error(`Failed to check ${domain}`);
      continue;
    }

    const expiresAt = new Date(data.expires);
    const daysLeft = Math.floor(
      (expiresAt.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
    );

    if (daysLeft <= 30) {
      alerts.push({
        domain,
        expires: data.expires,
        daysLeft,
        registrar: data.registrar,
        nameservers: data.nameservers,
      });
    }

    console.log(
      `${domain}: expires ${data.expires} (${daysLeft} days)`
    );
  }

  if (alerts.length > 0) {
    console.warn("\n--- EXPIRY ALERTS ---");
    for (const alert of alerts) {
      console.warn(
        `  ${alert.domain} expires in ${alert.daysLeft} days (${alert.expires})`
      );
      console.warn(`  Registrar: ${alert.registrar}`);
      console.warn(`  Nameservers: ${alert.nameservers.join(", ")}`);
    }

    // Send to Slack, PagerDuty, email, etc.
    await sendAlert(alerts);
  }

  return alerts;
}

async function sendAlert(alerts) {
  // Replace with your webhook URL
  const slackWebhook = process.env.SLACK_WEBHOOK_URL;
  if (!slackWebhook) return;

  const text = alerts
    .map(
      (a) =>
        `*${a.domain}* expires in *${a.daysLeft} days* (${a.expires}). Registrar: ${a.registrar}`
    )
    .join("\n");

  await fetch(slackWebhook, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: `:warning: Domain expiry alert\n${text}` }),
  });
}

// Run daily via cron or GitHub Actions
monitorDomains();

The script iterates through your domain list, checks each one, and collects domains expiring within 30 days. The sendAlert function posts to Slack. Swap it for PagerDuty, email, or any webhook endpoint.

You can extend this in a few ways: store results in a database to track expiry trends, add a 60-day and 90-day threshold for tiered alerts, or cross-reference the nameservers to detect unauthorized DNS changes.

Understanding domain status codes

The status array in the response tells you what operations the registry and registrar allow on the domain. Here's a reference for the most common codes:

Status code                     Meaning
──────────────────────────────  ─────────────────────────────────────────
client delete prohibited        Registrar-level lock; prevents deletion
client transfer prohibited      Registrar-level lock; blocks transfers
client update prohibited        Registrar-level lock; blocks DNS changes
server delete prohibited        Registry-level lock; prevents deletion
server transfer prohibited      Registry-level lock; blocks transfers
server update prohibited        Registry-level lock; blocks modifications
pending delete                  Domain is in the 5-day deletion grace period
redemption period               Domain expired; can be restored for a fee
auto renew period               Domain was auto-renewed by the registrar

Domains with "server transfer prohibited" and "server delete prohibited" have registry-level locks. These are stronger than client-level locks because only the registry operator can remove them. High-value domains like stripe.com and google.com have both layers.

If you see "pending delete" or "redemption period" in a domain's status, act fast. The domain is either about to be released or can be restored for a fee through the registrar.

When to use this endpoint

  • Domain portfolio monitoring. Track expiry dates across hundreds of domains. Alert your team before any domain lapses.
  • Phishing investigation. Check when a suspicious domain was registered. Phishing domains are often created hours or days before an attack.
  • Lead qualification. A domain created in 2003 suggests an established company. A domain registered last week tells a different story.
  • DNS change detection. Compare current nameservers against a known baseline. A nameserver change you didn't authorize could mean a domain hijack.
  • Compliance and due diligence. Verify domain ownership details during vendor onboarding or M&A due diligence. The registrar and status codes reveal the domain's security posture.

Key points

  • POST /v1/whois returns registrar, dates, status codes, and nameservers in normalized JSON.
  • The endpoint queries RDAP servers, not the legacy WHOIS text protocol. You get structured data without writing parsers.
  • Anonymous access works at 5 requests per minute with no API key. Paid plans remove that limit.
  • The response includes an expires field in ISO 8601. Use it to build expiry monitors, alert pipelines, or domain dashboards.

Frequently asked questions

What is the difference between WHOIS and RDAP?
WHOIS is the legacy protocol from 1982. It returns unstructured plain text with no standard format across registrars. RDAP (Registration Data Access Protocol) is the IETF-standardized replacement that returns structured JSON. The botoi /v1/whois endpoint queries RDAP servers and returns a normalized JSON response.
Is the WHOIS lookup API free?
Yes. Anonymous access requires no API key and allows 5 requests per minute plus 100 per day. Paid plans start at $9/month for higher rate limits.
Which TLDs does the API support?
The API queries the RDAP bootstrap registry, which covers .com, .net, .org, .io, .dev, .app, .co, and most gTLDs. Some country-code TLDs (ccTLDs) have limited RDAP coverage and may return partial data.
Why are owner contact fields missing from the response?
Most registrars apply GDPR-compliant privacy protection by default. Contact details (name, email, address) are redacted at the registry level. The API returns what the RDAP server provides: registrar, dates, status codes, and nameservers are always available.
Can I look up WHOIS data for an IP address instead of a domain?
This endpoint handles domain WHOIS only. For IP address ownership data (ASN, network organization, CIDR range), use the /v1/ip-whois/lookup endpoint.

Try this API

WHOIS 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.