Domain to Verified Work Email avatar

Domain to Verified Work Email

Pricing

from $8.50 / 1,000 published person matches

Go to Apify Store
Domain to Verified Work Email

Domain to Verified Work Email

Find the work email a named person publishes on their own company website, confirmed by a same-document schema.org Person assertion. No pattern guessing, no SMTP probing. $0.01 only when a published match is found; no-match, role mailbox, ambiguous and partial outcomes are free.

Pricing

from $8.50 / 1,000 published person matches

Rating

0.0

(0)

Developer

Tim Zinin

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

5 days ago

Last modified

Share

Domain to Verified Work Email: Published-Evidence Person Matching for Company Websites

What this Actor does: input, evidence, outcome

Get one work email per person only when the company's own public website publishes that literal address, names the person in the same visible context, and separately asserts the exact name-to-address pairing in machine-readable schema.org Person markup on the same page.

This is not a guessing service. It does not synthesize first.last@domain patterns, does not call MX or SMTP servers, does not run a catch-all probe, and does not treat "the mailbox accepted the message" as proof that a specific human owns it. Every non-free row in the Dataset is a published person match — the single billable outcome this product sells, and the only noun used for pricing, listing terminology and Dataset semantics throughout this page. Everything else the run finds is free: no-match, role-mailbox-only, ambiguous, unlinked, robots-blocked, partial or budget-stopped.

What you get

For each {fullName, domain} pair you submit, the Actor visits only the buyer-selected company's own website, reading at most four fixed pages in a fixed relevance order — /team, /about, /contact, then / — and returns exactly one of two outcomes:

  • A published person match. The literal email address exists as visible text or an explicit mailto: link on an allowed page. The requested full name appears in the same bounded visible context. The local part of the address follows a deterministic personal-name form (first, last, first.last, first+last, first-initial+last, or last+first-initial). The address is not a role mailbox, placeholder, image filename or third-party domain. And — the condition that makes this product different from a contact-page scraper — the same HTML document also carries valid bounded JSON-LD with an exact schema.org context, @type of Person (or the full schema.org Person URL), whose normalized name and literal email exactly match the requested person and the candidate address. Ordinary prose next to an address is never treated as this assertion; only a genuine machine-readable Person claim on the same page counts.
  • An honest free decision. No literal address at all, an address tied to a role or a different person, two candidates that both clear the bar (an explicit tie), a page the runtime could not read, a page robots.txt disallowed, or a budget stop before or after the source read completed. Every free decision still returns a full row with checked pages, unreachable pages, robots exclusions and a partialReason or plain no-match reason — never a bare empty result.

The Actor never claims mailbox ownership, consent to be contacted, present-day deliverability or willingness to receive outreach. Those four claims are outside what a published web page can prove, so every row — paid or free — carries safeToAutomate:false and safeForOutreach:false as non-negotiable machine flags, not just prose caveats. A published person match is evidence to route to a human for a compliance-checked next step, not a green light for automatic email sending.

Who uses it

  • Sales and partnerships teams building an enrichment step for a CRM record where an unverified guessed address already causes bounce-rate and deliverability damage, and who need an auditable "here is the exact page and the exact machine-readable claim" record before a human approves outreach.
  • Recruiting and sourcing teams confirming a public work address for a named candidate before a manual outreach step, without scraping a personal inbox or a paid people-search database — only what the target company itself already publishes.
  • Compliance and data-quality reviewers who need a defensible reason to accept or reject an address already sitting in a CRM or spreadsheet — this Actor's row is the reason, with source URL, bounded-context hash and observation time attached.
  • Developers and workflow builders who want a strict, flat, machine-checkable contract instead of parsing prose — an agent, an n8n node or an MCP client can branch on verificationStatus and found without guessing at free text.
  • Internal automation agents operating under human review — the Actor is explicitly designed so an LLM-driven workflow can consume its output safely: every non-terminal state is machine-typed, and the two safety flags default to false so an agent cannot silently escalate a free or ambiguous row into an outreach action.

This is a one-person, one-domain, on-demand utility. It is not a monitor, not a bulk company-wide crawler, not a mailbox verifier and not a people-search database. It does not accept a list of guessed addresses to "check" — it only accepts a name and a domain and only ever returns an address it found published, never one it invented.

Where the Actor sits in your pipeline: trigger, run, action

