# Bulk Domain Expiry & WHOIS Checker – RDAP Registrar, Dates (`kestrelwick/domain-expiry-checker`) Actor

Check hundreds of domains in one run: registrar, creation and expiry dates, days to expiry with an alert level, registry status codes, nameservers and DNSSEC. Official registry RDAP data, no API keys or proxies. Failed lookups are free.

- **URL**: https://apify.com/kestrelwick/domain-expiry-checker.md
- **Developed by:** [Kestrelwick Studios](https://apify.com/kestrelwick) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.25 / 1,000 domain looked ups

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bulk Domain Expiry & WHOIS Checker – RDAP Registrar, Dates, Status

Paste a list of domains and get **one clean row per unique domain**: whether it's registered, the **registrar**, the **creation, last-updated and expiry dates**, **days to expiry** with a ready-made **alert level** (expired, critical, warning, ok), the domain's age, registry status codes (transfer lock, redemption, pending delete...), nameservers and DNSSEC. We remove duplicates for you, and lookups that fail are free.

We read the data straight from each registry's official **RDAP** server (the modern, structured replacement for WHOIS), found through IANA's public RDAP directory. We don't need API keys, proxies or logins, and we don't scrape any website. A list of 100+ domains usually finishes in a few seconds.

Built and maintained by **Kestrelwick Studios**.

### Who it's for

- **Agencies and IT teams** managing dozens or hundreds of client domains who don't want a site to go dark because a renewal slipped.
- **Domain investors** tracking expiry dates, drops (redemption / pending delete) and whether names are still unregistered.
- **Security, fraud and KYB teams** checking domain age and registrar on vendor, lead or phishing lists. A domain registered 12 days ago tells you a lot.
- **SEO and M\&A due diligence** confirming who holds a domain, how long it has existed and whether it is transfer-locked.

### What we return

| Field | Example | Meaning |
|---|---|---|
| `input` | `https://www.bbc.co.uk/news` | The entry as you sent it |
| `domain` | `bbc.co.uk` | The registrable domain we looked up (subdomains, URLs and e-mail addresses are reduced to it) |
| `status` | `registered` | `registered`, `not_registered` (the registry has no record, so it's likely available), `unsupported_tld`, `invalid_input` or `error` |
| `registrar` | `MarkMonitor Inc.` | Sponsoring registrar |
| `registrarIanaId` | `292` | The registrar's IANA ID (gTLDs and some ccTLDs) |
| `createdAt` | `1997-09-15T04:00:00.000Z` | Registration date |
| `updatedAt` | `2019-09-09T15:39:04.000Z` | Last change at the registry |
| `expiresAt` | `2028-09-14T04:00:00.000Z` | Expiry date |
| `daysToExpiry` | `707` | Whole days until expiry (negative once expired) |
| `ageDays` | `10614` | Days since registration |
| `alert` | `ok` | `expired` (past expiry, or in redemption / pending delete), `critical` (under your critical threshold, default 30 days), `warning` (under your warning threshold, default 90 days), `ok`, `unknown` (the registry doesn't publish an expiry date) or `available` (not registered) |
| `statusCodes` | `["client transfer prohibited", ...]` | EPP status codes from the registry |
| `transferLocked` | `true` | A transfer-prohibited lock is set |
| `nameservers` | `["ns1.google.com", ...]` | Delegated nameservers |
| `dnssec` | `false` | Whether the delegation is DNSSEC-signed (`null` if the registry doesn't say) |
| `rdapServer` | `rdap.verisign.com` | The registry server that answered |
| `error` | `null` | Why a lookup failed, e.g. `.es has no public RDAP server` or `Timed out after 15s` |
| `checkedAt` | `2026-10-07T21:35:02.000Z` | When we checked |

Use the **Expiry alerts** view in the dataset to see just domain, alert, days to expiry, expiry date and registrar.

### Input

- **Domains**: one per line. Plain domains (`example.com`), full URLs (`https://shop.example.co.uk/cart`) and e-mail addresses (`jane@example.org`) all work, and international domains like `münchen.de` are converted for you. You can paste a whole spreadsheet column; we also split entries on commas and semicolons.
- **Warning threshold (days)**: defaults to 90.
- **Critical threshold (days)**: defaults to 30.
- **Advanced**: parallel lookups (default 10), parallel lookups per registry (default 3, to stay inside registry rate limits), timeout per lookup and a **max domains** cap to control spend.

```json
{ "domains": ["google.com", "https://www.bbc.co.uk/news", "github.io", "lemonde.fr", "jane@python.org"],
  "warningDays": 90, "criticalDays": 30 }
```

### Output example

```json
{ "input": "google.com", "domain": "google.com", "status": "registered", "registrar": "MarkMonitor Inc.",
  "registrarIanaId": "292", "createdAt": "1997-09-15T04:00:00.000Z", "updatedAt": "2019-09-09T15:39:04.000Z",
  "expiresAt": "2028-09-14T04:00:00.000Z", "daysToExpiry": 707, "ageDays": 10614, "alert": "ok",
  "statusCodes": ["client delete prohibited", "client transfer prohibited", "client update prohibited",
    "server delete prohibited", "server transfer prohibited", "server update prohibited"],
  "transferLocked": true, "nameservers": ["ns1.google.com", "ns2.google.com", "ns3.google.com", "ns4.google.com"],
  "dnssec": false, "rdapServer": "rdap.verisign.com", "error": null, "checkedAt": "2026-10-07T21:35:02.000Z" }
```

```json
{ "input": "this-domain-is-not-registered-kw2026.com", "domain": "this-domain-is-not-registered-kw2026.com",
  "status": "not_registered", "alert": "available", "registrar": null, "expiresAt": null, "rdapServer": "rdap.verisign.com", "error": null }
```

A run summary (domains received, duplicates removed, invalid entries, results per status) is saved as `SUMMARY` in the run's key-value store. Export the dataset as CSV, Excel, JSON or XML, or call the actor from the Apify API, Make, Zapier or n8n. Schedule it weekly and filter on `alert` to get renewal reminders.

### Coverage

We cover every TLD listed in IANA's RDAP directory (all generic TLDs such as .com, .net, .org, .info, .xyz, .app, .dev and .ai, plus ccTLDs such as .uk, .fr, .nl, .ca, .au, .br, .in, .pl, .cz, .no and .fi). We also query the official registry servers for .io, .co, .me, .us, .sh, .ac, .de, .ch and .li, which aren't in the directory yet.
Some country registries don't run a public RDAP server yet (for example .es, .it, .eu, .be, .at, .se, .dk, .jp, .cn and .ru). Those rows come back as `unsupported_tld` and are **free**.

### Deduplication

We dedupe on the registrable domain: `www.google.com`, `GOOGLE.com`, `https://google.com/search` and `mail.google.com` become one row (the first one you sent), and you're charged once.

### Pricing

Pay per result: **$2.48 per 1,000 domains** on the Free plan, going down to **$2.25 per 1,000** on higher Apify plans. **There's no start fee, duplicates are removed before billing, and failed lookups, unsupported TLDs and invalid entries are free.** A `not_registered` answer counts as a result, since it's a definitive answer from the registry. Platform usage is included.

### Tips

- Keep warning at 90 days and critical at 30 for renewals; domain investors watching drops can filter on `alert` = `expired`.
- `transferLocked = false` on a valuable domain is worth a look: most registrars lock transfers by default.
- Set a maximum charge per run in Apify to cap spend. We stop before going over it.
- For DNS, e-mail authentication (SPF, DMARC) and SSL certificate expiry, pair this with our **Bulk Domain Health Checker**.

### FAQ

**Is this the same as WHOIS?** It's the same registration data, from the same registries, in RDAP, the structured format ICANN now requires for gTLDs. Personal contact details are redacted by registries under GDPR, so we don't return registrant names or e-mails.
**Why is `expiresAt` empty for a .de or .ch domain?** Some registries (DENIC for .de, SWITCH for .ch) don't publish expiry dates. Those rows have `alert` = `unknown`.
**Does `not_registered` guarantee I can buy the domain?** No. It means the registry has no record of it. Premium, reserved or blocked names can still be unavailable at your registrar.
**What happens with very large lists?** We pace requests per registry and retry rate-limit (HTTP 429) and server errors with backoff. If a registry still refuses, the row comes back as `error` and it's free. You can re-run just those domains.
**Support:** open an issue on the actor's Issues tab and we'll respond.

# Actor input Schema

## `domains` (type: `array`):

One domain per entry: example.com, a full URL (https://www.example.co.uk/page) or even an e-mail address. We reduce each entry to its registrable domain, so www.shop.example.com becomes example.com. You can also paste a whole column; entries are split on new lines, commas and semicolons. Duplicates are removed and only charged once.

## `warningDays` (type: `integer`):

Domains expiring within this many days get alert = warning.

## `criticalDays` (type: `integer`):

Domains expiring within this many days get alert = critical. Already expired domains, or domains in redemption or pending delete, get alert = expired.

## `concurrency` (type: `integer`):

How many domains we look up at the same time in total.

## `perRegistryConcurrency` (type: `integer`):

Cap on simultaneous requests to any single registry, so large lists stay within registry rate limits. Lower it if you see many HTTP 429 errors.

## `timeoutSecs` (type: `integer`):

How long we wait for a registry to answer before retrying. Failed lookups are free.

## `maxItems` (type: `integer`):

Cap on unique domains looked up per run, to control spend.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "wikipedia.org",
    "https://www.bbc.co.uk/news",
    "github.io",
    "python.org",
    "lemonde.fr",
    "nic.us",
    "this-domain-is-not-registered-kw2026.com",
    "not a domain"
  ],
  "warningDays": 90,
  "criticalDays": 30,
  "concurrency": 10,
  "perRegistryConcurrency": 3,
  "timeoutSecs": 15,
  "maxItems": 10000
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "google.com",
        "wikipedia.org",
        "https://www.bbc.co.uk/news",
        "github.io",
        "python.org",
        "lemonde.fr",
        "nic.us",
        "this-domain-is-not-registered-kw2026.com",
        "not a domain"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kestrelwick/domain-expiry-checker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "google.com",
        "wikipedia.org",
        "https://www.bbc.co.uk/news",
        "github.io",
        "python.org",
        "lemonde.fr",
        "nic.us",
        "this-domain-is-not-registered-kw2026.com",
        "not a domain",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("kestrelwick/domain-expiry-checker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "google.com",
    "wikipedia.org",
    "https://www.bbc.co.uk/news",
    "github.io",
    "python.org",
    "lemonde.fr",
    "nic.us",
    "this-domain-is-not-registered-kw2026.com",
    "not a domain"
  ]
}' |
apify call kestrelwick/domain-expiry-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kestrelwick/domain-expiry-checker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/63v6UPuIL0lbpTH0g/builds/IihOkVjH7GdXOyyBC/openapi.json
