# crt.sh Subdomain Finder - Certificate Search (`dataio/crtsh-subdomain-finder`) Actor

Find every subdomain of a domain from public Certificate Transparency logs (crt.sh), one row per host: days until certificate expiry, issuer, environment inferred from the name (staging, dev, production) and first-seen date. Asset inventory, no API key.

- **URL**: https://apify.com/dataio/crtsh-subdomain-finder.md
- **Developed by:** [Tom Awake](https://apify.com/dataio) (community)
- **Categories:** Developer tools, SEO tools, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 hosts

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

### What does crt.sh Subdomain Finder do?

Every subdomain of a domain, from public Certificate Transparency logs — as
an **inventory**, not a log dump.

Since 2018 browsers reject any TLS certificate that was not published to a
public Certificate Transparency log (RFC 6962). That makes CT the most
complete public record of which hosts exist under a domain. But it is a
*log*: one entry per certificate issuance event, duplicates included,
expired certificates included, with no notion of a "host".

Measured on `shopify.com`: **8,239 log entries → 5,971 distinct
certificates → 627 hosts.** A 13:1 reduction. The 627 is the answer; the
8,239 is the raw material.

This Actor does the reduction, and computes the three things the log does
not carry: how many days until each host's certificate expires, which
environment the hostname belongs to, and when the host was first seen.

No API key. No account. Nothing to break.

***

### What makes this different from querying crt.sh yourself

Four things, each of which cost a measurement to find.

**1. One row per host, not per certificate.**
Certificates are reissued every 90 days, so a host that has existed for
three years carries a dozen of them. The inventory merges them: first seen,
last issued, certificate count, every issuer ever used, and the expiry date
that actually governs the host today.

**2. The obvious query silently returns only dead certificates.**
This is the trap at the centre of this source. On `shopify.com`:

| Query | Certificates | Of which currently valid | Hosts |
|---|---|---|---|
| `?q=shopify.com` | 5,252 | **0 (0%)** | 335 |
| `?q=shopify.com&exclude=expired` | 2,990 | 2,990 (100%) | 469 |
| Both, merged | 8,239 | 2,990 | **627** |

The two result sets share **zero certificate IDs**. crt.sh caps each query,
and the unfiltered variant returns only historical records. Query it the
obvious way and you get an inventory containing not one live host. This
Actor runs both and merges them — 627 hosts, 33% more than the better of
the two alone.

**3. Certificates are logged more than once.**
Every certificate is logged first as a pre-certificate, then as the final
certificate, and often in several logs. The duplicate entries differ *only*
by their crt.sh ID — same serial, same issuer, same dates, same names.
Measured inflation: **1.52× on anthropic.com, 1.38× on shopify.com.**
Rows are deduplicated on `(issuer, serial number)`, the real identity of a
certificate, so `certificateCount` means what it says.

**4. Days, not dates.**
`daysUntilExpiry` is the column you sort on when you manage an estate. The
source gives you `2026-09-26`; the question is "how long have I got".

***

### Environment classification

Only the part of the hostname **before the searched domain** is classified.
Without that rule, every host on `grafana.com` came out as Internal,
because `grafana` is itself the name of an internal-tooling product — the
company's own name was contaminating the classification of all its hosts.
The root domain says nothing about environment; only the prefix speaks.

The prefix is split into whole tokens — by dots, hyphens and underscores,
with trailing digits stripped so `staging3` and `shell-staging-10` are
recognised — and matched against known markers. Priority runs
Internal → Staging → Development → Mail, so
`api-staging.product-internal.example.com` is classified **Internal**: the
most sensitive marker wins, not the first one.

Anything with no marker is classified **Production**. That is deliberate.
A public host with a valid certificate is production until proven
otherwise, which is both the common case and the prudent assumption for an
inventory.

Measured on `shopify.com`, 627 hosts:

| Environment | Hosts | Share |
|---|---|---|
| Production | 448 | 71% |
| Staging | 157 | 25% |
| Mail | 9 | 1% |
| Internal | 8 | 1% |
| Development | 5 | 1% |

170 of 627 hosts are outside production.

***

### What it is for

- **Asset inventory.** Enumerate what actually exists under your domains,
  including the hosts nobody remembers provisioning.
- **Renewal monitoring.** Sort by `daysUntilExpiry`. A host expiring soon
  with a high `daysSinceLastIssue` is a forgotten certificate, not a
  renewal in progress.
- **Unexpected issuance.** `issuerCount` and `issuers` show every authority
  that has ever issued for a host. A new issuer you did not authorise is
  worth a look.
- **Due diligence.** Size an acquisition target's real estate before
  signing, from public record only.
- **Competitive and market research.** Product launches and new regions
  usually get a certificate before they get an announcement.

This Actor reads a public log. It makes **no connection of any kind to the
hosts it reports** — no port scan, no probe, no DNS resolution, no
vulnerability check. Everything returned was published by a certificate
authority to a public append-only log, by design, so that it could be
audited. Point it at domains you are responsible for, or use it for
research; the usual rules about what you do with an inventory still apply.

***

### Pricing: per host, never per row

You are charged per **unique host** in the output, whatever the output
shape. The certificate mode returns several times more rows for the same
set of hosts — charging by the row would bill you repeatedly for the same
discovery. A run returning 400 certificate rows across 93 hosts is charged
93\.

Wildcard entries are charged on their base name, so `*.example.com` and
`example.com` count once.

***

### Output

#### One row per host (default)

| Field | Meaning |
|---|---|
| `hostname` | The host, or `*.domain` for a wildcard |
| `environment` | Production / Staging / Development / Internal / Mail |
| `status` | Valid / Expiring soon (≤30 days) / Expired |
| `daysUntilExpiry` | Days until the host loses coverage; negative if lapsed |
| `expiresAt` | Date the latest covering certificate expires |
| `lastIssuedAt`, `daysSinceLastIssue` | When a certificate was last issued for this host |
| `firstSeenAt` | Earliest issuance ever logged — the host's age |
| `certificateCount` | Distinct certificates, after deduplication |
| `currentIssuer`, `issuers`, `issuerCount` | Current and historical authorities |
| `subdomainDepth` | 0 for the domain itself, 1 for a direct subdomain |
| `isWildcard`, `isActive` | Flags |
| `currentSerialNumber`, `crtShUrl` | Identifiers for verification |

#### One row per certificate

Adds `issuedAt`, `validityDays` (90 signals automated Let's Encrypt, 398 a
commercial certificate placed by hand), `certificateId`, `issuerFullName`,
and `coveredNames` — every name on the same certificate, which is where
hosts you were not looking for tend to appear.

Three dataset views ship with the Actor: **Inventory**, **Renewals due**,
and **Certificates**.

The **Certificates** view shows the six fields above, so it is empty in
the default host mode — those fields only exist when there is one row per
certificate. Switch `mode` to `certificates` before using that view.

***

### Limits

Stated plainly, because they affect what you can conclude.

- **crt.sh caps a single query.** The historical half of the result is
  capped at roughly 5,000 certificates per domain, so for very large
  domains the expired set is partial. The currently-valid set is retrieved
  by a separate query and is not affected — if you need completeness,
  completeness of the *live* estate is what you get.
- **CT covers publicly-trusted certificates only.** Hosts behind a private
  CA, an internal PKI, or no TLS at all leave no trace here. An empty
  result is not proof a host does not exist.
- **A logged host is not necessarily reachable.** A certificate was issued;
  whether anything answers on that name today is a different question, and
  this Actor does not ask it — that would require contacting the host.
- **Environment classification is inferred from the name.** It is a
  heuristic over naming conventions, not ground truth. It is accurate
  enough to triage a list of 600 hosts and not accurate enough to be
  evidence.
- **crt.sh is intermittently unavailable.** It returns HTTP 502 under load;
  four consecutive 502s before success were observed while building this.
  Every request is retried up to six times with exponential backoff, which
  is why runs occasionally take a minute rather than five seconds.
- **Wildcards hide their contents.** `*.example.com` is one row and an
  unknown number of real hosts.

***

### Use crt.sh Subdomain Finder as an API

Call it from your own code with the Apify client, here in Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("DataIO/crtsh-subdomain-finder").call(run_input={
    'domains': ['shopify.com'],
    'mode': 'hosts',
    'activeOnly': True,
    'maxItems': 500,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

It also works from JavaScript, Make, Zapier, n8n, and from AI agents through the Apify MCP server.

### Other actors you might like

- [npm Downloads Scraper](https://apify.com/DataIO/npm-registry-download-trends): Package Trends & Growth
- [WordPress Plugin Scraper](https://apify.com/DataIO/wordpress-org-plugin-stats): Installs & Ratings
- [Domain Expiry Checker](https://apify.com/DataIO/rdap-domain-expiry-monitor): RDAP Registrar & Status

### FAQ

#### Is it legal to use this data?

The actor reads public data from its official source, without logging in and without bypassing any access control. What you do with the data, for example contacting people listed in it, is your responsibility under the laws that apply to you, such as GDPR in Europe.

#### Can I run it on a schedule?

Yes. Create a schedule in Apify Console, daily or weekly for example, and each run delivers a fresh dataset, which you can send by email, webhook or integration.

#### Can AI agents use it?

Yes. It is available through the Apify MCP server, and every input field is described in its input schema, so an agent can call it directly.

### Source

[crt.sh](https://crt.sh), operated by Sectigo, aggregating the public
Certificate Transparency logs. Public, no authentication, no rate limit
published. This Actor is not affiliated with Sectigo.

# Actor input Schema

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

One or more domains, for example shopify.com. Subdomains are included automatically, so there is no need to list them. URLs and leading wildcards are accepted and cleaned up. Up to 50 domains per run.

## `mode` (type: `string`):

One row per host is the inventory: every certificate for a host is merged into a single line with its expiry, issuer and history. One row per certificate keeps each certificate separate, for audit trails and issuance timelines.

## `activeOnly` (type: `boolean`):

On by default, and faster: one query instead of two. Turning it off adds the historical record, which on shopify.com raises the inventory from 469 hosts to 627 — including hosts whose certificates have lapsed, often the most interesting ones.

## `expiringWithinDays` (type: `integer`):

Keep only hosts expiring within this many days. 30 is the usual renewal window. Leave empty to keep everything.

## `environments` (type: `array`):

Filter by the environment inferred from the hostname. Leave empty for all. Staging, Development and Internal together are the shadow estate: 170 of 627 hosts on shopify.com.

## `includeWildcards` (type: `boolean`):

Wildcard entries such as \*.example.com are real inventory but cover an unknown number of hosts. 5% of rows on shopify.com.

## `includeRelatedDomains` (type: `boolean`):

A certificate often covers several unrelated domains at once. Turning this on keeps those names, which is how sister brands and shared hosting providers surface. Off by default, because it widens the result beyond what you asked for.

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

If the inventory is larger, rows closest to expiry are kept first, so truncation never removes what you were looking for.

## Actor input object example

```json
{
  "domains": [
    "shopify.com"
  ],
  "mode": "hosts",
  "activeOnly": true,
  "includeWildcards": true,
  "includeRelatedDomains": false,
  "maxItems": 1000
}
```

# Actor output Schema

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

Every row produced by the run, in JSON.

# 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": [
        "shopify.com"
    ],
    "maxItems": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("dataio/crtsh-subdomain-finder").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": ["shopify.com"],
    "maxItems": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("dataio/crtsh-subdomain-finder").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": [
    "shopify.com"
  ],
  "maxItems": 1000
}' |
apify call dataio/crtsh-subdomain-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dataio/crtsh-subdomain-finder"
        }
    }
}
```

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/NAnA9d5RhpXiGb8E0/builds/SgWjQfXXCArW1iHC6/openapi.json