How to run

  1. Open the Actor's Input form (Console, API or an MCP client) and supply one to fifty {fullName, domain} objects, each with an optional companyName hint and an optional inputRef you control for CRM correlation.
  2. Optionally cap maxPagesPerPerson (1–4, default 3) and maxConcurrency (1–5, default 1; platform-billed work is always processed sequentially regardless of this setting — it only affects local, non-monetized batching).
  3. Start the run from the Console Start, the Apify API runs endpoint with your token, an Apify Task on a schedule, or an MCP tool call using the same strict input.
  4. Read the default Dataset for one row per submitted person — every row is present even for duplicates, ambiguous pages or source failures, so row count against input count is itself a completeness check.
  5. Read the key-value store record OUTPUT for one run-level summary: input rows, unique people after de-duplication, duplicates skipped, matched count, free-decision count, partial count, budget-stopped count, whether the run stopped early, and why.
  6. Route every published_person_match row to a human-reviewed next step. Do not wire the paid outcome directly into an autosend integration; the safety flags exist specifically to prevent that shortcut.

No login, browser session, proxy or CAPTCHA-solving step is ever required to run this Actor, because none of those techniques are part of its source-access design (see Sources and rights, below). A default-input smoke run does not require any buyer secret.

Pricing

$0.01 per published person match, plus a $0.005 Actor start fee. Every other outcome — no-match, role-address-only, unlinked personal address, ambiguous tie, robots-blocked page, unreachable page, partial coverage, or a budget stop before or after a source read — costs nothing beyond the flat start fee. The default Dataset item event itself is priced at exactly zero, so reading rows back never re-bills you.

The only data source is the company's own public website that you selected — there is no third-party data license or purchased list behind this Actor, so there is nothing to resell and no separate data bill on top of the price above.

The billable unit is always the same published person match — never a page read, never a "lookup," never a flat per-input charge. A run of fifty people where only three clear the full evidence bar bills for three published person matches ($0.03) plus the one start fee, not fifty.

Input contract

people is required: one to fifty objects. Each object requires fullName (a real first-plus-last name, 3–160 characters, must contain at least two whitespace-separated tokens) and domain (a bare public hostname, 4–253 characters — schemes, paths, credentials and IP literals are all rejected by pattern before any network call). companyName is an optional page-evidence hint, never used to invent an address. inputRef is an optional opaque string, up to 200 characters, copied back to the result row exactly as given — including leading, trailing, repeated or tab whitespace — for CRM/workflow correlation; it is validated but never normalized or interpreted.

You cannot supply a path, a full URL or a page list. The runtime alone selects the fixed, relevance-ordered page set — /team, /about, /contact, / — and maxPagesPerPerson only narrows how many of those four the run is willing to read for a given person, not which ones. maxConcurrency only affects local batching; it never parallelizes billed source work.

Unicode names and internationalized domain names are normalized through the current Public Suffix List, including private-boundary entries. Bare public suffixes, URLs, embedded credentials and IP literals are rejected before any source call. Duplicate {normalized full name, registrable domain} pairs are collapsed once, before any page is fetched, so re-submitting the same person against the same company twice in one run never double-charges you.

{
"people": [
{
"fullName": "King Lee",
"domain": "visionwide.co",
"companyName": "Visionwide",
"inputRef": "crm-lead-40217"
},
{
"fullName": "Dana Osei",
"domain": "examplecorp.com",
"companyName": "Example Corp",
"inputRef": "crm-lead-40218"
}
],
"maxPagesPerPerson": 3,
"maxConcurrency": 1
}

The runtime independently re-validates every field and rejects unknown properties and wrong JSON types even where the platform schema would already have caught them, so a caller bypassing the Console form through the raw API gets the same guarantees as one using it.

Example output

Every row shares one flat 31-field schema regardless of outcome, so a database, dataframe or workflow condition never needs to branch on record shape — only on field values. The three rows below are illustrative examples built directly from dataset_schema.json and the matching contract, showing the exact field shape and values this Actor emits for each outcome class.

Published person match

{
"recordType": "person_email_decision",
"schemaVersion": "1.0",
"entityId": "5c64f059712997f402463d54",
"inputRef": "crm-lead-40217",
"fullName": "King Lee",
"companyName": "Visionwide",
"domain": "visionwide.co",
"submittedDomain": "visionwide.co",
"email": "king.lee@visionwide.co",
"verificationStatus": "published_person_match",
"verificationLevel": "published_person_to_address_match",
"matchScore": 100,
"matchReasons": [
"literal_email_published",
"same_registrable_company_domain",
"full_name_on_page",
"personal_local_part_matches_name",
"schema_org_person_name_email_assertion",
"full_name_in_same_bounded_dom_context",
"company_name_on_page"
],
"sourceUrl": "https://visionwide.co/team",
"sourcePath": "/team",
"sourceEvidence": {
"literalEmailPublished": true,
"fullNameOnPage": true,
"personalLocalPart": true,
"publicationChannel": "visible_text",
"boundedContextSha256": "8b60d44f3fd265e726822263b87b1cf471d25ff506ed91690068718154d27246",
"fullNameInSameContext": true,
"schemaOrgPersonAssertion": true,
"disclaimer": "Visible published name-to-address evidence plus an exact schema.org Person assertion, source URL and observation time. Not mailbox ownership, consent, deliverability or outreach permission."
},
"checkedPages": [
{ "path": "/team", "url": "https://visionwide.co/team", "status": 200, "truncated": false },
{ "path": "/about", "url": "https://visionwide.co/about", "status": 200, "truncated": false }
],
"unreachablePages": [],
"robotsDisallowed": [],
"confidenceScore": 100,
"confidenceBand": "high",
"recommendedAction": "ingest_evidence_then_apply_your_legal_outreach_policy",
"safeToAutomate": false,
"safeToIngestEvidence": true,
"safeForOutreach": false,
"partial": false,
"partialReason": null,
"errorCode": null,
"error": null,
"retryable": false,
"observedAt": "2026-08-11T14:32:07.418Z",
"found": true
}

