RDAP‑first domain registration lookups with WHOIS fallback. Produces a single, normalized record shape regardless of source.
- RDAP‑first lookup via IANA bootstrap with automatic WHOIS fallback when needed
- Smart WHOIS client (TCP 43): authoritative TLD discovery, registrar referral follow, and curated exceptions
- Rich, normalized results: registrar, contacts, nameservers, EPP statuses, key dates, DNSSEC, privacy flag, source metadata
- RDAP enrichment: follows related/entity/registrar links (bounded) to fill in missing details
- TypeScript‑first: shipped types, ESM‑only, zero external HTTP client (uses global
fetch)
Important
Edge runtimes (e.g., Vercel Edge, Cloudflare Workers) do not support WHOIS (TCP 43 via node:net). Use RDAP‑only mode by setting { rdapOnly: true }.
Tip
See rdapper in action on Domainstack!
npm install rdapperimport { lookup } from "rdapper";
const { ok, record, error } = await lookup("example.com");
if (!ok) throw new Error(error);
console.log(record); // normalized DomainRecordNormalize arbitrary input (domain or URL) to its registrable domain (eTLD+1):
import { toRegistrableDomain } from "rdapper";
toRegistrableDomain("https://sub.example.co.uk/page"); // => "example.co.uk"
toRegistrableDomain("spark-public.s3.amazonaws.com"); // => "amazonaws.com" (ICANN-only default)
toRegistrableDomain("192.168.0.1"); // => nullConvenience helpers to quickly check availability:
import { isRegistered, isAvailable } from "rdapper";
await isRegistered("example.com"); // => true
await isRegistered("likely-unregistered-thing-320485230458.com"); // => false
await isAvailable("example.com"); // => false
await isAvailable("likely-unregistered-thing-320485230458.com"); // => truelookup(domain, options?) => Promise<LookupResult>- Tries RDAP first if supported by the domain’s TLD; if unavailable or fails, falls back to WHOIS (unless toggled off).
- Result is
{ ok: boolean, record?: DomainRecord, error?: string }.
toRegistrableDomain(input, options?) => string | null- Normalizes a domain or URL to its registrable domain (eTLD+1).
- Returns the registrable domain string, or
nullfor IPs/invalid input; options are forwarded totldts(e.g.,allowPrivateDomains).
isRegistered(domain, options?) => Promise<boolean>isAvailable(domain, options?) => Promise<boolean>
For quick checks, a minimal CLI is included:
npx rdapper example.com
echo "example.com" | npx rdapperWHOIS requires a raw TCP connection over port 43 via node:net, which is not available on edge runtimes. rdapper lazily loads node:net only when the WHOIS path is taken.
- Prefer RDAP only on edge:
import { lookup } from "rdapper";
const res = await lookup("example.com", { rdapOnly: true });- If
rdapOnlyis omitted and the code path reaches WHOIS on edge, rdapper throws a clear runtime error advising to run in Node or set{ rdapOnly: true }.
timeoutMs?: number– Total timeout budget per network operation (default15000).rdapOnly?: boolean– Only attempt RDAP; do not fall back to WHOIS.whoisOnly?: boolean– Skip RDAP and query WHOIS directly.followWhoisReferral?: boolean– Follow registrar referral from the TLD WHOIS (defaulttrue).maxWhoisReferralHops?: number– Maximum registrar WHOIS referral hops to follow (default2).rdapFollowLinks?: boolean– Follow related/entity RDAP links to enrich data (defaulttrue).maxRdapLinkHops?: number– Maximum RDAP related link hops to follow (default2).rdapLinkRels?: string[]– RDAP link rel values to consider (default["related","entity","registrar","alternate"]).customBootstrapUrl?: string– Override RDAP bootstrap URL.whoisHints?: Record<string, string>– Override/add authoritative WHOIS per TLD (keys are lowercase TLDs, values may include or omitwhois://).includeRaw?: boolean– IncluderawRdap/rawWhoisin the returned record (defaultfalse).signal?: AbortSignal– Optional cancellation signal.
The exact presence of fields depends on registry/registrar data and whether RDAP or WHOIS was used.
interface DomainRecord {
domain: string; // normalized name (unicode when available)
tld: string; // public suffix (can be multi-label, e.g., "com", "co.uk")
isRegistered: boolean; // availability heuristic (WHOIS) or true (RDAP)
isIDN?: boolean; // uses punycode labels (xn--)
unicodeName?: string; // RDAP unicodeName when provided
punycodeName?: string; // RDAP ldhName when provided
registry?: string; // registry operator (rarely available)
registrar?: {
name?: string;
ianaId?: string;
url?: string;
email?: string;
phone?: string;
};
reseller?: string;
statuses?: Array<{
status: string;
description?: string;
raw?: string;
}>;
creationDate?: string; // ISO 8601 (UTC)
updatedDate?: string; // ISO 8601 (UTC)
expirationDate?: string; // ISO 8601 (UTC)
deletionDate?: string; // ISO 8601 (UTC)
transferLock?: boolean; // derived from EPP statuses
dnssec?: {
enabled: boolean;
dsRecords?: Array<{
keyTag?: number;
algorithm?: number;
digestType?: number;
digest?: string;
}>;
};
nameservers?: Array<{
host: string;
ipv4?: string[];
ipv6?: string[];
}>;
contacts?: Array<{
type: "registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
name?: string;
organization?: string;
email?: string | string[];
phone?: string | string[];
fax?: string | string[];
street?: string[];
city?: string;
state?: string;
postalCode?: string;
country?: string;
countryCode?: string;
}>;
privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics
whoisServer?: string; // authoritative WHOIS queried (if any)
rdapServers?: string[]; // RDAP URLs tried (bootstrap bases and related/entity links)
rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
rawWhois?: string; // raw WHOIS text (only when options.includeRaw)
source: "rdap" | "whois"; // which path produced data
warnings?: string[];
}{
"domain": "example.com",
"tld": "com",
"isRegistered": true,
"registrar": { "name": "Internet Assigned Numbers Authority", "ianaId": "376" },
"statuses": [{ "status": "clientTransferProhibited" }],
"nameservers": [{ "host": "a.iana-servers.net" }, { "host": "b.iana-servers.net" }],
"dnssec": { "enabled": true },
"source": "rdap"
}- RDAP
- Discovers base URLs for the TLD via IANA’s RDAP bootstrap JSON.
- Tries each base until one responds successfully; parses standard RDAP domain JSON.
- Optionally follows related/entity links to registrar RDAP resources and merges results (bounded by hop limits).
- Normalizes registrar (from
entities), contacts (vCard), nameservers (ipAddresses), events (created/changed/expiration), statuses, and DNSSEC (secureDNS).
- WHOIS
- Discovers the authoritative TLD WHOIS via
whois.iana.org(TCP 43), with curated exceptions for tricky zones and public SLDs. - Queries the TLD WHOIS and follows registrar referrals recursively up to
maxWhoisReferralHops(unless disabled). - Normalizes common key/value variants across gTLD/ccTLD formats (dates, statuses, nameservers, contacts). Availability is inferred from common phrases (best‑effort heuristic).
- Discovers the authoritative TLD WHOIS via
Timeouts are enforced per request using a simple race against timeoutMs (default 15s). All network I/O is performed with global fetch (RDAP) and a raw TCP socket (WHOIS).
- Build:
npm run build(tsdown) - Test:
npm test(Vitest)- By default, tests are offline/deterministic.
- Watch mode:
npm run dev - Coverage:
npm run test:run -- --coverage - Smoke tests that hit the network are gated by
SMOKE=1, e.g.SMOKE=1 npm test.
- Lint/format:
npm run lint(Biome)
Project layout:
src/rdap/– RDAP bootstrap, client, and normalizationsrc/whois/– WHOIS TCP client, discovery/referral, normalization, exceptionssrc/lib/– utilities for dates, text parsing, domain processing, asyncsrc/types.ts– public types;src/index.tsre‑exports API and typesbin/cli.js– simple CLI for quick checks
- WHOIS text formats vary significantly across registries/registrars; normalization is best‑effort.
- Availability detection relies on common WHOIS phrases and is not authoritative.
- Some TLDs provide no RDAP service;
rdapOnly: truewill fail for them. - Registries may throttle or block WHOIS; respect rate limits and usage policies.
- Field presence depends on source and privacy policies (e.g., redaction/withholding).
- Public suffix detection uses
tldtswith ICANN‑only defaults (Private section is ignored). You can pass options through totldtsviatoRegistrableDomain/getDomainParts/getDomainTld(e.g.,allowPrivateDomains) to customize behavior. See: tldts migration notes.