# Domain & SSL Expiry Monitor (`arched_friend/domain-ssl-monitor`) Actor

Monitor domain registration and SSL certificate expiry across every site you own or manage. Days remaining, registrar, certificate issuer, trust problems and an alert level per domain, in one dataset.

- **URL**: https://apify.com/arched\_friend/domain-ssl-monitor.md
- **Developed by:** [Peach O](https://apify.com/arched_friend) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 domain checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Domain & SSL Expiry Monitor: Never Lose A Site To A Lapsed Renewal

**Know how many days are left on every domain and every certificate you are responsible for, before a customer tells you.**

An expired domain takes a site off the internet. An expired certificate puts a full page security warning in front of every visitor. Both are silent until the day they are not, and both are avoidable with one number: days remaining.

Point this at your list of domains and get that number for each one, plus the registrar holding the domain, the authority that issued the certificate, whether a transfer lock is set, and a plain language alert on anything that needs attention.

Built for agencies with client sites, IT and ops teams, and anyone holding a portfolio of domains they did not personally register.

### How it works

```mermaid
flowchart LR
    A[Your domain list] --> B[Read the host<br/>and the registrable domain]
    B --> C[Registry lookup<br/>over RDAP]
    B --> D[TLS handshake<br/>on port 443]
    C --> E[Expiry, registrar,<br/>lock, DNSSEC, nameservers]
    D --> F[Certificate expiry, issuer,<br/>hostnames, trust check]
    E --> G{Compare against<br/>your warning windows}
    F --> G
    G --> H[(One row per domain<br/>with an alert level)]
```

Two independent checks run for every entry. The registry lookup uses RDAP, the structured replacement for WHOIS that registries publish themselves, so there is no scraping and no key to supply. The certificate check opens the same TLS connection a browser opens and reads what the server actually serves, which is the only way to catch a renewal that quietly stopped working.

### What you get

One row per domain. A real row from a live run:

```json
{
  "domain": "stripe.com",
  "host": "stripe.com",
  "status": "ok",
  "alerts": [],
  "daysUntilDomainExpiry": 368,
  "daysUntilCertExpiry": 66,
  "domainExpiresAt": "2027-09-11T04:00:00.000Z",
  "registrar": "SafeNames Ltd.",
  "registrarIanaId": "447",
  "transferLocked": true,
  "dnssecEnabled": false,
  "nameservers": ["ns-1087.awsdns-07.org", "ns-423.awsdns-52.com"],
  "certIssuer": "DigiCert Inc",
  "certIssuerName": "DigiCert Global G3 TLS ECC SHA384 2020 CA1",
  "certExpiresAt": "2026-11-12T23:59:59.000Z",
  "certAltNames": ["stripe.com", "www.stripe.com"],
  "certCoversWww": true,
  "tlsProtocol": "TLSv1.3",
  "certTrusted": true,
  "certTrustError": null,
  "checkedAt": "2026-09-07T16:35:46.671Z"
}
```

And a row that needs someone to act:

```json
{
  "host": "expired.badssl.com",
  "status": "critical",
  "alerts": ["SSL certificate expired 4166 days ago"],
  "daysUntilCertExpiry": -4166,
  "certIssuer": "COMODO CA Limited",
  "certTrusted": false,
  "certTrustError": "CERT_HAS_EXPIRED"
}
```

### Alert levels

Every row carries one status, so a scheduled run sorts itself.

| Status | What it means | Examples |
| --- | --- | --- |
| `ok` | Nothing expiring inside your warning windows | Certificate renews in 66 days |
| `warning` | Something expires soon, or the setup is dated | Domain expires in 12 days, server negotiated TLS 1.1 |
| `critical` | Already broken for visitors | Certificate expired, certificate does not cover the hostname, domain lapsed |
| `error` | The check could not be completed | Host did not answer, domain is not registered |

The two warning windows are yours to set. Thirty days on the domain matches what registrars want before a renewal. Fourteen days on the certificate catches automated renewal that has stopped working, since Let's Encrypt starts renewing at thirty days and retries for a fortnight before the certificate actually lapses.

### What gets checked

```mermaid
flowchart TB
    subgraph Registry[Registry record, over RDAP]
        R1[Expiry date]
        R2[Registrar and IANA id]
        R3[Transfer lock]
        R4[DNSSEC]
        R5[Nameservers]
    end
    subgraph Certificate[Certificate, over TLS]
        C1[Expiry date]
        C2[Issuing authority]
        C3[Hostnames covered]
        C4[Trusted for this host]
        C5[Protocol version]
    end
```

The certificate check validates the full chain the way a browser does, so it catches three failures that a simple expiry date never will: a certificate that is trusted but does not cover the hostname being served, a self signed or incomplete chain, and a server still negotiating a protocol that browsers now reject.

### Example input

```json
{
  "domains": [
    "example.com",
    "https://www.clientsite.co.uk/pricing",
    "shop.anotherclient.com",
    "mail.example.com:8465"
  ],
  "domainWarningDays": 30,
  "certWarningDays": 14,
  "onlyProblems": true
}
```

A bare domain, a full URL and a subdomain all work. Add a port to check a certificate that is not served on 443. Turn on `onlyProblems` for a scheduled run so the dataset holds only what needs acting on, which makes it a working inbox rather than a report nobody opens.

### Run it from the command line

```bash
curl -X POST "https://api.apify.com/v2/acts/arched_friend~domain-ssl-monitor/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "domains": ["example.com", "clientsite.co.uk"],
    "domainWarningDays": 30,
    "certWarningDays": 14,
    "onlyProblems": true
  }'
```

### Put it on a schedule

Set the Actor to run daily and point the run at whatever you already use. Every row carries `status` and `alerts`, so a filter on `status != "ok"` is the whole integration: send those rows to Slack, open a ticket, or drop them in a sheet the account team reads. The run summary in the key value store holds the counts and the soonest expiries if you want a digest rather than rows.

### Pricing

$0.004 per domain checked. Both checks are included in that, and a domain checked once is billed once no matter how many times a lookup is retried.

| | This Actor | Certificate monitoring SaaS | Doing it by hand |
| --- | --- | --- | --- |
| 100 domains, daily | about $12 a month | $20 to $100 a month | Hours, and it stops happening by week three |
| Domain expiry included | Yes | Usually a separate product | |
| Certificate trust and hostname checks | Yes | Varies | |
| Data you own | Dataset, API, webhook | Their dashboard | |
| Setup | Paste a list | Agent or DNS records | |

### Notes and limits

Not every registry publishes the same detail, and the Actor says which rather than leaving an empty column.

- Registries with no public RDAP service, including `.de`, `.io`, `.so` and `.ch`, come back as `recordStatus: "unsupported-tld"`. The certificate check still runs normally, so those domains are monitored, just without a registration date.
- Some registries publish a record with no expiry date at all, `.nl` and `.au` among them. Those rows carry `recordStatus: "no-expiry-published"`.
- A domain the registry has never heard of comes back as `not-registered`, which is worth knowing if it was meant to be yours.
- Registries rate limit per address. The default of five domains at a time is comfortable for lists in the hundreds. Lower it if lookups start failing on a very large list.

### Related products

- **Tech Stack Checker** for what a site is built on, from the same list of domains.
- **Website Lead Extractor** for the emails, phones and social profiles on those sites.
- **Google Reviews Monitor** for what customers are saying about the businesses behind them.

# Actor input Schema

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

The domains to check, one per line. A bare domain, a full URL or a subdomain all work, so example.com, https://www.example.com/pricing and shop.example.co.uk are all accepted. Add a port such as mail.example.com:8443 to check a certificate that is not served on 443.

## `domainWarningDays` (type: `integer`):

Raise a warning when the domain registration expires within this many days. Registrars usually want 30 days of notice before a renewal, so 30 is a safe default.

## `certWarningDays` (type: `integer`):

Raise a warning when the SSL certificate expires within this many days. Let's Encrypt renews at 30 days and most automation retries for a fortnight, so 14 catches a renewal that has quietly stopped working.

## `onlyProblems` (type: `boolean`):

Drop every healthy domain and return only the rows with a warning, a critical alert or an error. Turn this on for a scheduled run so the dataset is an inbox, not a report.

## `checkRegistry` (type: `boolean`):

Look the domain up at its registry over RDAP for the expiry date, registrar, transfer lock, DNSSEC and nameservers. Turn it off to check certificates only.

## `checkCertificate` (type: `boolean`):

Open a TLS connection to the host and read the certificate it serves, including issuer, expiry, covered hostnames and any trust problem. Turn it off to check registrations only.

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

How many domains to check at once. Registries rate limit per IP address, so raise this only for large lists and lower it if lookups start failing.

## `timeoutSeconds` (type: `integer`):

How long to wait for a registry response or a TLS handshake before giving up on that domain and reporting it as unreachable.

## Actor input object example

```json
{
  "domains": [
    "example.com",
    "bbc.co.uk",
    "gymshark.com"
  ],
  "domainWarningDays": 30,
  "certWarningDays": 14,
  "onlyProblems": false,
  "checkRegistry": true,
  "checkCertificate": true,
  "concurrency": 5,
  "timeoutSeconds": 15
}
```

# Actor output Schema

## `domains` (type: `string`):

One row per domain, with days until the registration and the certificate expire, the registrar, the certificate issuer, an alert level and the alerts behind it.

## `runSummary` (type: `string`):

Counts by alert level, everything that needs attention, the soonest expiries and any domain whose registry publishes no expiry date.

# 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": [
        "example.com",
        "bbc.co.uk",
        "gymshark.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arched_friend/domain-ssl-monitor").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": [
        "example.com",
        "bbc.co.uk",
        "gymshark.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arched_friend/domain-ssl-monitor").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": [
    "example.com",
    "bbc.co.uk",
    "gymshark.com"
  ]
}' |
apify call arched_friend/domain-ssl-monitor --silent --output-dataset

```

## MCP server setup

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

```

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/dsGFqRlf8Ve6Ymetz/builds/XpAVi96sYgeEqbTcK/openapi.json