Every key, value shape and const in this row matches the actual dataset_schema.json conditional rules for found:true — the exact seven matchReasons tokens, the exact eight-key sourceEvidence object, the fixed recommendedAction string, and matchScore/confidenceScore/ confidenceBand pinned to 100/100/"high" are the only values the schema allows for a paid row.

Partial — coverage could not be completed

{
"recordType": "person_email_decision",
"schemaVersion": "1.0",
"entityId": "2ec54ae61eb6352a443897e7",
"inputRef": "crm-lead-88031",
"fullName": "Priya Chandran",
"companyName": null,
"domain": "northfieldstudio.io",
"submittedDomain": "northfieldstudio.io",
"email": null,
"verificationStatus": "partial_source_coverage",
"verificationLevel": "none",
"matchScore": 0,
"matchReasons": [],
"sourceUrl": null,
"sourcePath": null,
"sourceEvidence": {
"candidateCount": 0,
"roleAddressCount": 0,
"unlinkedPersonalAddressCount": 0,
"thirdPartyAddressCount": 0,
"disclaimer": "One allowed page could not be read before the page budget was exhausted; no candidate is eligible for payment from an incomplete scan."
},
"checkedPages": [
{ "path": "/team", "url": "https://northfieldstudio.io/team", "status": 200, "truncated": false },
{ "path": "/contact", "url": "https://northfieldstudio.io/contact", "status": null, "truncated": false }
],
"unreachablePages": [
{
"path": "/contact",
"status": null,
"errorCode": "SOURCE_TIMEOUT",
"error": "Source request timed out",
"retryable": true
}
],
"robotsDisallowed": [],
"confidenceScore": 0,
"confidenceBand": "none",
"recommendedAction": "retry_source_later",
"safeToAutomate": false,
"safeToIngestEvidence": false,
"safeForOutreach": false,
"partial": true,
"partialReason": "source_page_unavailable",
"errorCode": "SOURCE_TIMEOUT",
"error": "One allowed page (/contact) timed out before the run established complete coverage; the pages that were read are still reported above.",
"retryable": true,
"observedAt": "2026-08-11T14:41:52.005Z",
"found": false
}

Notice that /contact appears in both checkedPages (attempted, no status obtained) and unreachablePages (the failure detail) — the schema requires both entries together so a consumer never has to infer an attempt from a failure record alone. errorCode/error/retryable here use the exact bounded SOURCE_TIMEOUT family the schema defines for a genuine timeout, not a free-text guess.

Free — role mailbox found, no person match

{
"recordType": "person_email_decision",
"schemaVersion": "1.0",
"entityId": "f31dd870b14954a167a92211",
"inputRef": "crm-lead-40218",
"fullName": "Dana Osei",
"companyName": "Example Corp",
"domain": "examplecorp.com",
"submittedDomain": "examplecorp.com",
"email": null,
"verificationStatus": "role_address_only",
"verificationLevel": "none",
"matchScore": 0,
"matchReasons": [],
"sourceUrl": null,
"sourcePath": null,
"sourceEvidence": {
"candidateCount": 0,
"roleAddressCount": 1,
"unlinkedPersonalAddressCount": 0,
"thirdPartyAddressCount": 0,
"disclaimer": "Complete allowed-page coverage found only a published role mailbox; a role address never satisfies person-level evidence, however close the name appears."
},
"checkedPages": [
{ "path": "/team", "url": "https://examplecorp.com/team", "status": 200, "truncated": false },
{ "path": "/about", "url": "https://examplecorp.com/about", "status": 200, "truncated": false },
{ "path": "/contact", "url": "https://examplecorp.com/contact", "status": 200, "truncated": false }
],
"unreachablePages": [],
"robotsDisallowed": [],
"confidenceScore": 0,
"confidenceBand": "none",
"recommendedAction": "review_or_supply_a_different_public_source",
"safeToAutomate": false,
"safeToIngestEvidence": false,
"safeForOutreach": false,
"partial": false,
"partialReason": null,
"errorCode": null,
"error": null,
"retryable": false,
"observedAt": "2026-08-11T14:44:19.771Z",
"found": false
}

