# Confirm Domain — company name + phone → proven website (`alamr/confirm-domain`) Actor

Returns a company's domain only when the phone number from your record is found on the page — and hands back the page URL and the sentence it was found in. No proof, no charge.

- **URL**: https://apify.com/alamr/confirm-domain.md
- **Developed by:** [Danilo Dzyuban](https://apify.com/alamr) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 confirmed domains

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

## Confirm Domain

**Most enrichment APIs guess. This one proves.**

You send a company name and its phone number. You get back the domain — but only
when that phone number was actually found on the site, together with the page it was
found on and the sentence it appeared in. If there is no proof, there is no charge.

```json
{
  "input_name": "Midfield Dental Center",
  "domain": "midfielddentalcenter.com",
  "confidence": "confirmed",
  "confirmed_by": "phone_tel_link",
  "evidence": "…Call our office today at (205) 923-6828 to schedule your visit…",
  "evidence_url": "https://midfielddentalcenter.com/contact",
  "candidates_tested": 24,
  "candidates_alive": 3,
  "alternatives": ["midfielddental.com"],
  "billable": true
}
```

### Why this exists

Clearbit's Name-to-Domain API was shut down for new users on 30 April 2025. What
replaced it scores candidates by how similar a string looks to a company name. That
is a guess, and a guess is expensive downstream: you email the wrong domain, and you
pay for it with your sending reputation, not with the $0.005 the lookup cost.

A phone number is a harder anchor than a name. `smilecentre.ca` tells you nothing
about whether it belongs to Dr. Smith Dentistry. The number printed on that site does.

### Four outcomes, and you only pay for one

| `confidence` | What it means | Charged |
|---|---|---|
| `confirmed` | The phone from your record was found on the site | **yes** |
| `likely` | The company name was found, the phone was not | no |
| `ambiguous` | Live candidate domains exist, none could be proven | no |
| `not_found` | DNS answered, and no candidate domain exists | no |
| `check_failed` | DNS did not answer — the check did not happen, retry | no |

The last two used to be one outcome. They are not the same thing, and conflating
them is how a tool reports an answer it never obtained.

`confirmed_by` tells you *how* it was proven: `phone_tel_link` (a `tel:` link),
`phone_jsonld` (schema.org markup), or `phone_text` (visible page text).

### Measured, not claimed

Benchmark of 11 August 2026. **400 US and Canadian dental practices.** Ground truth
is the `website` tag from OpenStreetMap — an independent source, not this tool's own
output.

| Metric | Result |
|---|---|
| Confirmations returned | 45 of 400 records |
| Matched the independent domain exactly | **42 of 45** |
| Disagreed with it | 3 of 45 |
| Coverage — records reaching `confirmed` | **11.3%** |
| Throughput | 62 records/min |
| Proof source: `tel:` link / text / JSON-LD | 39 / 4 / 2 |

Three things you would find yourself, so they are here:

**An earlier pilot on 60 records looked much better — 18 of 19 correct, 31.7%
coverage — and it was misleading.** Sixty records do not generate enough load to
break anything. At 400 the DNS stage was answering roughly 9,600 queries, began
timing out, and the tool reported "no such domain" for records whose domains were
alive. That is fixed (see `check_failed` above), but the honest numbers are the ones
in the table, not the pilot's.

**All three disagreements are the same case, and it is arguable.** *Bischoff Family
Dentistry* resolves to `belmontdentistry.com`; *Riley Farm Dental* to
`valleyfamilydentalpractice.com`. These are practices whose own domain now redirects
to the one that absorbed or renamed them — the phone matched, the returned domain is
where the site actually lives, and the volunteer-maintained OSM tag holds the older
name. They are counted as errors above anyway, because we cannot prove that reading
for every case.

**Coverage is one record in nine.** Practices named after a person routinely sit on
an unrelated domain — *Kim L. Peters, DMD* on `northdeandental.com`, *King Dental* on
`drkingdentistry.com`. No name-based method reaches those, this one included. This is
an addition to your enrichment stack, not a replacement for it: you pay for the
fraction that can be proven, and nothing for the rest.

### Usage

#### As an Apify Actor

```json
{
  "records": [
    { "name": "Midfield Dental Center", "phone": "+1-205-923-6828", "country": "US" },
    { "name": "Grand River Dental", "phone": "+1-519-624-8001", "country": "CA" }
  ],
  "httpConcurrency": 24
}
```

Billing is pay-per-event, and there is exactly one event: `domain-confirmed`.

#### As a library

```python
from core import confirm_sync
print(confirm_sync("Midfield Dental Center", "+1-205-923-6828", "US"))
```

#### As a batch tool

```bash
python3 cli.py --in targets.jsonl --out confirmed.jsonl --country CA --only-confirmed
```

Accepts JSONL or CSV; column names `name`/`firm`/`company` and `phone`/`telephone`
are all recognised.

### How it works

1. **Candidates.** The name is folded to ASCII (so `Côté` and `Cote` are one thing),
   stripped of legal and industry filler, and expanded into up to eight slug forms —
   full, core, hyphenated, core+industry tail, initials — crossed with the TLDs that
   country actually uses.
2. **DNS first.** All candidates for the whole batch go out in one pass as raw UDP to
   four public resolvers, ~20,000 checks per minute. The system resolver serialises
   these requests — measured at 157/min, and *worse* on more threads. If UDP port 53
   is unreachable — common in cloud runners and sandboxes, and observed live after
   sustained querying — it falls back to DNS-over-HTTPS automatically. Without that
   fallback the tool would silently return `not_found` for everything, which is the
   worst possible failure: it looks like an answer.
3. **HTTP second, only on survivors.** Roughly one candidate in thirteen exists, so
   the expensive step runs on a small fraction. Home page first, then contact pages.
4. **Proof.** The number is matched on its last 10 digits, so formatting is irrelevant.
   It is looked for in `tel:` links and schema.org markup before visible text —
   that single change is what took coverage from 15% to 32%, because on many sites the
   visible number is an image while the `tel:` link is always real.
5. **Parked domains are rejected**, not returned as matches. Squatters register the
   `.net` and `.org` spellings of a name and serve a 150-character page containing
   that name — so a page is only treated as real above 400 characters of text, and
   the domain's own words are struck from the text before the name is looked for.
   Otherwise a parking page "confirms" every company whose name it was built from.

### Limits, stated up front

- A record with no phone number can never be better than `likely`.
- Sites begin rate-limiting above roughly 30 parallel requests; the default is 24,
  and during the benchmark hosts stopped answering after sustained runs.
- Only the first ~300 KB of each page is read. Contact details are never below that.
- This tool reads public web pages at ordinary browser rates and identifies itself in
  its User-Agent. It works with businesses and their websites, not with individuals.

### License

MIT.

# Actor input Schema

## `records` (type: `array`):

Companies to resolve. Each item: { "name": "...", "phone": "...", "country": "CA" }. The phone is what proves the match — without it a record can only ever come back as `likely`.

## `name` (type: `string`):

Shortcut for one-off lookups. Ignored when Records is set.

## `phone` (type: `string`):

Any format. Matched on the last 10 digits, so (905) 555-1234 and +1.905.555.1234 are the same number.

## `country` (type: `string`):

ISO-2 code. Decides which TLDs are tried: CA → .ca/.com/.net/.org, GB → .co.uk/.com, and so on. Defaults to .com/.net/.org.

## `maxRecords` (type: `integer`):

Hard cap on how many records are processed, so a bad input file cannot run up a bill.

## `httpConcurrency` (type: `integer`):

Parallel site checks. Higher is faster but ruder; sites start rate-limiting above ~30.

## Actor input object example

```json
{
  "records": [
    {
      "name": "Midfield Dental Center",
      "phone": "+1-205-923-6828",
      "country": "US"
    },
    {
      "name": "Grand River Dental",
      "phone": "+1-519-624-8001",
      "country": "CA"
    }
  ],
  "country": "US",
  "maxRecords": 0,
  "httpConcurrency": 24
}
```

# Actor output Schema

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

Every record with its confidence, the domain, and the evidence the domain was confirmed by.

## `dataset` (type: `string`):

The same results as raw JSON, for loading into your own systems.

# 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 = {
    "records": [
        {
            "name": "Midfield Dental Center",
            "phone": "+1-205-923-6828",
            "country": "US"
        },
        {
            "name": "Grand River Dental",
            "phone": "+1-519-624-8001",
            "country": "CA"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("alamr/confirm-domain").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 = { "records": [
        {
            "name": "Midfield Dental Center",
            "phone": "+1-205-923-6828",
            "country": "US",
        },
        {
            "name": "Grand River Dental",
            "phone": "+1-519-624-8001",
            "country": "CA",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("alamr/confirm-domain").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 '{
  "records": [
    {
      "name": "Midfield Dental Center",
      "phone": "+1-205-923-6828",
      "country": "US"
    },
    {
      "name": "Grand River Dental",
      "phone": "+1-519-624-8001",
      "country": "CA"
    }
  ]
}' |
apify call alamr/confirm-domain --silent --output-dataset

```

## MCP server setup

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

```

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/vz5omv4V6wAPq5aEF/builds/QbQ2GoDZuzz2ozscU/openapi.json
