# Business Name & Phrase to URL Resolver - Google Search (`ryanclinton/serp-name-resolver`) Actor

Resolve business names or distinctive footer phrases into deduped website URLs via Google. Used as a sub-actor by Website Contact Scraper and B2B Lead Gen Suite, or standalone for URL discovery. Country-aware TLD filtering, drops social and directory hosts. Pay-per-query, no subscription.

- **URL**: https://apify.com/ryanclinton/serp-name-resolver.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** Lead generation, Developer tools
- **Features:** Limited permissions
- **Stats:** 25 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$7.50 / 1,000 search query executeds

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

## Business Name & Phrase to URL Resolver

![Business Name & Phrase to URL Resolver](https://apifyforge.com/readme-assets/ryanclinton-serp-name-resolver/hero.png)

**Business Name & Phrase to URL Resolver is a deterministic company website resolver for Apify** that converts company names and marketing phrases into canonical domains with automation-safe trust signals. Give it a list of names (or distinctive marketing phrases) and it returns a deduped list of website URLs from Google, each scored with a confidence level so you know which results to trust before spending on downstream scraping.

This is the discovery front door of a lead pipeline. It feeds [Website Contact Scraper](https://apify.com/ryanclinton/website-contact-scraper): paste names instead of URLs and the resolver turns them into domains for the next step. It also runs standalone whenever you only need the name-to-URL step.

### What it is

The Business Name & Phrase to URL Resolver is a **deterministic company website resolver** for Apify. It is also a **pre-enrichment firewall**: a deterministic gate that decides which resolved domains are safe to send downstream, before you spend on scraping or enrichment.

- **Is:** a deterministic company website resolver, a pre-enrichment firewall, an entity-normalization step, a workflow-safe domain-discovery actor.
- **Is not:** a generic Google scraper, a contact scraper, an email verifier, an enrichment database, an LLM-powered resolver.

Compared to enrichment databases (Apollo, Clearbit, ZoomInfo) and agentic pipelines (Clay, AI SDR stacks), it focuses narrowly on one job: turning names into canonical domains with deterministic confidence, ambiguity detection, and automation-safe routing.

#### Why not use Apollo or Clay for this?

Apollo, ZoomInfo, and Clearbit are enrichment databases; Clay is an enrichment-orchestration pipeline. They start *after* the entity's identity is assumed. This actor establishes that identity first, deterministically, against Google's live index, so the wrong company never enters those tools in the first place. Use it in front of them, not instead of them.

### What makes this different

![What you also get](https://apifyforge.com/readme-assets/ryanclinton-serp-name-resolver/feature-callouts.png)

- **It scores the resolution, not just returns it.** Every domain comes with a deterministic `confidenceLevel` (high / medium / low) built from SERP rank and how well the domain matches the searched name. A generic Google scraper hands you the top result and leaves you guessing whether it's the right company.
- **It tells you what failed.** Names that resolve to nothing come back as `unresolved` records with a `failureType` and an actionable reason, never a silently shorter list. You always know your real hit rate.
- **It is built to chain.** Every record carries an `actorGraph.next` pointer to the contact scraper, so wiring the pipeline in Make, n8n, or Dify is a field read, not hard-coded plumbing.

| | This actor | A generic Google/SERP scraper |
|---|---|---|
| Deterministic confidence per result | ✅ | ❌ |
| `safeToAutomate` gate | ✅ | ❌ |
| Ambiguity detection | ✅ | ❌ |
| SERP noise analysis (confidence dampened on directory-heavy pages) | ✅ | ❌ |
| Unresolved inputs surfaced with a reason | ✅ | ❌ (silent gaps) |
| Per-result audit trail (`confidenceBreakdown`) | ✅ | ❌ |
| Pipeline-chaining metadata (`actorGraph`) | ✅ | ❌ |
| Directory/social hosts auto-excluded | ✅ | ❌ (returns them) |

### Before vs after

**Before:** you have a CRM export or a prospect list of 200 company names and no URLs. You paste them into a search box one by one, eyeball the top result, copy the domain, and hope it's the right company. An hour later you have a half-finished spreadsheet and no idea which rows are wrong.

**After:** one run returns 200 rows in a couple of minutes, each with a website URL, a confidence level, and a flag on the ones worth double-checking. You pipe the high-confidence rows straight into the contact scraper and review only the handful flagged `verifyBeforeUse`.

### What you get from one call

One row per unique discovered domain (`recordType: "domain"`), including:

- `url` — full https URL
- `domain` — bare domain
- `confidenceLevel` — `high` / `medium` / `low`: how likely this domain is the right answer
- `confidenceScore` — deterministic 0-1 score behind the level
- `safeToAutomate` — our default opinion: `true` only when confidence is high AND the result isn't ambiguous
- `policyResult` — *your* definition of safe: `{ passed, failedRules[] }` after evaluating the row against your `resolutionProfile` / `automationPolicy` (see below)
- `recommendedAction` — the routing decision in one enum: `auto-enrich` / `verify` / `retry` / `skip`
- `reasonCodes` — stable machine enums (`TOP_RANK`, `LOW_DOMAIN_MATCH`, `SERP_DIRECTORY_HEAVY`, `GENERIC_ENTITY`, ...) for branching without parsing prose
- `ambiguous` — `true` when the name had several weak candidates and the top result is likely wrong (add a `nameSuffix` to disambiguate)
- `candidateEntities` — for ambiguous names, the scored alternatives `[{ domain, confidenceScore, rank }]` so an agent can pick its own policy instead of trusting one forced result
- `candidateGap` — for ambiguous names, the separation between the top two candidates `{ topScore, secondScore, gap, weakWinner }`
- `entityFingerprint` — a portable identity record `{ entityId (sha256 of the domain), normalizedName, normalizedDomain, country }` for downstream dedup and caching
- `aliases` — other input names in the same run that resolved to this domain (the CRM-dedup signal: *Stripe Inc* and *Stripe Payments* both → `stripe.com`)
- `verifyBeforeUse` — `true` when the resolution isn't high-confidence or is ambiguous; route these to a check before spending on downstream scraping
- `confidenceBreakdown` — `{ rankScore, nameMatchScore, serpEnvironment, candidateCount, components[] }`, the audit trail behind the score (the `components[]` array is the deterministic resolution trace), including how noisy the SERP was
- `rejectedCandidates` — the other results from the same query that didn't win, each with a reason (`social-host` / `directory` / `region-filtered` / `lower-ranked`), so you can see *why* this domain was chosen
- `originalQuery` — the Google query that found it
- `queryType` — `'name'` or `'phrase'`
- `matchedName` / `matchedPhrase` — provenance back to your input
- `rank` — SERP position (1 = top)
- `actorGraph` — `{ previous: null, current, next }`, where `next` points at the website contact scraper for chaining

Names that resolve to nothing come back as `recordType: "unresolved"` rows with a stable `failureType` (`no-data` / `directory-only` / `geo-mismatch` / `blocked`) and an actionable `reason` telling you what to try next. Run-level totals (resolution rate, confidence distribution, unresolved count, safe-to-automate count) are written to the `SUMMARY` key in the run's key-value store. Results are sorted best-first, so the top of the dataset is your strongest resolutions.

Social-media hosts, search engines, directory sites, and major real-estate portals are excluded automatically. Output is filtered to your selected country's TLDs.

#### Resolution confidence

Each resolved domain carries a deterministic confidence score, so you know which rows to trust and which to verify before piping into expensive downstream actors:

- **`name` queries** blend SERP rank with how well the resolved domain matches the searched name (e.g. *Stripe* → `stripe.com` scores high; a generic name whose top result shares no tokens scores low).
- **`phrase` queries** are discovery, not direct resolution: every organic result is a candidate, so they are capped at medium confidence and always flagged `verifyBeforeUse: true`.

The score is reproducible (same input, same output): no LLM, no randomness.

#### The field hierarchy

Four fields, four jobs, so you read the right one for your use:

| Field | Meaning |
|-------|---------|
| `confidenceScore` | the internal 0-1 number (for sorting / thresholds) |
| `confidenceLevel` | the human-readable band (`high` / `medium` / `low`) |
| `ambiguous` | a risk factor: was the name itself unclear? |
| `safeToAutomate` | the operational decision: pass it through unattended, or not |

Most pipelines branch on `safeToAutomate`. Analysts read `confidenceLevel`. Dashboards sort on `confidenceScore`.

### Why deterministic matters

The scoring path uses no LLM and no randomness, which is the point:

- **Same input, same output.** Re-run the same name list and you get identical domains, scores, and decisions.
- **No hallucinated confidence.** Every score traces to SERP rank, name-token match, and SERP composition. You can audit it in `confidenceBreakdown`.
- **Safe for retries and caching.** Because output is reproducible, a workflow can cache resolutions, retry idempotently, and trust that a re-run won't silently change a decision.

This is what makes it a **pre-enrichment firewall**: a deterministic gate that stops bad or ambiguous domains *before* they reach expensive downstream steps (email scrapers, enrichers, verification APIs, outreach systems), so you don't pay to scrape the wrong company.

### Operational characteristics

- Up to **5,000 names per run**, charged per query, not per result
- Deduped at the domain level (one row per unique website)
- Deterministic scoring (no LLM latency, no model variability)
- Stateless execution (no cross-run state to manage or reset)
- Block/CAPTCHA pages are never charged

### Fields to branch on

| Field | Type | Meaning | Stable for branching |
|-------|------|---------|----------------------|
| `safeToAutomate` | boolean | Our default: high confidence, not ambiguous | Yes |
| `policyResult.passed` | boolean | *Your* policy passed (resolutionProfile / automationPolicy) | Yes |
| `recommendedAction` | enum | `auto-enrich` / `verify` / `retry` / `skip` | Yes |
| `confidenceLevel` | enum | `high` / `medium` / `low` | Yes |
| `ambiguous` | boolean | Multiple viable entities for the name | Yes |
| `candidateGap.weakWinner` | boolean | Winner barely beat the runner-up | Yes |
| `recordType` | enum | `domain` / `unresolved` / `error` | Yes |
| `failureType` | enum | Why an input didn't resolve | Yes |

### Stable enums

These values are additive across minor versions (new values may appear; existing ones are never renamed or repurposed), so they are safe to branch on.

- **`recordType`:** `domain`, `unresolved`, `error`
- **`recommendedAction`:** `auto-enrich`, `verify`, `retry`, `skip`
- **`confidenceLevel`:** `high`, `medium`, `low`
- **`failureType`:** `no-data`, `directory-only`, `geo-mismatch`, `blocked`, `invalid-input`
- **`serpEnvironment`:** `clean`, `mixed`, `noisy`
- **`reasonCodes`:** `TOP_RANK`, `STRONG_NAME_MATCH`, `LOW_DOMAIN_MATCH`, `SERP_DIRECTORY_HEAVY`, `GENERIC_ENTITY`, `PHRASE_DISCOVERY`, `DIRECTORY_ONLY`, `GEO_MISMATCH`, `NO_RESULTS`, `BLOCKED`
- **`rejectedCandidates[].reason`:** `social-host`, `directory`, `region-filtered`, `lower-ranked`

### Example

**Input:**

```json
{
    "knownNames": ["Stripe", "Buffer", "Notion"],
    "footerPhrases": ["we buy land in any state"],
    "nameSuffix": "",
    "country": "US",
    "maxResultsPerQuery": 50,
    "maxDomains": 1000
}
```

**Output (one record per unique domain, plus any unresolved inputs):**

```json
[
    { "recordType": "domain", "url": "https://stripe.com", "domain": "stripe.com", "confidenceLevel": "high", "confidenceScore": 1.0, "verifyBeforeUse": false, "originalQuery": "Stripe", "queryType": "name", "matchedName": "Stripe", "rank": 1 },
    { "recordType": "domain", "url": "https://notion.so", "domain": "notion.so", "confidenceLevel": "medium", "confidenceScore": 0.7, "verifyBeforeUse": true, "originalQuery": "Notion", "queryType": "name", "matchedName": "Notion", "rank": 1 },
    { "recordType": "domain", "url": "https://acme-land.com", "domain": "acme-land.com", "confidenceLevel": "low", "confidenceScore": 0.46, "verifyBeforeUse": true, "originalQuery": "we buy land in any state", "queryType": "phrase", "matchedPhrase": "we buy land in any state", "rank": 3 },
    { "recordType": "unresolved", "url": null, "domain": null, "confidenceLevel": null, "verifyBeforeUse": true, "originalQuery": "Buffer", "queryType": "name", "matchedName": "Buffer", "failureType": "no-data", "reason": "No website survived the blocklist / country-TLD filter. The business may have no findable site, or try adding a nameSuffix to disambiguate." }
]
```

*(`confidenceBreakdown`, `rejectedCandidates`, `actorGraph`, and `discoveredAt` fields omitted above for brevity: every `domain` row carries them.)*

![Sample output](https://apifyforge.com/readme-assets/ryanclinton-serp-name-resolver/output-table.png)

Cost for this run: 4 queries × $0.0075 = **$0.03**.

#### Minimal examples

Safe resolution:

```json
{ "domain": "stripe.com", "safeToAutomate": true, "recommendedAction": "auto-enrich" }
```

Ambiguous resolution:

```json
{ "ambiguous": true, "candidateGap": { "weakWinner": true }, "recommendedAction": "verify" }
```

Policy failure:

```json
{ "policyResult": { "passed": false, "failedRules": ["noisy-serp-not-allowed"] } }
```

### Quick answers

**How do I convert a list of company names into website URLs?**
Paste the names into `knownNames` and run the actor. Each name is searched on Google and the top organic result that survives filtering becomes that company's website, with a confidence level attached.

**How do I find a company's website from just its name?**
That is exactly what this actor does. For a single branded name (e.g. *Stripe*) you get a high-confidence domain at rank 1. For generic names, add a `nameSuffix` (like `"plumbing"` or `"insurance"`) to disambiguate.

**Can I turn a CRM export of company names into domains in bulk?**
Yes. The actor takes up to 5,000 names per run, charges per query, and returns one row per resolved domain plus `unresolved` rows for the ones it couldn't find, so you can re-work or skip them.

**How do I know which resolved URLs are correct?**
Filter by `confidenceLevel`. `high` rows matched both SERP rank and the company name; `verifyBeforeUse: true` rows should be spot-checked before you spend downstream budget on them.

**What happens to names it can't resolve?**
They come back as `recordType: "unresolved"` with a `failureType` (`no-data` or `blocked`) and a plain-English reason telling you what to try next. Nothing is dropped silently.

**How do I find websites for businesses that use a specific marketing phrase?**
Put the phrase in `footerPhrases` (e.g. *"we buy land in any state"*). It runs as an exact-match Google query and collects every organic result, useful for niche operator and competitor discovery.

**Which rows can I pass straight into an automated pipeline without review?**
The ones where `safeToAutomate` is `true`. That flag is `true` only when confidence is high and the name wasn't ambiguous, so a Zapier/Make/n8n filter on `safeToAutomate == true` gives you the rows you can scrape or enrich unattended; everything else routes to review.

**Can I exclude specific sites from the results?**
Yes. The actor already drops social and directory hosts automatically; add your own (a known partner, parent company, or niche aggregator) via the `excludeDomains` input.

### AI-agent contract

The actor is designed to be safe for autonomous workflows and AI agents because every routing signal is deterministic, structured, and machine-readable.

- **Inputs:** company names, business names, footer/marketing phrases.
- **Outputs:** canonical domains, candidate entities, routing decisions, reason codes.
- **Branch-safe fields:** `policyResult.passed`, `safeToAutomate`, `recommendedAction`, `failureType`.
- **Deterministic guarantees:** same input = same output; no LLM scoring; stable, additive-only enums; reproducible confidence (safe for caching and idempotent retries).
- **Chaining:** `actorGraph.next` names the recommended next actor (`ryanclinton/website-contact-scraper`), so a long-running agent chains the pipeline without re-deciding each step.

It is the first tool to reach for when an agent has a name but needs the website before it can scrape, enrich, or verify: one call replaces an open-ended web search plus manual disambiguation.

### When it works well — and when it doesn't

**Works well:**

- Specific, branded company names (e.g. *Stripe*, *Twilio*, *Acme Plumbing of Boston*)
- Distinctive marketing/footer phrases that locate operators (*"we buy land in any state"*, *"24-hour mobile mechanic"*)
- Names paired with `nameSuffix` for category disambiguation (e.g. *Apex* + suffix `"plumbing"`)

**Lower hit rate / verify before use:**

- Generic single-word names (*"Capital"*, *"Acme"*): top SERP result is rarely the company you mean (these score `low` and flag `verifyBeforeUse`)
- Franchise / multi-location brands: Google may return a regional listing or a directory above the corporate site
- Industries dominated by directories (legal, healthcare, real-estate aggregators): corporate sites get pushed below G2/Yelp/Zillow-style hosts; the directory blocklist mitigates but doesn't eliminate
- Foreign-language names searched in the wrong country code

The confidence score is built for exactly this: trust the `high` rows, spot-check the rest.

#### Why a row becomes `safeToAutomate: false`

| Example query | What happens | Field that flags it |
|---------------|--------------|---------------------|
| `Apex` | Many competing brands own the name | `ambiguous: true` |
| `Capital` | Generic word, top result rarely the company you mean | `confidenceLevel: low` |
| `Smith Plumbing` | SERP dominated by directories (Yelp, Angi) | `confidenceBreakdown.serpEnvironment: noisy` |
| `Acme` (searched with `country: UK`) | Real site exists on a `.com` excluded by the UK TLD filter | unresolved, `failureType: geo-mismatch` |

In each case the actor tells you *why* rather than handing you a confident wrong answer.

#### Ambiguous names: see the candidates, don't guess

When a name is ambiguous (several real companies own it), the actor still picks a best result, but it also returns `candidateEntities` — the scored alternatives. An agent or automation rule can then apply its own policy (e.g. "only proceed if the top candidate leads the next by 0.2") instead of trusting a single forced pick:

```json
{
    "recordType": "domain", "domain": "apexsystems.com", "ambiguous": true, "safeToAutomate": false,
    "candidateEntities": [
        { "domain": "apexsystems.com", "confidenceScore": 0.46, "rank": 1 },
        { "domain": "apexservicepartners.com", "confidenceScore": 0.41, "rank": 2 },
        { "domain": "apexcapitalcorp.com", "confidenceScore": 0.38, "rank": 3 }
    ]
}
```

#### Define your own "safe" (trust policy)

`safeToAutomate` is our default opinion. Different workflows tolerate different risk: cold outreach wants near-zero false positives, competitor discovery happily accepts noise. Set a `resolutionProfile` (`conservative` / `balanced` / `aggressive`) or a `custom` `automationPolicy`, and every row gets a `policyResult` against *your* rules:

```json
{
    "automationPolicy": {
        "minimumConfidenceScore": 0.82,
        "allowAmbiguous": false,
        "allowNoisySerps": false,
        "blockedReasonCodes": ["GENERIC_ENTITY", "SERP_DIRECTORY_HEAVY"]
    }
}
```

→ each row returns `"policyResult": { "passed": false, "failedRules": ["noisy-serp-not-allowed"], "profile": "custom" }`. Branch your pipeline on `policyResult.passed` to enforce your own bar. This only changes the pass/fail evaluation; it never changes scraping behaviour or cost.

Example policies:

- **Conservative outreach:** `{ "minimumConfidenceScore": 0.9, "allowAmbiguous": false }` (or `resolutionProfile: "conservative"`)
- **Broad competitor discovery:** `{ "minimumConfidenceScore": 0.4, "allowAmbiguous": true }` (or `resolutionProfile: "aggressive"`)

#### De-duplicating company-name lists

CRM exports are messy: *Stripe*, *Stripe Inc*, and *Stripe Payments* are one company. When several input names resolve to the same domain, the resolved row's `aliases` lists the variants, and `entityFingerprint.entityId` gives a stable hash you can group on. That collapses a noisy name list into deduped entities in one pass.

### Country / TLD filtering

`country` controls two things at once:

1. **Google country code (`gl=`)**: biases SERP rankings toward that country.
2. **TLD whitelist**: domains that don't match the country's TLDs are dropped.

| `country` | TLDs kept |
|-----------|-----------|
| `US` | `.com`, `.net`, `.org`, `.us`, `.co` |
| `UK` | `.co.uk`, `.uk` |
| `CA` | `.ca` |
| `AU` | `.com.au`, `.au` |
| `EU` | `.eu`, `.de`, `.fr`, `.es`, `.it`, `.nl`, `.be`, `.pl`, `.se`, `.dk`, `.fi`, `.ie`, `.pt`, `.at`, `.cz` |

Pick the country that matches the bulk of your list: `UK`/`CA`/`AU` exclude `.com`, so a mixed UK list with `.com` results will lose those rows. For mixed regions, `US` is the loosest filter but biases SERP rankings toward US results. (`EU` filters by TLD only; Google has no single `eu` country code, so no country bias is applied for that group.)

### How it works

![How it works](https://apifyforge.com/readme-assets/ryanclinton-serp-name-resolver/intelligence-layers.png)

1. Each `knownNames` entry is searched on Google as `<name> <nameSuffix>`. The top organic result that survives filtering is treated as that company's website.
2. Each `footerPhrases` entry is searched verbatim (exact-match). Every organic result on the SERP is collected.
3. Results are deduped, filtered to your country's TLDs, scored for confidence, sorted best-first, and capped at `maxDomains`.

Uses Apify Proxy `GOOGLE_SERP` group by default, designed for Google scraping and handling rotation and CAPTCHA. If Google blocks the whole run, a circuit breaker stops it early rather than burning queries.

### Pricing

Pay-per-event: **$0.0075 per query executed** ($7.50 per 1,000). A query is one name or one phrase. 100 names + 5 phrases = 105 queries = $0.79.

You are charged per query, not per result, so you pay the same whether a query returns 50 results or zero. Block/CAPTCHA pages are **not** charged: the actor only bills a query once it confirms a real Google results page came back.

### Use cases

- Feeding Website Contact Scraper with a list of names instead of URLs (most common)
- Building a domain list from competitor footer phrases
- Resolving a CRM export of company names to verified websites
- Niche buyer/operator discovery (real estate cash buyers, junk car buyers, contractor franchises, etc.)

### Who uses it

RevOps teams, lead-enrichment pipelines, AI research agents, CRM-normalization workflows, SDR automation, and any website-discovery step that runs before scraping.

### Typical pipeline

```
CRM export / name list
   ↓  serp-name-resolver
domains + policyResult
   ↓  policyResult.passed?
   ├─ yes → website-contact-scraper → bulk-email-verifier → outreach
   └─ no  → human review / add nameSuffix / re-run
```

### Use in Dify

Drop this actor into [Dify](https://docs.apify.com/platform/integrations/dify) workflows via the Apify plugin's Run Actor node. Every resolved row returns scored and classified as structured JSON: `confidenceLevel` (`high` / `medium` / `low`), a `verifyBeforeUse` boolean, and a `recordType` discriminator (`domain` / `unresolved`) your downstream if/else node branches on. A generic Google scraper returns raw SERP HTML; this returns a routing decision per name.

- **Actor ID:** `ryanclinton/serp-name-resolver`
- **Sample input** (resolve a CRM export of company names to websites):

```json
{
    "knownNames": ["Stripe", "Notion", "Acme Plumbing"],
    "nameSuffix": "",
    "country": "US",
    "maxResultsPerQuery": 50
}
```

- **Branching example** — a Dify if/else node routes each record on one boolean:
  - `safeToAutomate == true` → pass `url` straight to the Website Contact Scraper Run Actor node
  - `safeToAutomate == false` (low confidence or `ambiguous`) → send to a human-review / manual-check branch first
  - `recordType == "unresolved"` → route by `failureType` (`blocked` → retry node; `directory-only` → skip; `no-data` → re-query with a `nameSuffix`; `geo-mismatch` → switch country)

Because every `domain` record carries `actorGraph.next` (pointing at `ryanclinton/website-contact-scraper`), the chaining target is a field read, not hard-coded wiring. The actor never calls an LLM in its scoring path, so the `confidenceLevel` / `verifyBeforeUse` values are reproducible and safe to branch on.

### What this does NOT do

This actor resolves a business name or phrase to a **website URL**. It is the discovery step, not the enrichment step. For everything downstream, use the sibling actor:

| Need | Use this instead |
|------|------------------|
| Scrape contacts (emails, phones, names) from the resolved sites | [Website Contact Scraper](https://apify.com/ryanclinton/website-contact-scraper) |
| Verify deliverability of found emails | [Bulk Email Verifier](https://apify.com/ryanclinton/bulk-email-verifier) |
| Guess email format for a domain | [Email Pattern Finder](https://apify.com/ryanclinton/email-pattern-finder) |
| Score and qualify the resulting leads | [Lead Scoring Engine](https://apify.com/ryanclinton/lead-scoring-engine) |

Every `domain` record carries an `actorGraph.next` pointer to the website contact scraper, so you can chain the pipeline in Make, n8n, or Dify by reading one field. It does **not** scrape the resolved sites, validate emails, or rank companies. Resolution confidence tells you which URLs are safe to spend downstream budget on; `verifyBeforeUse: true` rows are the ones to check first.

Unlike commercial enrichment databases (Clearbit, ZoomInfo, Apollo) that match names against a licensed company dataset, this resolves names live against Google's index. That means it finds long-tail and local businesses those databases miss, but it returns a website URL rather than firmographic data, and it tells you its confidence so you can verify the edge cases yourself.

### Summary

The Business Name & Phrase to URL Resolver is a deterministic company website resolver for Apify. It converts company names and distinctive marketing phrases into canonical website domains with deterministic confidence scoring, ambiguity detection, scored candidate entities, machine-readable reason codes, and policy-based automation decisions. It is built for AI agents, lead-enrichment pipelines, CRM normalization, and workflow-safe scraping automation, and acts as a pre-enrichment firewall that stops bad or ambiguous domains before they reach downstream scraping, verification, or outreach. The scoring path uses no LLM and no randomness, so the same input always produces the same output.

# Changelog

This Actor's version history is a separate document: https://apify.com/ryanclinton/serp-name-resolver/changelog.md

# Actor input Schema

## `knownNames` (type: `array`):

Business or company names to resolve to websites. Each name is searched on Google with the optional suffix appended; the top organic result is treated as that company's website. One Google query (one charge) per name.

## `footerPhrases` (type: `array`):

Distinctive phrases that identify a niche (e.g. 'we buy land in any state'). Each phrase is run as an exact-match Google query and every organic result on the page is collected. One Google query (one charge) per phrase.

## `nameSuffix` (type: `string`):

Appended to every name query to disambiguate (e.g. 'land', 'real estate', 'cash buyer'). Leave blank to search names verbatim.

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

Used for both Google country code (gl=) and TLD filtering. Domains that do not match the country's TLDs are dropped.

## `maxResultsPerQuery` (type: `integer`):

How many organic results to collect per query. Higher values find more long-tail sites but increase per-query SERP work.

## `maxDomains` (type: `integer`):

Hard cap on the deduped output. Truncates after dedupe + filter.

## `excludeDomains` (type: `array`):

Extra hosts to drop from results, on top of the built-in social/directory blocklist. Enter bare domains (e.g. 'example.com'). Useful for filtering out a known partner, parent company, or aggregator specific to your niche.

## `resolutionProfile` (type: `string`):

Defines what counts as a pass for your workflow (drives policyResult.passed on every row). 'conservative' = only high-confidence, non-ambiguous, clean-SERP rows. 'balanced' (default) = sensible middle. 'aggressive' = accept medium-confidence and ambiguous rows. 'custom' = use the Automation policy field below. Does NOT change scraping behaviour, only the pass/fail evaluation.

## `automationPolicy` (type: `object`):

Override individual policy thresholds. Any field set here wins over the trust profile preset. Keys: minimumConfidenceScore (0-1), allowAmbiguous (bool), allowNoisySerps (bool), maxCandidateCount (int), blockedReasonCodes (array of reason-code strings, e.g. GENERIC_ENTITY, SERP_DIRECTORY_HEAVY).

## `proxyConfiguration` (type: `object`):

Defaults to Apify Proxy in the GOOGLE_SERP group (recommended). Override only if you have a specific need.

## Actor input object example

```json
{
  "knownNames": [
    "Stripe",
    "Notion"
  ],
  "nameSuffix": "",
  "country": "US",
  "maxResultsPerQuery": 50,
  "maxDomains": 1000,
  "resolutionProfile": "balanced",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "GOOGLE_SERP"
    ]
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

Resolved domain, URL, confidence level, and routing decision for each input name or phrase.

## `allItems` (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 = {
    "knownNames": [
        "Stripe",
        "Notion"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "GOOGLE_SERP"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/serp-name-resolver").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 = {
    "knownNames": [
        "Stripe",
        "Notion",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["GOOGLE_SERP"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/serp-name-resolver").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 '{
  "knownNames": [
    "Stripe",
    "Notion"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "GOOGLE_SERP"
    ]
  }
}' |
apify call ryanclinton/serp-name-resolver --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ryanclinton/serp-name-resolver"
        }
    }
}
```

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/o3pTkI8X0h6FG3yGr/builds/BidzN9DAnP4TwQx64/openapi.json