Notice what stays constant across all three rows: the same 31 keys, the same nullable-not-missing shape, and the same pair of safety flags fixed at false. A consumer never has to sniff which fields exist before reading a row.

Field dictionary

FieldMeaningImportant boundary
recordTypeAlways "person_email_decision"Constant discriminator; safe for schema routing
schemaVersionAlways "1.0"Bump on any breaking Dataset change, never silently
entityIdStable 24-hex-character row identityDeduplication key, not a CRM record ID
inputRefYour caller reference, copied exactlyNever normalized; whitespace preserved on purpose
fullNameNormalized requested nameEcho of input, not a claim the person exists at the domain
companyNameOptional company hint you suppliedEvidence signal only; never used to fabricate an address
domainNormalized registrable company domainPSL-normalized; IDN and Unicode handled before matching
submittedDomainThe domain string EXACTLY as your input spelled itWhen it differs from domain, IDNA folded a confusable spelling (e.g. a fullwidth letter) onto the ASCII domain — compare the two before acting on the row
emailThe literal published addressnull on every free row — never a guessed pattern
verificationStatusOne of ten deterministic outcome codesOnly published_person_match is billable
verificationLevelpublished_person_to_address_match or noneDescribes what kind of evidence was found, not a confidence score
matchScore0–100 deterministic evidence scoreThe only matcher-reachable paid value is exactly 100
matchReasonsNamed evidence tags for a matchEmpty array on every free row
sourceUrlThe exact page the match came fromnull unless one page carries the full assertion
sourcePathWhich of the four allowed paths matchednull, /team, /about, /contact or / — never a caller-supplied path
sourceEvidenceStructured evidence object with a mandatory disclaimerdisclaimer is present on every row, paid or free
checkedPagesOrdered list of pages actually readUnique paths only, drawn from the fixed four-path sequence
unreachablePagesPages that could not be read, with error detailEvery entry's errorCode/error/retryable triple is drawn from one closed, schema-enforced family — never a free-text guess
robotsDisallowedAllowed-set paths robots.txt blockedThe Actor never reads a disallowed path to "check anyway"
confidenceScore0–100 routing-confidence numberNot a probability-of-correctness statistic; a deterministic bucket key
confidenceBandhigh, ambiguous or noneCoarse routing bucket for confidenceScore
recommendedActionOne of a small closed set of queue labels, pinned by verificationStatus/retryableA routing tag for a human queue, never an authorization to act — the exact string per status is schema-enforced, not freely chosen at write time
safeToAutomateAlways falseFixed by the Dataset schema; no future row can flip this without a schema version bump
safeToIngestEvidenceWhether the row's evidence is complete enough to store downstreamDistinct from safeForOutreach — evidence can be ingestible without being outreach-ready
safeForOutreachAlways falseThis product never certifies a message is safe to send
partialWhether complete allowed-page coverage was establishedA true row can never carry verificationStatus:"published_person_match"
partialReasonWhy coverage was incompletenull unless partial:true; otherwise exactly one of source_page_unavailable, source_page_truncated, unsupported_content_type, robots_disallowed_page, robots_unavailable_fail_closed, matcher_work_limit_exceeded, budget_stopped_before_source_read, budget_stopped_after_source_read, person_processing_error
errorCodeBounded machine error codenull on a clean decision; present on every unreachable/failure row
errorShort bounded human-readable error textNever contains request headers, cookies or page bodies
retryableWhether a bounded retry is appropriateDoes not override the run's own budget and deadline caps
observedAtWhen this Actor observed the sourceDistinct from any timestamp the source page itself displays
foundWhether this row is the one billable outcome classThe only field a workflow strictly needs to gate payment logic

Evidence and boundaries

What "verified" does and does not mean here. In this product, verified means visible published person-to-address evidence plus an exact schema.org Person name-email assertion, with source URL and observation time. It does not mean mailbox ownership, consent, present deliverability or willingness to receive outreach. That sentence is not marketing language — it is copied from the product contract and repeated in every accepted row's disclaimer field so the boundary travels with the data, not just this page.

Why the schema.org requirement exists. A literal address next to a name in ordinary prose is weak evidence — company pages routinely list people near addresses that belong to someone else, near role mailboxes, or near stale contact blocks. Requiring the same page to independently assert the exact pairing through machine-readable schema.org/Person JSON-LD removes an entire class of false positives that a text-proximity-only extractor would happily bill for. If a page has the name, has the address, and even has a JSON-LD block — but that block does not name-and-email match the exact requested candidate, uses an aliased or missing @context, or is embedded as an object rather than the expected shape — the row is free, not paid.

