# Bulk DKIM Selector Finder: Discover DKIM Keys & Providers (`sysadmin-toolbox/dkim-selector-finder`) Actor

Find which DKIM selectors a domain publishes. Tests 145+ known selector names (Microsoft 365, Google, SendGrid, Mailchimp, Mailgun, Klaviyo...) for thousands of domains and reports each key: provider, CNAME target, RSA bits, Ed25519, revoked, test mode and weak-key issues.

- **URL**: https://apify.com/sysadmin-toolbox/dkim-selector-finder.md
- **Developed by:** [SysAdmin Toolbox](https://apify.com/sysadmin-toolbox) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.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?

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 DKIM Selector Finder: Discover DKIM Keys & Providers

Find out **which DKIM selectors a domain publishes**, for one domain or ten thousand. The actor tests **145 well-known selector names** (or ~355 with the extended list) under `<selector>._domainkey.<domain>`, follows CNAMEs, and for every key it finds reports the **sending provider**, the **CNAME target**, the **key type and RSA size**, whether the key is **revoked** or in **test mode**, and any **problems**: weak 1024-bit or sub-1024-bit keys, dangling CNAMEs, malformed records, duplicate TXT records, SHA-1-only hash tags.

Use it to:

- **Inventory every service that sends mail as your domains** (Microsoft 365, Google Workspace, SendGrid, Mailchimp, HubSpot, Zendesk...), including old ones nobody remembers setting up.
- **Find weak keys to rotate**: 1024-bit RSA keys are still everywhere (even on very large domains).
- **Clean up DNS**: dangling CNAMEs, revoked keys and records that are not DKIM at all.
- **Research sending stacks** for a list of companies (sales, M\&A due diligence, MSP onboarding).

### Why this one

- **Its whole job is selector discovery.** Email-audit bundles usually test a handful of selectors as a side check. This actor tests 145+ names with provider hints, optional date-based patterns, and your own names.
- **Provider from the CNAME target, not just the name.** `s1` can be SendGrid, Atlassian or Freshworks, and `k1` can be Mailchimp or Mailgun. When the record is a CNAME, the target zone (for example `sendgrid.net`, `mcsv.net`, `onmicrosoft.com`, `dkim.amazonses.com`) decides.
- **Real key parsing.** The `p=` value is decoded as a DER public key, so the RSA size is exact (SubjectPublicKeyInfo, with PKCS#1 accepted). Ed25519 keys (RFC 8463) are recognised too.
- **Honest DNS answers.** NXDOMAIN, "name exists but no TXT", and SERVFAIL/timeouts are counted separately. Failed lookups are retried, then sent to a second public resolver, and anything still unknown is listed rather than reported as "not found".
- **Wildcard-aware.** A random selector is probed first. If `*._domainkey` answers everything, matching answers are hidden instead of being reported as 145 fake selectors. Parked domains that publish a wildcard empty key (`v=DKIM1; p=`) are recognised.
- **Bulk-friendly input.** Paste domains, email addresses or URLs. You can also paste `selector._domainkey.example.com` names, or a whole `DKIM-Signature` header (the `d=` and `s=` tags are used).

### Input

```json
{
  "domains": ["example.com", "jane@example.org", "https://www.example.net/contact", "s1._domainkey.example.io"],
  "extraSelectors": ["hs1-1234567", "mycustom2024"],
  "selectorSet": "common",
  "dateSelectors": false,
  "includeRecordText": false
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `domains` | required | Domains, emails, URLs, `selector._domainkey.domain` names or DKIM-Signature headers. One per line, or comma/space separated; `#` starts a comment. IDNs are converted. Duplicates are removed and invalid entries are listed in `SUMMARY`. Up to 10,000 per run. |
| `extraSelectors` | – | Your own selector names, tried first on every domain |
| `selectorSet` | `common` | `common` (145 names) or `extended` (~355: adds SparkPost `scphMMYY` 2015–2025, Salesforce Marketing Cloud `10dkim1`–`51dkim1`, years and rarer generic names) |
| `dateSelectors` | false | Adds `YYYYMM`, `sYYYYMM`, `YYYYMM01` and `scphMMYY` for the last 24 months (about 96 extra lookups per domain) |
| `includeRecordText` | false | Adds the raw TXT record to each selector row |
| `outputRows` | `all` | `all`, `selectors` (only keys found) or `domains` (only one summary row per domain) |
| `concurrency` | 25 | DNS lookups in flight (max 100) |
| `dnsResolver` | `cloudflare` | `cloudflare` or `google` DNS-over-HTTPS; the other one is the fallback |
| `timeoutSecs` | 5 | Per DNS request |

### Output

The dataset has **one summary row per domain** (`rowType: "domain"`) followed by **one row per selector found** (`rowType: "selector"`). The two views are **Overview** and **Selector details**.

Domain row:

```json
{
  "rowType": "domain",
  "domain": "example.com",
  "status": "WARN",
  "selectorsFound": 4,
  "selector": "selector1, selector2, google, s1",
  "provider": "Microsoft 365, Google Workspace, SendGrid",
  "keyBits": 1024,
  "weakestKey": { "selector": "google", "keyBits": 1024 },
  "strongestKeyBits": 2048,
  "revokedSelectors": [],
  "issues": [
    "WARN: selector1: CNAME target selector1-example-com._domainkey.example.onmicrosoft.com does not exist (NXDOMAIN, dangling CNAME)...",
    "WARN: google: RSA key is 1024 bits; 2048 bits is the current recommendation"
  ],
  "selectorsTested": 145,
  "nxdomain": 139,
  "noData": 2,
  "lookupErrors": 0,
  "domainkeyNode": "NOERROR",
  "wildcardDkim": false,
  "note": null
}
```

Selector row:

```json
{
  "rowType": "selector",
  "domain": "example.com",
  "selector": "s1",
  "fqdn": "s1._domainkey.example.com",
  "status": "OK",
  "recordType": "CNAME → TXT",
  "cnameTarget": "s1.domainkey.u1234567.wl123.sendgrid.net",
  "provider": "SendGrid",
  "providerSource": "cname-target",
  "v": "DKIM1",
  "keyType": "rsa",
  "keyBits": 2048,
  "revoked": false,
  "testing": false,
  "hashAlgorithms": null,
  "serviceTypes": null,
  "flags": "s",
  "keySha256": "q0u6...=",
  "ttl": 3600,
  "issues": ["INFO: t=s: i= must match d= exactly (no subdomains)"]
}
```

**Domain status:** `OK` (keys found, no warnings), `WARN`, `CRITICAL`, `NONE_FOUND` (no tested name exists), `INCOMPLETE` (none found, but some lookups failed), `ERROR` (every lookup failed), `DOMAIN_NOT_FOUND` (the domain itself is NXDOMAIN).
**Selector status:** `OK`, `WARN`, `CRITICAL`, or `REVOKED` (empty `p=`).

**Issue rules:**

- **CRITICAL**: RSA key under 1024 bits, undecodable or missing `p=`, `v=` other than DKIM1.
- **WARN**: 1024-bit RSA, RSA over 4096 bits, dangling CNAME (target is NXDOMAIN), CNAME target without TXT, more than one DKIM record at a selector, `h=` without sha256, `s=` that excludes email, a TXT that is not a DKIM record, every selector found is revoked or broken.
- **INFO**: revoked key, test mode (`t=y`), `t=s`, Ed25519 key (pair it with RSA), PKCS#1 key encoding, several selectors sharing one public key.

The key-value store record **`SUMMARY`** has status counts, provider counts across all domains, the most common selectors, the weakest keys, domains with no selector found, invalid inputs and DNS statistics.

### Providers recognised

| Provider | Selectors tried | Recognised CNAME target |
| --- | --- | --- |
| Microsoft 365 | `selector1`, `selector2` | `*.onmicrosoft.com`, `*.dkim.mail.microsoft` |
| Google Workspace / Gmail | `google`, `20161025`, `20210112`, `20221208`, `20230601` | – (TXT) |
| Mailchimp / Mandrill | `k1`, `k2`, `k3`, `mandrill`, `mte1`, `mte2` | `mcsv.net`, `mandrillapp.com` |
| SendGrid | `s1`, `s2`, `smtpapi`, `sendgrid` | `sendgrid.net` |
| Mailgun | `mx`, `k1`, `krs`, `pic`, `smtp`, `mailo`, `pdk1`, `pdk2`, `mg` | `mailgun.org` |
| Amazon SES | (hashed names, see limits) | `dkim.amazonses.com` |
| Zoho Mail | `zoho`, `zmail`, `zohomail` | `zoho.*` |
| Salesforce / Marketing Cloud / Pardot | `sf1`, `sf2`, `sf`, `200608`, `pardot` (+ `10dkim1`–`51dkim1` extended) | `custdkim.salesforce.com`, `exacttarget.com` |
| HubSpot | `hs1`, `hs2`, `hubspot` | `hubspotemail.net` |
| Postmark | `pm` | `mtasv.net` |
| SparkPost (Bird) | `scph` (+ `scphMMYY` extended / date) | `sparkpostmail.com` |
| Klaviyo | `kl`, `kl1`, `kl2`, `km1`–`km4`, `kt1`, `kt2`, `ks1`, `ks2` | `klaviyo.com` |
| Yahoo / AOL | `s1024`, `s2048` | – |
| Fastmail | `fm1`, `fm2`, `fm3`, `mesmtp` | `fmhosted.com` |
| Proton Mail | `protonmail`, `protonmail2`, `protonmail3` | `proton.ch` |
| Zendesk, Freshdesk, Help Scout, Intercom, Gorgias | `zendesk1`–`3`, `fd`, `fd2`, `fdm`, `strong1`, `strong2`, `intercom`, `gor`, `gor2` | `zendesk.com`, `freshpo.com`, `helpscout.net`, `intercom-mail.com` |
| Constant Contact, Brevo, Mailjet, MailerLite/MailerSend, ActiveCampaign, Campaign Monitor, Kit, AWeber, Marketo, Everlytic, Emma, Sailthru, Resend, Elastic Email | `ctct1`, `ctct2`, `mail`, `brevo1`, `brevo2`, `mailjet`, `litesrv`, `ml`, `ms1`, `ms2`, `mlsend2`, `acdkim1`, `acdkim2`, `dk`, `cm`, `cka`, `aweber_key_a`–`c`, `m1`, `m2`, `everlytickey1`, `everlytickey2`, `e2ma-k1`–`k3`, `slt`, `resend`, `api` | `constantcontact.com`, `sendinblue.com`, `mailjet.com`, `mlsend.com`, `acems*.com`, `aweber.com` |
| Mimecast, Proofpoint, Cisco, Hornetsecurity | `mimecast`, `mc1`–`mc3`, `pphosted`, `ces1`, `ces2`, `hse1`, `hse2` | `mimecast.com`, `pphosted.com`, `hornetsecurity.com` |
| Mailbox hosts | `sig1` (iCloud+), `secureserver1/2` (GoDaddy), `privateemail` (Namecheap), `s1-ionos`, `s2-ionos`, `hostingermail-a/b/c`, `x` (MXroute/DirectAdmin), `default` (cPanel/Plesk) | `icloudmailadmin.com`, `secureserver.net` |
| Generic | `dkim`, `dkim1`, `dkim2`, `mail`, `email`, `smtp`, `key1`, `key2`, `s3`, `rsa`, ... | – |

Many names are shared between services, so a hint from the name alone is shown with `providerSource: "selector-name"`, and a CNAME match with `providerSource: "cname-target"`.

### Limits (please read)

- **DKIM selectors cannot be enumerated.** DNS has no way to list the names under `_domainkey`, so any selector finder can only test names it knows. `NONE_FOUND` means *none of the tested names exist*, not that the domain has no DKIM. To test the exact selector, take the `s=` tag from a `DKIM-Signature` header of a real message and put it in `extraSelectors` (or paste the header into `domains`).
- **Amazon SES Easy DKIM** uses three random 32-character selectors, and **HubSpot** adds an account number (`hs1-1234567`). Postmark's dated selectors (`20240115103045pm`), Mimecast's dated ones and other custom or per-account names can't be guessed. They are only found when you supply them in `extraSelectors`.
- `domainkeyNode` is the DNS answer for `_domainkey.<domain>` itself. NXDOMAIN there suggests nothing exists below it (RFC 8020), but some DNS servers don't follow that rule, so it is shown as a hint and not used to skip lookups.
- Lookups go through public DNS-over-HTTPS resolvers (Cloudflare, Google), so results reflect public DNS. Internal or split-horizon zones are not visible.
- Only DNS is queried. No mail is sent and no servers are contacted beyond the resolvers.

### Pricing

Pay per event: **$2 per 1,000 domains** ($0.002 per domain checked, whatever the selector list size). Invalid and duplicate inputs aren't charged.

### FAQ

**Why does a domain show "none found" when its mail passes DKIM?**
Its active selector is a name that isn't in the list (common with Amazon SES, HubSpot and self-hosted signers). Look at a received message's `DKIM-Signature` header, find `s=`, and add that name to `extraSelectors`.

**Microsoft 365 shows a dangling `selector1` or `selector2` CNAME. Is that broken?**
Not necessarily. Microsoft 365 publishes two selectors and often only the active one has a key behind it. It becomes a problem only if that selector is the one signing, or if DKIM is turned off and the records are leftovers.

**Is a 1024-bit key a problem?**
1024-bit RSA still verifies everywhere, but 2048 bits is the current recommendation (Microsoft 365, Google Workspace and most ESPs support it). Keys under 1024 bits are ignored by receivers (RFC 8301), so they are rated CRITICAL.

**What does REVOKED mean?**
The record has an empty `p=`, which is how a retired key is withdrawn. It is normal after a rotation. If every selector found is revoked, the active one simply wasn't tested.

**Can I schedule it?**
Yes. Save your domain list as a task and add a schedule, for example monthly, to catch new senders and weak keys.

### Related

- [Bulk SPF / DMARC / MX Auditor](https://apify.com/sysadmin-toolbox/email-security-audit): SPF, DMARC and MX checks for many domains.
- [Microsoft 365 Tenant Finder](https://apify.com/sysadmin-toolbox/m365-tenant-finder): tenant ID and identity provider for company domains.
- [Bulk SSL Certificate Expiry Monitor](https://apify.com/sysadmin-toolbox/bulk-ssl-expiry-monitor): certificate expiry on any port, including STARTTLS mail servers.

# Actor input Schema

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

Domains to check. One per line (comma- or space-separated lists work too). Email addresses and URLs are reduced to their domain (user@example.com, https://www.example.com/page). You can also paste selector.\_domainkey.example.com or a DKIM-Signature header (d= and s= tags) to test that exact selector. Duplicates are removed; invalid entries are listed in SUMMARY. Max 10,000 domains per run.

## `extraSelectors` (type: `array`):

Your own selector names, tried on every domain first (for example hs1-123456, 20240115123456pm or a custom name from the s= tag of a DKIM-Signature header). Full names like s1.\_domainkey.example.com are accepted and trimmed to the selector.

## `selectorSet` (type: `string`):

Which built-in list to test. Every domain costs the same; extended just makes more DNS lookups per domain.

## `dateSelectors` (type: `boolean`):

Adds YYYYMM, sYYYYMM, YYYYMM01 and SparkPost scphMMYY for each of the last 24 months (about 96 extra lookups per domain). Useful for self-hosted signers that rotate monthly.

## `includeRecordText` (type: `boolean`):

Adds the full DKIM TXT record (including the p= key) to each selector row.

## `outputRows` (type: `string`):

The dataset holds one summary row per domain (rowType = domain) followed by one row per selector found (rowType = selector).

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

DNS-over-HTTPS lookups in flight at once across all domains.

## `dnsResolver` (type: `string`):

Public resolver used for lookups. SERVFAIL, timeouts and HTTP errors are retried and then sent to the other resolver.

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

Per DNS-over-HTTPS request.

## Actor input object example

```json
{
  "domains": [
    "microsoft.com",
    "github.com",
    "mailchimp.com"
  ],
  "selectorSet": "common",
  "dateSelectors": false,
  "includeRecordText": false,
  "outputRows": "all",
  "concurrency": 25,
  "dnsResolver": "cloudflare",
  "timeoutSecs": 5
}
```

# 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": [
        "microsoft.com",
        "github.com",
        "mailchimp.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sysadmin-toolbox/dkim-selector-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": [
        "microsoft.com",
        "github.com",
        "mailchimp.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sysadmin-toolbox/dkim-selector-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": [
    "microsoft.com",
    "github.com",
    "mailchimp.com"
  ]
}' |
apify call sysadmin-toolbox/dkim-selector-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sysadmin-toolbox/dkim-selector-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/m6hDUaNGQdtK8Ch8n/builds/vCDxsDrYVQxCeopD2/openapi.json
