# Email Provider & MX Lookup - SPF, DMARC Checker (Bulk) (`lukehunter/email-provider-mx-lookup`) Actor

Bulk email provider lookup and MX lookup from public DNS. For a list of domains, URLs, emails or Google Maps rows it reports Google Workspace vs Microsoft 365, SPF and DMARC checker results, security gateways and lead signals for $0.004/domain.

- **URL**: https://apify.com/lukehunter/email-provider-mx-lookup.md
- **Developed by:** [Luke Hunter](https://apify.com/lukehunter) (community)
- **Categories:** Lead generation, Business
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.00 / 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.

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

## Email Provider & MX Lookup - SPF, DMARC Checker (Bulk)

**For sales teams, lead-gen agencies, cold-email operators, RevOps and security consultants who need to know how a list of companies handles email.** Paste domains, website URLs, email addresses or a Google Maps / company dataset and get one row per domain: the **email provider** (Google Workspace, Microsoft 365, Zoho, Proton, Fastmail, Mimecast, Proofpoint and more), the full **MX lookup**, an **SPF DMARC checker** verdict, BIMI and MTA-STS, the SaaS tools the company has verified its domain with, and ready-made lead signals.

**$0.004 per domain with a DNS answer. 1,000 domains = $4. NXDOMAIN and failed lookups are free.**

It is a **bulk email provider lookup** built on public **DNS over HTTPS** only (Google Public DNS, with Cloudflare as fallback). It never opens an SMTP connection, never probes a mailbox and never checks whether an address exists: that would be invasive, so it is not in the product. No scraping, no login, no proxy, no API key.

### What you get

| Field group | What it tells you |
|---|---|
| `mxRecords`, `acceptsEmail`, `hasNullMx` | The MX lookup (priority and host), and whether the domain accepts email at all (MX, or the A/AAAA fallback; a null MX means "no email here"). |
| `emailProvider`, `emailProviders` | Provider inferred from the MX hosts: Google Workspace, Microsoft 365, Zoho Mail, Proton Mail, Fastmail, Mimecast, Proofpoint, Barracuda, IONOS, OVH, GoDaddy, Yandex, Amazon SES / WorkMail and others, else `Self-hosted / other`. |
| `securityGateway`, `securityGatewayVendor`, `underlyingProvider` | A filtering gateway (Mimecast, Proofpoint, Barracuda, Cisco, Trend Micro...) in front of the mailboxes, and the mailbox provider behind it when SPF reveals one. |
| `spfPresent`, `spfRecord`, `spfIncludes`, `spfProviders`, `spfAll` | The SPF record, its includes, the known senders in it (SendGrid, Mailchimp, Mandrill, HubSpot, Salesforce, Amazon SES, Mailgun, Postmark, Brevo, Klaviyo, Zendesk, Freshdesk...) and the closing `-all` / `~all` / `?all` / `+all`. |
| `dmarcPresent`, `dmarcPolicy`, `dmarcPct`, `dmarcHasRua` | DMARC policy (`none`, `quarantine`, `reject`), `pct`, and whether aggregate reports are requested. The report address itself is never output. |
| `bimiPresent`, `mtaStsPresent` | Whether the domain publishes BIMI and MTA-STS records. |
| `verificationTxt` | Vendors the company verified its domain with (Google, Microsoft, Atlassian, DocuSign, Stripe, Apple, Facebook...). Vendor names only, a tech-stack signal; token values are never output. |
| `leadSignals` | `usesGoogleWorkspace`, `usesMicrosoft365`, `sendsMarketingEmail`, `hasHelpdesk`, `emailSecurityMaturity` (`none`, `basic`, `strong`). |

Input is forgiving: `https://www.Acme.co.uk/contact?x=1`, `Jo@Acme.co.uk` and `acme.co.uk` all become `acme.co.uk`; international names such as `bücher.de` are converted to punycode; duplicates are merged.

### Quick start

```json
{
  "domains": ["apify.com", "shopify.com", "stripe.com"],
  "maxItems": 3
}
```

### Recipes

**Find which leads use Google Workspace vs Microsoft 365**

```json
{
  "domains": ["acme-example.com", "https://www.globex-example.org/about", "sales@initech-example.net"],
  "maxItems": 500
}
```

Filter the dataset on `leadSignals.usesGoogleWorkspace` or `leadSignals.usesMicrosoft365`, or group by `emailProvider`. Companies behind a Mimecast or Proofpoint gateway still show their mailbox provider in `underlyingProvider` when their SPF record names it.

**Qualify a Google Maps lead list by email provider**

Run your Google Maps scraper first, then pass its default dataset ID:

```json
{
  "datasetId": "<default dataset ID of your Google Maps run>",
  "maxItems": 2000
}
```

Or paste rows directly: each row's `website` (then `domain`, `url`, or `email`) field is used.

```json
{
  "rows": [
    { "title": "Acme Dental", "website": "https://www.acme-example.com/" },
    { "title": "Globex Plumbing", "email": "info@globex-example.org" }
  ]
}
```

Rows with only a free-mail address (gmail.com, outlook.com...) are skipped, free, since that says nothing about the business. Keep rows where `acceptsEmail` is `true` and the provider matches the pitch; drop `hasNullMx` and `nxdomain` rows before you send anything.

**Monitor DMARC policy and provider changes**

```json
{
  "domains": ["acme-example.com", "globex-example.org"],
  "onlyChangedSinceLastRun": true,
  "slackWebhookUrl": "https://hooks.slack.com/services/<T>/<B>/<token>"
}
```

Save it as a Task and add a weekly Schedule. The first run records every domain as the baseline. Later runs deliver, charge and notify only domains that are new to the watch or whose **email provider** or **DMARC policy** changed (a company moving from Google Workspace to Microsoft 365, a `p=none` that became `p=reject`, a domain that dropped DMARC). Each changed row carries `changeType: "changed"` and `changes` such as `{"field": "emailProvider", "from": "Google Workspace", "to": "Microsoft 365"}`. A migration is a buying signal for email security, migration and backup vendors.

### Alerts: Discord, Telegram, Slack, Teams, Google Chat, phone push or any app

Add one or more destinations in the **Notifications (optional)** section and each run sends the domains it delivered. It only reads public DNS and notifies you: it never emails anyone.

| Channel | Fill in | What you get |
|---|---|---|
| Discord | `discordWebhookUrl` | One card per domain, 10 per message |
| Telegram | `telegramBotToken` + `telegramChatId` | Up to 10 domains per message, one line each |
| Slack | `slackWebhookUrl` | Block Kit message, up to 10 domains per message |
| Mattermost, Rocket.Chat | `slackCompatibleWebhookUrl` | A Markdown list |
| Microsoft Teams | `teamsWebhookUrl` | Adaptive Card from a Teams Workflows webhook |
| Google Chat | `googleChatWebhookUrl` | Card in a space |
| ntfy (phone push, no account) | `ntfyTopic` (optional `ntfyServer`, `ntfyToken`) | A push per domain, up to 10, then one "...and N more" |
| Pushover (phone push) | `pushoverUserKey` + `pushoverAppToken` | A push per domain, up to 10, then one "...and N more" |
| Zapier, Make, n8n, Pipedream | `webhookUrl` | One JSON POST per 100 delivered rows with the full dataset rows |

A change message reads like `Email setup changed: acme-example.com`, with `email provider: Google Workspace → Microsoft 365; DMARC: none → reject`. Messages carry vendor names, policies and booleans only, never an SPF or TXT value. A failing channel never fails the run; notifications are free. `maxNotifications` (default 25, max 100) caps chat and phone messages per run.

### Input

| Field | Default | What it does |
|---|---:|---|
| `domains` | `[]` | Domains, URLs or email addresses. |
| `rows` | `[]` | Dataset-style objects; the first usable `website`, `domain`, `url`... field (else `email`) is used. |
| `datasetId` | none | Read rows from an Apify dataset in your account (up to 50,000 rows), e.g. a Google Maps run. |
| `maxItems` | `100` | Hard cap on domains looked up and charged, 1 to 10,000. |
| `onlyChangedSinceLastRun` | `false` | Monitor mode: deliver only new or changed domains. |
| `stateStoreName` | `domain-email-provider-lookup-state` | Named store holding the monitor memory. Use a different name per watch list. |
| notification fields | none | See the alerts table. |

At least one of `domains`, `rows` or `datasetId` is required; an input with nothing usable stops with a clear error.

### Example output

```json
{
  "domain": "acme-example.com",
  "input": "https://www.acme-example.com/pricing",
  "status": "ok",
  "error": null,
  "warnings": [],
  "acceptsEmail": true,
  "hasNullMx": false,
  "mxRecords": [
    { "priority": 1, "host": "aspmx.l.google.com" },
    { "priority": 5, "host": "alt1.aspmx.l.google.com" }
  ],
  "emailProvider": "Google Workspace",
  "emailProviders": ["Google Workspace"],
  "securityGateway": false,
  "securityGatewayVendor": null,
  "underlyingProvider": null,
  "spfPresent": true,
  "spfRecord": "v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net ~all",
  "spfIncludes": ["_spf.google.com", "sendgrid.net", "servers.mcsv.net"],
  "spfProviders": ["Google Workspace", "SendGrid", "Mailchimp"],
  "spfAll": "~all",
  "spfMultipleRecords": false,
  "dmarcPresent": true,
  "dmarcPolicy": "quarantine",
  "dmarcSubdomainPolicy": null,
  "dmarcPct": 100,
  "dmarcHasRua": true,
  "bimiPresent": false,
  "mtaStsPresent": false,
  "verificationTxt": ["Google", "Atlassian", "Facebook"],
  "leadSignals": {
    "usesGoogleWorkspace": true,
    "usesMicrosoft365": false,
    "sendsMarketingEmail": true,
    "hasHelpdesk": false,
    "emailSecurityMaturity": "strong"
  },
  "changeType": null,
  "changes": null,
  "checkedAt": "2026-10-10T12:00:00.000Z"
}
```

This sample is illustrative (a reserved-style example domain). Every row has the same fields; anything not applicable is `null` or empty, never guessed.

#### Status values

| `status` | Meaning | Charged |
|---|---|---|
| `ok` | The required DNS lookups (MX, TXT, `_dmarc`) got an answer. This includes a domain that exists but has no email records at all (`acceptsEmail: false`). | Yes |
| `nxdomain` | The domain does not exist. | No |
| `dns_error` | Neither resolver gave a usable answer (outage, SERVFAIL, timeout). Safe to run again. | No |
| `invalid` | The entry was not a domain, URL or email. | No |
| `skipped` | A free-mail address such as gmail.com taken from an email field. | No |

#### How the provider and security ratings are decided

- **Provider** is read from the most preferred MX group (lowest priority number) by matching the host's domain suffix against known providers. A secondary MX does not override it; every provider seen is listed in `emailProviders`. An unrecognised host is `Self-hosted / other`.
- **Gateway:** when Mimecast, Proofpoint, Barracuda or a similar filter is the preferred MX it is reported as the provider with `securityGateway: true`. `underlyingProvider` comes from the SPF record when it names Google Workspace, Microsoft 365 or Zoho; `usesGoogleWorkspace` and `usesMicrosoft365` honour it.
- **`emailSecurityMaturity`:** `none` = no SPF and no DMARC; `strong` = SPF ending `-all` or `~all` and DMARC `quarantine` or `reject` at `pct` 100; everything else is `basic`.
- **`sendsMarketingEmail`** is true when SPF names a marketing platform (Mailchimp, HubSpot, Klaviyo, Brevo, Constant Contact, Marketo, ActiveCampaign...). Transactional senders (SendGrid, Mailgun, Postmark, SES) are listed in `spfProviders` but do not count. **`hasHelpdesk`** is true for Zendesk, Freshdesk, Help Scout, Intercom or Atlassian in SPF.

### Use with AI agents (MCP)

This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through [Apify's MCP server](https://mcp.apify.com). Only a list of domains is needed, and each input property description is short and carries an example, so agents fill it in correctly.

**Claude Desktop** (`claude_desktop_config.json`), with your own token from Apify Console > Settings > API & Integrations:

```json
{
  "mcpServers": {
    "apify-email-provider-lookup": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.apify.com?tools=lukehunter/email-provider-mx-lookup",
        "--header", "Authorization: Bearer ${APIFY_TOKEN}"
      ],
      "env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }
    }
  }
}
```

Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL `https://mcp.apify.com?tools=lukehunter/email-provider-mx-lookup` and sign in to Apify. Never paste a real token into a shared file or chat.

**Example.** You ask: "Which of these ten prospects run on Microsoft 365, and do any have no DMARC?" The agent calls the tool `lukehunter--email-provider-mx-lookup` with:

```json
{
  "domains": ["acme-example.com", "globex-example.org"],
  "maxItems": 10
}
```

It is charged $0.004 per domain with a DNS answer, capped by `maxItems`.

### Pricing

Pay-per-event: **$0.004 per domain** whose DNS lookups succeeded (`status: ok`). NXDOMAIN, DNS errors, unusable input and free-mail skips are delivered in the dataset for transparency and are never charged. In monitor mode only new or changed domains are delivered and charged; unchanged domains cost nothing.

| Domains delivered | Cost |
|---:|---:|
| 3 | $0.012 |
| 100 (default cap) | $0.40 |
| 1,000 | $4.00 |
| 10,000 (maximum) | $40.00 |

`maxItems` (hard maximum 10,000) and your Apify per-run spending limit both stop delivery before the cap is passed. Price changes follow Apify's notice rules; the Pricing tab always shows the rate you are charged.

### Limitations

- **Public DNS only.** Results reflect the records the domain publishes today, through Google Public DNS (Cloudflare if Google fails). The Actor never connects to a mail server, so it cannot say whether a mailbox exists, whether mail would be accepted or delivered, or what a company's spam filter does.
- **The provider is an inference** from MX hostnames. A company can host mail with a vendor under its own MX names (shown as `Self-hosted / other`), or forward mail elsewhere. Treat `emailProvider` as a segmentation signal, not an audit.
- **Subdomains are looked up as given.** `mail.acme.com` is checked as `mail.acme.com`; there is no public-suffix reduction to a parent domain. Pass the company's main domain.
- **SPF is read, not evaluated.** Includes are not expanded recursively, so `spfProviders` covers what the record names directly (plus `redirect=`), not senders hidden two levels down. `spfIncludes` and `spfRecord` show exactly what was published.
- **Malformed DNS is reported, not hidden.** Several SPF or DMARC records (invalid by RFC), bad MX data and truncated resolver answers add entries to `warnings`. Several DMARC records count as no DMARC, as receivers treat them.
- **Verification tokens are never output.** Only the vendor names for TXT records like `google-site-verification=` are reported.
- **Politeness.** About 10 queries in flight at once, with retry and backoff on 429 and 5xx. A run stops after 40 minutes and says how many domains were not looked up.
- **Not affiliated** with Google, Microsoft or any provider named here. Use results in line with applicable law (for outreach, GDPR, PECR, CAN-SPAM); this Actor does not send email.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~email-provider-mx-lookup/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"domains": ["apify.com", "shopify.com", "stripe.com"], "maxItems": 3}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~email-provider-mx-lookup').call({
  "domains": [
    "apify.com",
    "shopify.com",
    "stripe.com"
  ],
  "maxItems": 3
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~email-provider-mx-lookup').call(run_input={"domains": ["apify.com", "shopify.com", "stripe.com"], "maxItems": 3})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Only new items each run:** set `"onlyChangedSinceLastRun": true` on a scheduled Task and every run after the first delivers, and charges for, only items it hasn't delivered before for that same input. That makes it a ready-made change feed.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

**Do you connect to the mail server or check if an address exists?** No, never. Only public DNS records are read.

**Why do I get a row for a domain I wasn't charged for?** Rows with `nxdomain`, `dns_error`, `invalid` or `skipped` are free. Filter on `status: ok` to see only charged rows.

**What does `Self-hosted / other` mean?** The MX hosts are not one of the known providers: a company's own server, a regional host, or a provider not in the list.

**Why is a gateway like Mimecast the provider?** It is the preferred MX, so it is what a sender reaches first. Check `underlyingProvider` and the `leadSignals` for the mailbox vendor behind it.

**Can I use my Google Maps results?** Yes: pass the run's dataset ID as `datasetId`, or paste rows with a `website` field.

**How do I get alerts when a provider changes?** Set `onlyChangedSinceLastRun`, add a destination and a weekly Schedule. See the DMARC monitoring recipe.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[French Company Leads](https://apify.com/lukehunter/french-companies-sirene)**: French companies (entreprises) from the official Sirene-based API, filtered by NAF code, department, headcount and creation date, with SIREN/SIRET, officers and new-company alerts.
- **[Poland KRS Company Lookup](https://apify.com/lukehunter/poland-krs-company-lookup)**: Polish companies from the official KRS API by KRS number: NIP, REGON, address, share capital, PKD and the board as published, with optional VAT status.
- **[Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper)**: emails, phone numbers and social profile links from any list of company domains, with the page each came from and a changed-contacts-only mode for scheduled runs.
- **[Impressum Scraper](https://apify.com/lukehunter/impressum-scraper)**: legal entity data from the imprint of any company website in Germany, Austria and Switzerland: legal name and form, managing directors, HRB/HRA or FN register, VAT id and address, with a changed-data-only mode for scheduled runs.
- **[Website Tech Stack Detector](https://apify.com/lukehunter/website-tech-stack-detector)**: the technologies a website runs (CMS, ecommerce platform, analytics, ad pixels, chat, payments, CDN, frameworks) from its home page, with the evidence for each match and a changed-technologies-only mode for scheduled runs.
- **[Google Maps Email Extractor & Lead Enricher](https://apify.com/lukehunter/lead-enricher)**: emails, phones, social links, named people and the tech stack of every business in your Google Maps Scraper results, one enriched row per business, with filters such as only Shopify stores with an email.
- **[Federal Register Rule Effective-Date Radar](https://apify.com/lukehunter/federal-rule-effective-date-radar)**: final federal rules about to take effect, with citations, dockets and urgency.
- **[UK Tenders Monitor](https://apify.com/lukehunter/uk-tenders-monitor)**: new UK public-sector tenders from Contracts Finder and Find a Tender, filtered by CPV code, value, region and buyer, with Slack, Discord and Telegram alerts.

# Actor input Schema

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

Domains to check. Also accepts URLs and email addresses; scheme, www, paths and the part before @ are stripped, and non-ASCII names are converted to punycode. Example: \["apify.com", "https://www.shopify.com/pricing", "sales@stripe.com"]. Duplicates are merged.

## `rows` (type: `array`):

Rows from a Google Maps or company dataset: objects with a website, domain, url or email field. The first usable one per row is used. Example: \[{"title": "Acme", "website": "https://acme.com"}, {"email": "info@example.org"}]. Free-mail addresses such as gmail.com are skipped.

## `datasetId` (type: `string`):

Read rows from an Apify dataset in your account instead of pasting them, for example the default dataset ID of a Google Maps scraper run. Rows need a website, domain or email field, e.g. Wa5gtYQ2FkBpHnBMs. Up to 50,000 rows are read; maxItems still applies.

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

Hard cap on domains looked up and charged, 1 to 10000, e.g. 100. At $0.004 per domain with a DNS answer, 100 domains cost at most $0.40. Domains beyond the cap are not looked up or charged. NXDOMAIN and failed lookups are never charged.

## `onlyChangedSinceLastRun` (type: `boolean`):

Deliver and charge only domains that are new to the watch or whose email provider or DMARC policy changed since the last run, e.g. on a weekly Schedule. The first run delivers every domain as the baseline. Pair with a Slack, Discord or Telegram destination below.

## `stateStoreName` (type: `string`):

Named key-value store that remembers each domain's provider and DMARC state between runs, used only when monitoring is on. Example: my-lead-domains. Use a different name to keep separate watch lists. Letters, numbers and hyphens only.

## `discordWebhookUrl` (type: `string`):

Post results to a Discord channel. In Discord: Server Settings -> Integrations -> Webhooks -> New Webhook -> choose the channel -> Copy Webhook URL. Example: https://discord.com/api/webhooks/<id>/<token>. Leave empty to skip Discord.

## `telegramBotToken` (type: `string`):

Post results to Telegram. Create a bot by chatting with @BotFather on Telegram (send /newbot) and paste the token it gives you, e.g. 123456789:AA.... Needs "Telegram chat ID" below. Leave empty to skip Telegram.

## `telegramChatId` (type: `string`):

Where the bot should post: a numeric chat id (e.g. 123456789, or -1001234567890 for a group or channel) or a public @channelusername. The bot must be added to that chat or channel first (as an administrator for a channel), and for a private chat you must press Start on the bot. Needs "Telegram bot token" above.

## `slackWebhookUrl` (type: `string`):

Post results to a Slack channel (Block Kit cards, up to 10 per message). Create an incoming webhook at api.slack.com/apps -> Create New App -> Incoming Webhooks -> Add New Webhook to Workspace. Example: https://hooks.slack.com/services/T000/B000/XXXX. Leave empty to skip Slack.

## `slackCompatibleWebhookUrl` (type: `string`):

Post results to a Mattermost or Rocket.Chat incoming webhook, which accept Slack-style webhook payloads. Must be a public https:// address, e.g. https://chat.example.com/hooks/abc123 (no localhost, private IP or .local/.internal hosts). These apps do not render Slack Block Kit, so they get a plain Markdown list. For real Slack use "Slack webhook URL" above. Leave empty to skip.

## `teamsWebhookUrl` (type: `string`):

Post results to a Teams channel as an Adaptive Card. In Teams: channel -> ... -> Workflows -> "Post to a channel when a webhook request is received" -> copy the URL. The host must end in .logic.azure.com or .powerplatform.com, e.g. https://prod-12.westeurope.logic.azure.com/workflows/... The retired Office 365 connector webhooks are not supported. Leave empty to skip Teams.

## `googleChatWebhookUrl` (type: `string`):

Post results to a Google Chat space. In the space: Apps & integrations -> Webhooks -> Add webhook -> copy the URL. Example: https://chat.googleapis.com/v1/spaces/<space>/messages?key=...\&token=.... Leave empty to skip Google Chat.

## `ntfyTopic` (type: `string`):

Get a phone push with no account via ntfy (ntfy.sh). Install the ntfy app, subscribe to a topic name, and enter the same name here. Anyone who knows the topic can read it, so pick a long, unguessable name (letters, numbers, - and \_), e.g. acme-leads-7k2x9q. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip ntfy.

## `ntfyServer` (type: `string`):

Only used with "ntfy topic". Leave as https://ntfy.sh, or enter the public https:// address of your own ntfy server, e.g. https://ntfy.example.com.

## `ntfyToken` (type: `string`):

Only used with "ntfy topic", for a self-hosted server or a reserved topic that needs a login. An access token from ntfy (starts with tk\_, e.g. tk_abc123...). Leave empty for open topics.

## `pushoverUserKey` (type: `string`):

Get a phone push via Pushover. Your user key (30 characters, e.g. uQiRzpo4DXghDmr9QzzfQu27cmVRsG) is shown on the Pushover dashboard (pushover.net). Needs "Pushover app token" below. At most 10 individual pushes per run, then one "...and N more". Leave empty to skip Pushover.

## `pushoverAppToken` (type: `string`):

The API token of a Pushover application you create at pushover.net/apps/build (name it after this Actor), e.g. azGDORePK8gMaC0QOYAMyEEuzJnyUi. Needs "Pushover user key" above.

## `webhookUrl` (type: `string`):

POST the delivered rows as JSON to any public https:// URL: {source, actor, runId, count, items}. At most 100 rows per POST; every delivered row is sent regardless of the max-notifications cap. For example, route results to email, a spreadsheet or any app. Localhost, private IP and .local/.internal addresses are rejected. Leave empty to skip.

## `maxNotifications` (type: `integer`):

Caps how many results are sent to chat and phone channels per run (Discord, Telegram, Slack, Teams, Google Chat, ntfy, Pushover), 1-100, default 25 (e.g. 10). Extra results are still in the dataset. The generic webhook is not capped. Only used when a destination above is set.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "shopify.com",
    "stripe.com"
  ],
  "maxItems": 3,
  "onlyChangedSinceLastRun": false,
  "stateStoreName": "domain-email-provider-lookup-state",
  "ntfyServer": "https://ntfy.sh",
  "maxNotifications": 25
}
```

# Actor output Schema

## `domains` (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": [
        "apify.com",
        "shopify.com",
        "stripe.com"
    ],
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/email-provider-mx-lookup").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": [
        "apify.com",
        "shopify.com",
        "stripe.com",
    ],
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/email-provider-mx-lookup").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": [
    "apify.com",
    "shopify.com",
    "stripe.com"
  ],
  "maxItems": 3
}' |
apify call lukehunter/email-provider-mx-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lukehunter/email-provider-mx-lookup"
        }
    }
}
```

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/HgRlKMadfntaoXEuu/builds/z8ajkkqEdCdp8g4ys/openapi.json