Why hidden content is excluded before matching. Real company pages hide old staff blocks, template placeholders and legacy contact cards with display:none, zero-opacity layers, off-screen positioning, closed <details> elements and CSS techniques ranging from simple inline styles to !important cascades and CSS-escape obfuscation. The Actor deterministically removes script, template, hidden-input and datalist/option nodes, fetches the stylesheets the page links (from any HTTPS host whose own robots.txt permits it — a fonts service, a CDN, an asset subdomain), and evaluates the bounded CSS cascade for hides, foreground/background contrast and tiny-box clipping.

How uncertainty is scoped: to the element, not the page. A CSS rule the engine cannot evaluate exactly — an @media/@supports block, a complex selector, a var() value — marks the elements that rule could touch as visibility-unknown. A candidate address whose chain touches a marked element is never billed: the row comes back as a free partial_source_coverage with partialReason: matcher_work_limit_exceeded and the exact reason in error. A competitor on a marked element still counts as evidence and still withholds a sale. Only CSS whose reach cannot be bounded at all — an @import, an unknown at-rule, a stylesheet that could not be fetched, or a page past the published work ceilings (64 fetched sheets of up to 600 KB each, 8M CSS characters, 65,536 visibility-relevant rules, 131,072 selectors, 1,048,576 selector-target applications) — makes the whole page unknowable, again as a free partial. Measured on live sites the day this shipped: tailscale.com (Tailwind v4, cross-host sheets, 17,500 selectors per page) completes cleanly, and visionwide.co (WordPress + bootstrap, 33 sheets) evaluates fully and correctly refuses to bill an address that its theme shows only inside a click-to-open popup. The honest residue: an address whose visibility genuinely depends on viewport or unevaluable values stays unbilled — a withheld row costs a sale, a wrong row costs your trust and your money.

Why role addresses and weak local parts are rejected. contact-us, customer-care, editorial, recruiters, human-resources, admissions and a defined family of similar role-token variants never become paid rows, even if a name happens to sit nearby. Weak local-part forms (first-name-only or last-name-only) additionally require the requested full name in the immediate bounded evidence context and fail closed if that context contains a conflicting person name. This is a deliberate asymmetry: the bar for treating something as a real personal address is higher than the bar for treating it as merely present text.

Why the source path is narrow by design. The Actor reads only the buyer-selected company's own public website, bounded to the same registrable domain, at most four fixed pages (/team, /about, /contact, /), over HTTPS on the default port only. The single exception is rendering context: stylesheets those pages link are fetched from whatever HTTPS host serves them, each under its own robots.txt, and never contribute evidence — only visibility. Every hostname is resolved once per run and pinned to globally routable addresses before any connection; private, loopback, metadata, documentation, transition and mixed private/public resolutions are all blocked before a socket opens. Redirects are followed manually, only within the same public-suffix registrable-domain boundary, and every hop re-verifies scheme, domain and address. There is no browser, no JavaScript execution, no login, no CAPTCHA-solving and no proxy — this is a static-HTML reader with a hard run-wide cap of 300 actual HTTP requests.

Why robots.txt is honored strictly, not politely. The policy file is fetched fail-closed: if it is missing, ambiguous, wrongly typed, redirected through a login page or disallows the target path, that path is simply not read — not read-with-a-warning. The compiled policy respects exact user-agent groups over wildcard groups, closes ambiguous or malformed directive groups before any page request, and matches wildcard/end-anchor rules with literal-segment matching rather than compiling caller-influenced text as a regular expression.

Why two identical evidence rows are not each other's proof of correctness. If two candidate addresses on the same domain both carry a complete, valid schema.org Person assertion for the requested name, the run does not guess a winner — it returns an explicit free tie. Certainty, not volume of near-misses, is what this product sells.

Decision routing

Every row's verificationStatus maps to exactly one of the following ten outcomes. A downstream workflow should switch on this field directly rather than re-deriving it from email, found or prose.

verificationStatusBillable?What it meansrecommendedAction you will see
published_person_matchYesComplete, unambiguous, schema.org-backed evidenceingest_evidence_then_apply_your_legal_outreach_policy
ambiguous_multiple_matchesNoTwo or more candidates each cleared the evidence barretry_source_later if retryable:true, else review_or_supply_a_different_public_source
partial_match_not_billableNoSome but not all conditions were metretry_source_later if retryable:true, else review_or_supply_a_different_public_source
partial_source_coverageNoNot every allowed page could be readretry_source_later if retryable:true, else review_or_supply_a_different_public_source
role_address_onlyNoOnly a role mailbox was foundreview_or_supply_a_different_public_source
unlinked_personal_address_onlyNoA personal-shaped address exists but lacks the schema.org assertion or name-context linkreview_or_supply_a_different_public_source
no_published_person_matchNoComplete coverage, genuinely nothing foundreview_or_supply_a_different_public_source
budget_stopped_before_source_readNoThe run's cap was reached before this person was processedraise_max_total_charge_or_reduce_input
budget_stopped_after_source_readNoThe cap was reached after reading pages but before a result could be deliveredraise_max_total_charge_or_reduce_input
partial_processing_errorNoAn internal processing limit or error stopped this person's evaluationretry_source_later

Every value in that last column is a schema-enforced constant, not free text — the Dataset schema pins the exact string per status (and, for the three retry-eligible statuses, per retryable value too), so a downstream switch statement can match on it exactly rather than pattern-matching prose.

A workflow implementation only needs a handful of rules to stay correct:

  • Only continue an automated pipeline step on found === true and partial === false.
  • Never branch on the presence of email alone — email is null on every free row by design, and a non-null email can never appear outside published_person_match under the enforced schema, so treat any mismatch as a hard integration bug, not a valid state.
  • Treat ambiguous_multiple_matches and unlinked_personal_address_only as "evidence exists but is not sufficient," not as a weaker version of a match — they are just as unbillable as a clean no-match, and neither is eligible for a lower-confidence autosend tier.
  • Retry only rows where retryable === true, and only after the specific delay/backoff your workflow already applies to other bounded-source jobs — this Actor's own run-level cap does not retry automatically.
  • Deduplicate downstream on entityId, not on {fullName, domain}, since the entity ID is stable across identical resubmission while your own upstream normalization might differ slightly.

Commercial playbooks

CRM email-quality gate

Before a guessed or purchased address is allowed into an active outreach sequence, run it — or the name/domain pair it came from — through this Actor. A published_person_match becomes supporting evidence for a human reviewer's decision to proceed; anything else becomes a reason to hold the record for manual verification rather than let a guessed pattern degrade sender reputation. This playbook explicitly does not wire the result into an autosend step; the Actor's own safety flags are designed to stop that shortcut, and the human review step is not optional.

Recruiting candidate-contact confirmation

When a sourcing workflow already has a candidate's public profile and target company, this Actor checks whether that same company's own site independently publishes a matching, schema-asserted address for that exact name. A match gives a recruiter a defensible, source-linked reason to reach out through the confirmed channel instead of a purchased database guess; a free result is a signal to fall back to the platform's native messaging instead of fabricating an email.

Partner and vendor identity verification

Before onboarding a named contact from a partner or vendor company into a billing, support or security-notification list, this Actor confirms that the company's own website — not a third-party aggregator — currently publishes that person at that address with a machine-readable assertion. This is a lightweight identity-hygiene check, not a KYC or sanctions-screening substitute.

CRM data-quality audit sweep

Run a batch of existing CRM contacts (name + company domain, using inputRef to carry the CRM record ID) to find stale or fabricated-looking addresses. Rows with no_published_person_match or role_address_only for a contact your CRM currently lists as a personal, verified address are a worklist for data-hygiene review — not an automatic field overwrite, since the source page not currently publishing an address does not prove the address itself is wrong.

Support and escalation contact confirmation

For B2B accounts where an internal contact list has gone stale, this Actor can re-confirm whether a named account contact still has a personal address published on the vendor's own site before an account manager escalates an issue directly instead of through a shared support alias.

Integration recipes

The Actor is built as a machine-first microservice: a strict, closed Input schema; a uniform, strictly typed Dataset; and a separately schema-validated OUTPUT summary record. Callers can drive it through the Apify Console, the Apify API, a scheduled Apify Task, or an MCP client using the same Input contract described above.

Scheduled CRM enrichment pattern

  1. A buyer-owned Apify Task stores the strict Input — no secrets are required by this Actor's own contract, since it reads only public pages.
  2. A schedule starts a bounded run against a queue of {fullName, domain, inputRef} triples pulled from CRM records that currently lack a verified address.
  3. The workflow waits for terminal success and reads KVS OUTPUT for matched, freeDecisions, partial and budgetStopped counts before touching Dataset rows.
  4. It reads Dataset rows and routes every published_person_match into a human review queue keyed by inputRef.
  5. It logs every free verificationStatus value back to the CRM record as a "checked, not verified" marker rather than leaving the field silently untouched.

Event-driven single-lookup pattern

  1. A new-lead webhook or form submission triggers a single-person run with one people entry.
  2. The caller polls run status or awaits a webhook/finish callback, then reads the one Dataset row.
  3. If found === true, the lead record gets a "company-verified address available for review" flag; the address itself is still routed to a human before it enters an outreach sequence.
  4. If partial === true and retryable === true, the caller requeues once after its own standard backoff window rather than treating the gap as a permanent no-match.

Agent/MCP pattern

  1. The agent supplies exactly the fields the Input schema allows — it cannot inject a path, a credential or an alternate protocol even if it tries, because the schema and the runtime validator both reject anything outside the closed contract.
  2. It reads found, partial, verificationStatus, safeToAutomate and safeForOutreach before deciding on any next step.
  3. It never converts a role_address_only or unlinked_personal_address_only row into an affirmative claim just because an address string exists in the row's evidence object.
  4. It always routes a published_person_match to a human queue rather than an autonomous send action, honoring safeForOutreach:false as a hard stop, not a suggestion.
  5. It cites entityId, sourceUrl and observedAt in any work item or ticket it creates, so a human reviewer can re-check the original page directly.

Data-warehouse pattern

Append Dataset rows keyed by entityId. Keep observedAt (this Actor's observation time) distinct from any timestamp your own pipeline assigns on ingestion. Preserve null fields as null rather than coercing them to empty strings, since a null email is a structurally different fact from an empty string one. Treat a future schemaVersion bump as an explicit migration, not a silently compatible row shape.

Operating guide

Choosing maxPagesPerPerson. The default of 3 reads the three highest-relevance pages before the homepage fallback. Raising it to 4 adds the homepage as a fourth attempt for companies whose staff information lives only there; lowering it to 1 or 2 trades completeness for a tighter page budget when you already have strong reason to believe the address lives on /team specifically.

Reading the OUTPUT summary before Dataset rows. The run-level summary tells you, in one object, whether the whole batch behaved as expected — inputRows, uniquePeople, duplicateInputsSkipped, matched, freeDecisions, partial, budgetStopped, stopped and stopReason — before you spend time iterating individual rows. A stopped:true run with a non-null stopReason means the batch did not fully complete and some people at the tail of your input list may never have been processed; check inputRows against the actual Dataset row count to confirm.

Understanding replay safety. Every paid delivery is journaled in two phases before it ever reaches you: source_reserved before any page is read, then delivery_pending with the exact row hash before the atomic Dataset write, and only delivered after a confirmed platform receipt. If a worker process is interrupted between the atomic write and receiving its receipt, the next process reconciles a bounded read of the Dataset before doing any new source work, recognizes a row whose hash matches the pending entry as already delivered, and skips it rather than re-billing you. An unresolved mismatch between the journal and the Dataset becomes a fatal, non-retried state rather than a silent duplicate charge — this Actor would rather stop than guess.

Understanding the budget-stop states. If your run's result cap is reached before a person is processed at all, that row is budget_stopped_before_source_read and no page was ever fetched for it. If the cap is reached after pages were already read for that person but before a decision could be delivered, the row is budget_stopped_after_source_read and preserves every checked/unreachable page fact it gathered — the evidence of what was actually read is never discarded just because the budget ran out at the wrong moment.

Understanding concurrency. maxConcurrency only governs local, non-monetized batching of work before it reaches the platform boundary. On-platform source reads and paid deliveries are always sequential: another source read only starts while at least one more possible result still fits under your run's cap, so you cannot be charged for parallel work that raced past your intended limit.

Interpreting checkedPages and unreachablePages together. These two arrays are drawn from the same fixed four-path universe and are designed to be read together: every checked page that returned a non-200 status has a matching unreachablePages entry, and every unreachablePages entry that carries a status has a matching checkedPages witness. A path never appears as both fully checked with a 200 status and unreachable — treat any apparent contradiction in your own tooling as a bug in how you're reading the row, not an ambiguity the Actor itself would ever produce.

FAQ

Does this Actor guess email address patterns?

No. It never synthesizes first.last@domain or any other pattern. Every address it returns is one it found as literal published text or an explicit mailto: link on the company's own site.

No, and this is the single most important boundary of the product. "Verified" here means published, page-linked, schema.org-asserted evidence — not that the mailbox exists in a working state, not that the named person controls it, and not that they have agreed to receive anything. safeForOutreach is fixed to false on every row for exactly this reason.

Can I automatically send outreach based on a published_person_match row?

The Dataset schema is deliberately built to make that hard to do by accident. safeToAutomate and safeForOutreach are both fixed false, and recommendedAction labels are queue tags for a human, not authorization strings for an automated send integration. Treat every paid row as "ready for a human to review," not "ready to message."

What happens if a person has two published addresses that both look valid?

The run returns an explicit ambiguous_multiple_matches free decision rather than guessing between them. Certainty is a condition of billing, not a tiebreaker applied after the fact.

Does it read pages I don't ask for?

No. The runtime alone selects the exact fixed set — /team, /about, /contact, / — in that relevance order, bounded by maxPagesPerPerson. You cannot supply an arbitrary path or URL, and the PAGE reads never leave the registrable domain even across redirects; stylesheets those pages link are the one cross-host fetch — rendering context only, each host consulted through its own robots.txt, never a source of evidence.

What happens if robots.txt disallows a page?

That page is never read, and it appears in robotsDisallowed rather than checkedPages. This applies even if a schema.org-asserted address would very likely exist there — the source policy is honored strictly, not selectively.

Why does a "no match" row still cost the start fee?

The $0.005 start fee covers the run/session overhead itself, independent of outcome; the $0.01 per-person charge is what is scoped to the published person match outcome specifically. Every free decision still costs the flat start fee and nothing per result.

Is browser automation, a proxy or a login step ever used?

No. The Actor is a static-HTML reader over HTTPS with SSRF-safe DNS pinning. There is no browser engine, no JavaScript execution, no CAPTCHA-solving and no proxy configuration surface in the Input schema at all.

What is entityId and can I use it as a permanent database key?

It is a stable 24-character hex identity generated for the row within this Actor's own delivery and replay-safety journal. It is safe to use for deduplication and idempotency inside your own pipeline; it is not a CRM contact ID and carries no meaning outside this Actor's context.

How is this different from a plain "find contact page and extract emails" scraper?

A plain extractor bills for any literal address it can find near a name in prose. This Actor adds a strictly higher, machine-checkable bar: the same page must also carry a valid schema.org Person assertion that exactly names the pairing. That is a materially narrower, more defensible claim, and it is why a much larger share of runs against real company sites are expected to return a free decision rather than a paid one — completeness of the public web's schema.org adoption is a real limiting factor, not a defect in this Actor.

Can an AI agent safely call this Actor unattended?

Yes, for the lookup step itself — the strict input, flat output and conservative safety flags are designed for exactly that. The one hard rule an agent must keep is routing published_person_match results to a human before any outreach action, because the Actor's own flags will not silently authorize that step for it.

Sources and rights

The only data source is the buyer-selected company's own public website, bounded to the same registrable domain, over HTTPS on the default port, limited to four fixed relevance-ordered paths per person. robots.txt is honored fail-closed on every hostname before any page is read. No private, internal, metadata or non-public network destination can ever be reached — every hostname is resolved once and every resolved address is checked against a blocklist covering private, loopback, metadata, documentation, transition and deprecated/retired address ranges before a connection is attempted, and every redirect hop repeats that check rather than trusting the first one. The Actor does not use a proxy, browser engine, login flow or CAPTCHA-solving step, and it never reads a page path outside the fixed four-path set regardless of what a caller supplies.

The Dataset stores a SHA-256 hash of the bounded evidence context rather than the surrounding page text, and it never stores unrelated data incidentally observed on the page — no phone numbers, no other people's addresses, no page content beyond what the matching contract itself requires as evidence. Buyers are responsible for using the returned evidence in a manner consistent with the target company's own terms of use and applicable data-protection law in their jurisdiction; this Actor's narrow read scope and strict robots.txt compliance reduce but do not eliminate that responsibility, and the human-review requirement built into every safety flag exists specifically to keep a person in the loop before any contact action is taken.

Limits

  • One to fifty people per run; one company domain per person, never a company-wide crawl.
  • At most four fixed, Actor-selected pages per person — never a caller-supplied path.
  • Static HTML only: no browser, no JavaScript execution, no login, no CAPTCHA-solving, no proxy.
  • A single run-wide cap of 300 actual HTTP requests (250 logical robots/page reads before redirects).
  • No pattern-guessed addresses, no MX/SMTP/catch-all probing, no deliverability claim of any kind.
  • No claim of mailbox ownership, consent or outreach permission — ever, on any row.
  • safeToAutomate and safeForOutreach are fixed false for every row the Dataset schema allows.
  • No bulk company-wide extraction mode; this is a per-person, per-domain utility, not a monitor.

Support boundary

This Actor is independently reviewed before release. Support covers deterministic input validation, the source-access and matching-contract behavior described in this page, Dataset/Output/KVS schemas, and delivery/replay evidence. It cannot decide whether a specific outreach action is appropriate for your jurisdiction or industry, cannot restore access to a target company's website if it changes its own robots.txt policy or page structure, cannot guarantee that a company continues to publish an address it published at observation time, and cannot provide legal advice on contact or data-protection obligations. When reporting a problem, include the Actor run ID, the redacted entityId, verificationStatus, errorCode and approximate observedAt time. Never send full page HTML, unredacted addresses beyond what you already hold, or any credential — this Actor's own contract never requires one, and support will never ask for one either.