# Lawyer & Attorney Lead Scraper — Law Firm Contacts (`samstorm/lawyer-lead-scraper`) Actor

Find law firms on Google Maps by practice area and city, with public emails, phones and source links where available. Start with 3 records: $0.15 in result fees, plus startup and platform usage. No external API key. Multi-city search, repeat-run exclusions, and CSV/CRM exports.

- **URL**: https://apify.com/samstorm/lawyer-lead-scraper.md
- **Developed by:** [Sam Kleespies](https://apify.com/samstorm) (community)
- **Categories:** Lead generation
- **Features:** Limited permissions
- **Stats:** 115 total users, 3 monthly users, 96.9% runs succeeded, 2 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 enriched attorney leads

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## Lawyer & Attorney Lead Scraper

**New:** [Walkthrough: Multi-city law firm leads to Google Sheets](#walkthrough-multi-city-law-firm-leads-to-google-sheets) — setup steps and importable n8n JSON below.

**Start here: [run the 3-lead sample](https://apify.com/samstorm/lawyer-lead-scraper/examples/demo---pi-lawyers-in-chicago).** Inspect the source links, contact fields and verification status before scaling. No external data-provider API key is required; Apify Actor fees and platform usage apply.

Find law firms by practice area and location, extract publicly listed business emails and social links, and export the results as JSON, CSV, or Excel. Useful for legal technology sales, agency prospecting, and business research.

### Who this helps

- Legal software: research firms by practice area and territory.
- Agencies: build a segmented list for a specific service and inspect each firm before outreach.
- Repeat research: use locations plus excludeMapsUrls to avoid re-collecting known listings.

### Quick start

1. Choose a practice area and a city, such as **Personal Injury Lawyer — Chicago, IL**.
2. The default is **3 results**. Start with **3–10 results** and inspect the output.
3. Email extraction is on by default. Enable optional email verification only when you need DNS/SMTP evidence; social enrichment is also off by default.
4. Export the dataset. Use Full output to retain all verification evidence.

A result is a **business record**, not a guaranteed individual-attorney email. Firms using contact forms or inaccessible websites may have no extracted email.

### Pricing

The currently configured base event price is **$50 per 1,000 returned business records ($0.05 per result)**, plus **$0.00005 per Actor-start event**. Start-event quantity depends on allocated memory (one per GB, minimum one); at 4 GB this is $0.0002. **Apify platform usage is charged separately**. The live Pricing tab is authoritative and may show applicable Store discounts.

| Returned records | Result fees before platform usage and startup |
|---|---:|
| 10 | $0.50 |
| 100 | $5.00 |
| 500 | $25.00 |

Records without an email are still chargeable business records. Platform usage can occur even if a search returns no results or fails. Set Apify's run timeout and spending limits before running a large job. These prices do not imply a guarantee of email availability or deliverability.

### Email verification: what the fields mean

- **emailDomainHasMx**: whether the domain has mail-exchange records. This does not verify an individual mailbox.
- **emailVerified: true**: SMTP accepted the recipient and rejected a random-recipient probe. This is evidence at check time, not a delivery guarantee.
- **smtp_unavailable**, **dns_unavailable**, **smtp_accepted_unconfirmed**, **catch_all**, and **unknown**: mailbox verification was inconclusive. These never produce a true verified flag.
- **undeliverable**, **invalid_format**, and **no_mx_records** identify negative checks; inspect the status rather than treating every false flag as an invalid mailbox.

Some providers block SMTP verification or accept all recipients. We do not guarantee a bounce-rate percentage. Verification can be disabled if you use a separate service.

### Inputs

| Input | Purpose |
|---|---|
| businessType | Practice area; choose Custom for a custom query |
| location | City or region for a single search |
| locations | Optional list of up to 20 locations; overrides location |
| searchQuery | Custom query; use {location} when searching multiple cities |
| maxResults | Total result cap across all locations, 1–500 |
| excludeMapsUrls | Optional Maps URLs from an earlier export to exclude |
| enrichEmails | Extract email addresses from business websites |
| verifyEmails | Attempt DNS and SMTP checks on extracted emails |
| enrichSocials | Extract linked social profiles |
| outputFormat | full, hubspot, or salesforce |
| maxRuntimeSeconds | Soft processing budget checked between stages/batches |
| proxyUrl | Optional custom proxy; otherwise uses configured Apify proxy |

### Multi-city and repeat workflows

Use locations for a single run across several cities. Results are deduplicated across the run by Maps identity and normalized business name/address. maxResults applies to the combined output.

For Custom searches, include a placeholder, such as **employment attorney in {location}**. Supply previously collected Maps URLs in excludeMapsUrls for a new-record workflow. This excludes matching businesses; it is not an automatic change-detection or update service.

The run checkpoints collected records and progress in its own default key-value store. Resurrecting an interrupted run reuses those checkpoints and reconciles completed dataset writes. Starting a separate run starts a fresh search. No shared customer dataset is used.

Completed enrichment batches are written as they finish. A failed or interrupted run can therefore contain useful partial output. The OUTPUT_SUMMARY key-value record reports result counts, email coverage, SMTP-confirmed counts, first-result timing, and completion status.

#### Get past previously collected listings

For repeat searches, set **maxCandidatesPerLocation** above **maxResults** and supply Maps URLs from your previous export in **excludeMapsUrls**. For example, scan up to 50 candidates to return at most 10 records. Excluded and duplicate records are removed before email/social enrichment and dataset output. The scan limit is optional (1–500 per location); omitting it preserves the previous remaining-result search limit.

A larger scan can increase separately billed platform usage and run time. It does not guarantee ten new records or complete coverage. Keep a run timeout and spending limit. A `no_new_results` summary means no new records were found in the inspected candidates, not that the entire city has no more businesses.

```json
{
  "businessType": "Personal Injury Lawyer",
  "location": "Chicago, IL",
  "maxResults": 10,
  "maxCandidatesPerLocation": 50,
  "excludeMapsUrls": [],
  "enrichEmails": true,
  "verifyEmails": false,
  "enrichSocials": false,
  "outputFormat": "full"
}
```

Replace the empty exclusion array with your own prior Maps URLs. CID, place-ID and common embedded feature-ID links are normalized where available; different URLs are not always proof of different businesses. No history is shared across customer accounts.

### Output and CRM imports

Full output includes name, category, address, phone, website, mapsUrl, rating, reviewCount, email, emailVerified, emailDomainHasMx, emailVerificationStatus, emailVerificationNote, allEmails, and socialLinks.

HubSpot and Salesforce modes provide convenient column names, but imports may require field mapping or custom properties in your CRM. Full output retains the most verification detail.

### Reliability and limits

Google Maps coverage varies by query and location. Email extraction depends on publicly accessible website content. Business listings are not a verified directory of individually licensed attorneys.

Confirmed no-match searches are reported separately from source blocks or extraction failures. If a run fails, inspect its status and log before retrying. Partial records may already be present and billed. Start small when changing locations or query types.

For bulk use, keep a reasonable time and spending cap. The soft processing budget is checked between stages; an active network operation may finish after that budget. Apify's run timeout provides a separate hard control.

### Automation

Use Apify Saved Tasks, schedules, API calls, and dataset exports for recurring workflows. Review data quality before sending leads into a CRM or outreach system. Use professional contact data responsibly and follow the rules applicable to your use.

### Support and feedback

Open an issue with your search settings, expected result, and a fresh shared run link. Do not post API tokens, proxy credentials, or sensitive customer data. If the Actor is useful, an honest Store review helps other users assess it.

### September 2026 update

- Domain validation no longer labels an unconfirmed mailbox as verified.
- Added bounded concurrent verification and catch-all handling.
- Added multi-location inputs, cross-location deduplication, exclusions, checkpoints, and batched result delivery.
- Source extraction errors no longer become silent empty successes.
- Corrected pricing examples and clarified email and CRM limitations.

### Copyable input for your next run

This is a configuration example, not a guaranteed result count. Start small and set a run-cost limit. The result limit is shared across both cities. Add previous Maps URLs to excludeMapsUrls when you want new listings.

```json
{
  "businessType": "Personal Injury Lawyer",
  "location": "Chicago, IL",
  "locations": [
    "Chicago, IL",
    "Austin, TX"
  ],
  "maxResults": 10,
  "excludeMapsUrls": [],
  "enrichEmails": true,
  "verifyEmails": false,
  "enrichSocials": false,
  "outputFormat": "full"
}
```

### Related business segments

Use the dedicated [Lawyer](https://apify.com/samstorm/lawyer-lead-scraper), [Dentist](https://apify.com/samstorm/dentist-lead-scraper), or [Med Spa](https://apify.com/samstorm/med-spa-lead-scraper) Actor for the segment you serve. Each has a small sample on its Examples tab.

### Walkthrough: Multi-city law firm leads to Google Sheets

This starter workflow connects this Actor to a Google Sheet using n8n. It starts with ten results and uses your own service credentials. The Actor extracts public business information; email availability varies and the template does not send outreach.

#### 1. Check a small sample

Open the [prepared sample](https://apify.com/samstorm/lawyer-lead-scraper/examples/demo---pi-lawyers-in-chicago), review its settings and current Pricing tab, and inspect the resulting names, source URLs and contact fields. A sample run has normal Apify charges; it is not a promise of free data or a fixed number of matches.

#### 2. Import and connect

Copy the workflow JSON below into a local .json file, then use n8n's **Import from File**. Alternatively paste the copied JSON onto a blank n8n canvas. The template uses built-in nodes and remains inactive.

Create/select an n8n **Header Auth** credential for your Apify account: header name **Authorization**, value **Bearer YOUR_APIFY_TOKEN**. Assign it to **Start Actor**, **Get run**, and **Get dataset**. Keep secrets in credentials, never in workflow JSON or public issues.

Create a Google Sheet with a tab named **Leads**. Put the following field names across row 1, one field per column, in this order:

```text
mapsUrl	name	category	address	phone	website	email	emailVerified	emailVerificationStatus	emailVerificationNote	emailDomainHasMx	rating	reviewCount	sourceRunId	capturedAt
```

Select your Google Sheets credential in **Append or update rows**. In **Configure**, replace sheetDocumentId with the ID between /d/ and /edit in your Sheet URL. The match column is **mapsUrl**. Values are written as RAW text/data so scraped text is not executed as a spreadsheet formula.

#### 3. Run once and inspect

In **Configure**, change businessType and location for your market. The locations array overrides location and shares the total maxResults cap. Keep the required location field present. Keep outputFormat **full**, which includes verification evidence. Email enrichment is on; SMTP verification and social enrichment are off initially. Enable them only when needed and review the current price.

Click **Execute workflow**. The Actor starts once, waits for completion, validates the dataset, and exports. Failed/aborted/timed-out runs and empty datasets stop before the destination write. Duplicate Maps URLs within a batch are removed. The Sheet appends new Maps URLs and updates matching rows. Blank fields can replace earlier values, so use a dedicated data sheet rather than your manually curated CRM.

Check your first exported rows against the Apify dataset. Do not interpret MX records, catch-all results or an unconfirmed SMTP check as a verified mailbox. The template marks emailVerified true only when the Actor also reports smtp_verified. No mailbox check guarantees delivery.

#### 4. Repeat without avoidable costs

If export fails after scraping succeeded, copy the 17-character run ID from the Apify run page into **existingRunId** and execute again. This retries export without launching another paid scrape. Leave it blank when you want a fresh search. The template retrieves up to 101 records and fails above 100; use pagination before scaling beyond 100. Do not point it at a larger historical dataset expecting complete export.

For new prospects, add previous Maps URLs to excludeMapsUrls and rotate the locations or segment. Repeated searches can return the same listings. Destination deduplication does not make repeated scraping free.

Only after a successful manual test, optionally replace Manual start with an n8n Schedule Trigger. Choose your timezone and cadence, publish the workflow, prevent overlapping executions, and set a monthly budget. Keep existingRunId blank for fresh scheduled searches. Configure an n8n Error Workflow/notification for failed executions. This starter does not create a schedule or send alerts automatically.

#### Costs and compatibility

Each fresh execution can incur Actor result fees, applicable enrichment fees and separately billed Apify platform usage, plus any n8n/destination subscription costs. The default $2 maxTotalChargeUsd limits **Actor event charges**, not separately billed platform usage. The Actor timeout is 600 seconds; n8n execution timeout is 900 seconds. Consult the current Pricing tab before scaling.

The template uses HTTP Request 4.2, Code 2, IF 2.2, Wait 1.1 and Google Sheets 4.6. Local tests cover mapping and failure handling; a complete run against your own n8n and Google Sheets credentials is required before production use.

#### Manual export alternative

Run the Actor with full output, open the dataset, and export CSV or Excel. Import into a dedicated Google Sheet and deduplicate on mapsUrl.

#### Troubleshooting

- **401/403:** check which service's credential is selected and its permissions.
- **No leads / source failure:** inspect the Apify run log and OUTPUT_SUMMARY; try a narrower, known location.
- **Destination error:** fix the destination and retry using existingRunId.
- **Unexpected duplicates:** compare Maps URLs, and avoid concurrent writes. Do not rename the mapsUrl match column.
- **Need help:** open an Actor issue with the run link, business type, location and expected/actual behavior. Do not include tokens or private customer data.

References: [n8n workflow import](https://docs.n8n.io/workflows/export-import/), [Apify API](https://docs.apify.com/api/v2), [Sheets append or update](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets/sheet-operations/).

#### Importable n8n workflow JSON

```json
{"name":"Multi-city law firm leads to Google Sheets | samstorm","nodes":[{"name":"Manual start","id":"manual-start","type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[0,300],"parameters":{}},{"name":"Configure","id":"configure","type":"n8n-nodes-base.code","typeVersion":2,"position":[220,300],"parameters":{"jsCode":"// Edit these values. Existing run ID reuses results without starting a new paid run.\nconst config={\n  \"actorId\": \"f6o4KrbNJeGC0AnqF\",\n  \"existingRunId\": \"\",\n  \"sheetDocumentId\": \"REPLACE_WITH_GOOGLE_SHEET_ID\",\n  \"sheetName\": \"Leads\",\n  \"actorInput\": {\n    \"businessType\": \"Personal Injury Lawyer\",\n    \"location\": \"Chicago, IL\",\n    \"locations\": [\n      \"Chicago, IL\",\n      \"Austin, TX\"\n    ],\n    \"excludeMapsUrls\": [],\n    \"maxResults\": 10,\n    \"enrichEmails\": true,\n    \"verifyEmails\": false,\n    \"enrichSocials\": false,\n    \"outputFormat\": \"full\"\n  },\n  \"maxActorEventChargeUsd\": 2,\n  \"timeoutSeconds\": 600\n};\nif(!Number.isInteger(config.actorInput.maxResults) || config.actorInput.maxResults<1 || config.actorInput.maxResults>100) throw new Error('Set maxResults between 1 and 100.');\nif(config.actorInput.outputFormat!=='full') throw new Error('Keep outputFormat full for this mapping.');\nif(config.existingRunId && !/^[a-zA-Z0-9]{17}$/.test(config.existingRunId)) throw new Error('Enter the 17-character Apify run ID, not a URL.');\nif(!/^[a-zA-Z0-9_-]{20,}$/.test(config.sheetDocumentId) || config.sheetDocumentId.includes('REPLACE')) throw new Error('Set your Google Sheet document ID in Configure before starting.');\nreturn [{json:config}];"}},{"name":"Reuse existing run","id":"reuse-existing-run","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[440,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"Reuse existing run","leftValue":"={{ !!$json.existingRunId }}","rightValue":"","operator":{"type":"boolean","operation":"true"}}],"combinator":"and"},"options":{}}},{"name":"Start Actor","id":"start-actor","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[660,440],"parameters":{"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{"timeout":75000,"response":{"response":{"responseFormat":"json"}}},"method":"POST","url":"=https://api.apify.com/v2/actors/{{ $json.actorId }}/runs","sendQuery":true,"queryParameters":{"parameters":[{"name":"build","value":"latest"},{"name":"waitForFinish","value":"60"},{"name":"timeout","value":"={{ $json.timeoutSeconds }}"},{"name":"memory","value":"4096"},{"name":"maxTotalChargeUsd","value":"={{ $json.maxActorEventChargeUsd }}"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ $json.actorInput }}"},"retryOnFail":false,"maxTries":3,"waitBetweenTries":2000,"notes":"Choose your own Header Auth credential. Authorization: Bearer YOUR_TOKEN. Never embed tokens in this template.","notesInFlow":false},{"name":"Get run","id":"get-run","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[660,140],"parameters":{"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{"timeout":75000,"response":{"response":{"responseFormat":"json"}}},"url":"=https://api.apify.com/v2/actor-runs/{{ $json.runId || $('Configure').first().json.existingRunId }}","sendQuery":true,"queryParameters":{"parameters":[{"name":"waitForFinish","value":"60"}]}},"retryOnFail":true,"maxTries":3,"waitBetweenTries":2000,"notes":"Choose your own Header Auth credential. Authorization: Bearer YOUR_TOKEN. Never embed tokens in this template.","notesInFlow":false},{"name":"Validate run","id":"validate-run","type":"n8n-nodes-base.code","typeVersion":2,"position":[880,300],"parameters":{"jsCode":"const r=$input.first().json.data;\nconst config=$('Configure').first().json;\nif(!r || !/^[a-zA-Z0-9]{17}$/.test(r.id || '') || r.actId!==config.actorId) throw new Error('Unexpected Apify run response or wrong Actor. No records exported.');\nif(!['READY','RUNNING','SUCCEEDED'].includes(r.status)) throw new Error('Apify run '+r.id+' ended '+r.status+'. Inspect its log and partial dataset; no CRM write performed.');\nif(r.status==='SUCCEEDED' && !/^[a-zA-Z0-9]{17}$/.test(r.defaultDatasetId || '')) throw new Error('Succeeded run has no valid dataset ID.');\nreturn [{json:{runId:r.id,datasetId:r.defaultDatasetId || '',complete:r.status==='SUCCEEDED'}}];"}},{"name":"Run succeeded","id":"run-succeeded","type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1100,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"Run succeeded","leftValue":"={{ $json.complete }}","rightValue":"","operator":{"type":"boolean","operation":"true"}}],"combinator":"and"},"options":{}}},{"name":"Wait before next check","id":"wait-before-next-check","type":"n8n-nodes-base.wait","typeVersion":1.1,"position":[1100,100],"parameters":{"amount":15,"unit":"seconds"}},{"name":"Get dataset","id":"get-dataset","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1320,300],"parameters":{"authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{"timeout":60000,"response":{"response":{"responseFormat":"json","fullResponse":true}}},"url":"=https://api.apify.com/v2/datasets/{{ $json.datasetId }}/items","sendQuery":true,"queryParameters":{"parameters":[{"name":"format","value":"json"},{"name":"clean","value":"true"},{"name":"limit","value":"101"}]}},"retryOnFail":true,"maxTries":3,"waitBetweenTries":2000,"notes":"Choose your own Header Auth credential. Authorization: Bearer YOUR_TOKEN. Never embed tokens in this template.","notesInFlow":false},{"name":"Validate and map leads","id":"validate-and-map-leads","type":"n8n-nodes-base.code","typeVersion":2,"position":[1540,300],"parameters":{"jsCode":"const payload=$input.first().json;\nconst rows=payload.body;\nif(!Array.isArray(rows)) throw new Error('Expected a JSON dataset array. Check the Get dataset node response settings.');\nif(!rows.length) throw new Error('No leads returned. Review the Apify log and OUTPUT_SUMMARY before changing the search.');\nif(rows.length>100) throw new Error('This starter workflow supports at most 100 records; use dataset pagination for larger runs.');\nconst seen=new Set();\nconst result=[];\nconst runId=$('Validate run').last().json.runId;\nconst text=v=>typeof v==='string'?v.trim():'';\nfor(const row of rows){\n const name=text(row.name), mapsUrl=text(row.mapsUrl).split('#')[0];\n if(!name || !/^https:\\/\\/(?:[a-z0-9-]+\\.)?google\\.[a-z.]+\\/maps(?:\\/|\\?)/i.test(mapsUrl)) throw new Error('A record lacks a business name or valid Google Maps source. Nothing in this batch was exported.');\n if(seen.has(mapsUrl)) continue;\n seen.add(mapsUrl);\n const status=text(row.emailVerificationStatus);\n const email=text(row.email);\n result.push({json:{mapsUrl,name,category:text(row.category),address:text(row.address),phone:text(row.phone),website:text(row.website),email,\n emailVerified:!!email && row.emailVerified===true && status==='smtp_verified',\n emailVerificationStatus:status || (email?'not_checked':'no_email'),emailVerificationNote:text(row.emailVerificationNote),\n emailDomainHasMx:typeof row.emailDomainHasMx==='boolean'?row.emailDomainHasMx:'',\n rating:typeof row.rating==='number'?row.rating:'',reviewCount:typeof row.reviewCount==='number'?row.reviewCount:'',sourceRunId:runId,capturedAt:new Date().toISOString()}});\n}\nreturn result;"}},{"name":"Append or update rows","id":"append-or-update-rows","type":"n8n-nodes-base.googleSheets","typeVersion":4.6,"position":[1760,300],"parameters":{"operation":"appendOrUpdate","documentId":{"__rl":true,"value":"={{ $('Configure').first().json.sheetDocumentId }}","mode":"id"},"sheetName":{"__rl":true,"value":"={{ $('Configure').first().json.sheetName }}","mode":"name"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":["mapsUrl"],"schema":[{"id":"mapsUrl","displayName":"mapsUrl","required":false,"defaultMatch":true,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"name","displayName":"name","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"category","displayName":"category","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"address","displayName":"address","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"phone","displayName":"phone","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"website","displayName":"website","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"email","displayName":"email","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"emailVerified","displayName":"emailVerified","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"emailVerificationStatus","displayName":"emailVerificationStatus","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"emailVerificationNote","displayName":"emailVerificationNote","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"emailDomainHasMx","displayName":"emailDomainHasMx","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"rating","displayName":"rating","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"reviewCount","displayName":"reviewCount","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"sourceRunId","displayName":"sourceRunId","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true},{"id":"capturedAt","displayName":"capturedAt","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{"cellFormat":"RAW","handlingExtraData":"error"}}},{"name":"Setup and limits","id":"setup-and-limits","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[0,-300],"parameters":{"content":"## Multi-city law firm leads to Google Sheets\n1. Read the setup walkthrough on the Actor README.\n2. Assign YOUR Apify Header Auth to the three Apify HTTP nodes.\n3. Select Google Sheets OAuth on Append or update rows. Create headers and set document ID.\n4. Review Configure, then test manually.\n\nDefaults: 10 results, verification off, 600-second Actor timeout. $2 is an Actor-event charge limit; separately billed platform usage still applies. Failed/empty runs stop export.\n\nTo retry export after a destination error, set existingRunId. Do not restart the Actor unnecessarily.\n\nNo automatic schedule. Prevent concurrent executions against the same destination.\nValidation: local control-flow/mapping tests; customer credential setup and live destination test required.","width":560,"height":520}}],"connections":{"Manual start":{"main":[[{"node":"Configure","type":"main","index":0}]]},"Configure":{"main":[[{"node":"Reuse existing run","type":"main","index":0}]]},"Reuse existing run":{"main":[[{"node":"Get run","type":"main","index":0}],[{"node":"Start Actor","type":"main","index":0}]]},"Start Actor":{"main":[[{"node":"Validate run","type":"main","index":0}]]},"Get run":{"main":[[{"node":"Validate run","type":"main","index":0}]]},"Validate run":{"main":[[{"node":"Run succeeded","type":"main","index":0}]]},"Run succeeded":{"main":[[{"node":"Get dataset","type":"main","index":0}],[{"node":"Wait before next check","type":"main","index":0}]]},"Wait before next check":{"main":[[{"node":"Get run","type":"main","index":0}]]},"Get dataset":{"main":[[{"node":"Validate and map leads","type":"main","index":0}]]},"Validate and map leads":{"main":[[{"node":"Append or update rows","type":"main","index":0}]]}},"active":false,"settings":{"executionOrder":"v1","executionTimeout":900,"saveDataErrorExecution":"all","saveDataSuccessExecution":"none"},"pinData":{},"tags":[]}
```

### Email source and association checks — October 2026

Full output now includes `emailSource`, `emailDomainRelationship` and `emailNeedsReview`; each item in `allEmails` keeps its source URL and extraction method. The crawler reads published text and email links, removes hidden templates and placeholder addresses, and follows at most two additional contact/about links. It retains the actual website path supplied by Maps. Static pages that block requests or require JavaScript can still hide contacts.

`same_domain` means the email domain matches the website host or a subdomain. `public_mailbox_provider` preserves addresses such as Gmail and Yahoo; `other_domain` can be a parent company, vendor or unrelated contact. Both require source review. These labels do not verify business ownership, permission or deliverability. SMTP status remains a separate check. Full output is recommended for evidence review before CRM import.

### Repeat searches and city scope

Use `excludeMapsUrls` with Maps URLs from a previous Full export. Exclusions are applied before place-detail visits. By default, the candidate scan budget is the output cap plus exclusions, capped at 500; `maxCandidatesPerLocation` overrides it. `maxResults` remains the output cap. A recognizable US `City, ST` request checks the detailed address locality; this is not a geographic boundary lookup. Custom queries that do not include the configured location keep their query scope. Inspect `OUTPUT_SUMMARY` for scan counts and partial results when Google stops loading or the time limit is reached.

`emailPurpose` flags obvious press, recruitment and privacy mailbox names. These specialized addresses remain in the evidence but are ranked below ordinary contacts and require review. This is a name-based heuristic, not a guarantee of recipient role.

# Actor input Schema

## `businessType` (type: `string`):

Select the type of legal professional to find on Google Maps. The actor automatically constructs the search query — for example, selecting 'Personal Injury Lawyer' with location 'Los Angeles, CA' finds personal injury attorneys in that city. Select 'Criminal Defense Attorney' to build a DUI or felony defense law firm list. Choose 'Custom' to write your own search query instead.

## `location` (type: `string`):

The city, state, or region to search for law firms in. Use 'City, ST' format for US locations — for example, 'Los Angeles, CA' for personal injury attorneys or 'Houston, TX' for immigration lawyers. You can also use ZIP codes, neighborhoods, or broader regions like 'Downtown Manhattan'. The more specific your location, the more targeted your attorney lead list.

## `searchQuery` (type: `string`):

A raw Google Maps search query, used only when Business Type is set to 'Custom'. For all other types, the search is built automatically from Business Type + Location. Write queries the same way you would type them into Google Maps — for example: 'workers compensation lawyers in Phoenix AZ', 'employment attorney Manhattan', or 'real estate attorney 90210'.

## `maxResults` (type: `integer`):

Maximum business records returned across all locations combined (1–500). The default is 3. Start with 3–10 records to validate coverage and data quality. Maps may return fewer matching businesses.

## `enrichEmails` (type: `boolean`):

When enabled, the actor visits each law firm website and scans contact pages, footers, and about pages to extract email addresses. Disable this option if you only need phone numbers and addresses, or want faster results without website crawling.

## `verifyEmails` (type: `boolean`):

Optional DNS and SMTP checks. Off by default to reduce waiting; enable when you need verification evidence. True emailVerified requires recipient acceptance plus rejection of a random recipient. MX records, catch-all servers and inconclusive checks do not confirm a mailbox. Delivery is not guaranteed.

## `enrichSocials` (type: `boolean`):

Optional collection of public social profile links from business websites. Off by default; enable when social links are useful for your research.

## `outputFormat` (type: `string`):

Full preserves source URLs, email verification status and domain evidence. HubSpot and Salesforce provide column mappings; review destination fields and custom properties before import. CSV, JSON and Excel exports are available from the dataset.

## `proxyUrl` (type: `string`):

Optional HTTP proxy URL for using third-party residential proxies instead of Apify's built-in proxies. Format: http://username:password@host:port. Compatible with IPRoyal, Smartproxy, Bright Data, and other rotating proxy services. Leave empty to use Apify's default residential proxies.

## `locations` (type: `array`):

Up to 20 cities or regions. Overrides Location when supplied. Max Results is a total cap across all locations. For a Custom query, include {location} in the query.

## `excludeMapsUrls` (type: `array`):

Exclude previously collected businesses by their Maps URL. Supply up to 5000 URLs from an earlier export to collect new records without automatically sharing data across runs.

## `maxRuntimeSeconds` (type: `integer`):

Stop starting new work after this time budget. An in-progress scrape or enrichment batch may take additional time. Use Apify run timeout and spending controls for hard limits.

## `maxCandidatesPerLocation` (type: `integer`):

Optional: scan more candidate listings before quality checks. For example, scan 50 to return at most 10 new records. When empty, the scan limit is the remaining result count plus the number of excluded Maps URLs, capped at 500. Larger scans can increase platform usage and runtime; maxResults still caps returned records. Source and time limits may yield fewer matches.

## Actor input object example

```json
{
  "businessType": "Family Lawyer",
  "location": "Chicago, IL",
  "searchQuery": "immigration lawyers in Miami, FL",
  "maxResults": 50,
  "enrichEmails": true,
  "verifyEmails": false,
  "enrichSocials": false,
  "outputFormat": "hubspot",
  "proxyUrl": "http://user:pass@proxy.example.com:8080",
  "maxRuntimeSeconds": 600
}
```

# Actor output Schema

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

Business records in the requested output format. Full output includes Google Maps source links and email-verification evidence; CRM formats use different column names.

## `summary` (type: `string`):

Completed-run counts, coverage, exclusions, first-result timing and partial-run outcome. Available after the Actor writes OUTPUT_SUMMARY; use this to distinguish no matches from no new records or a time-limited search.

# 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 = {
    "location": "Chicago, IL",
    "searchQuery": "personal injury lawyers in Chicago, IL",
    "maxResults": 3,
    "verifyEmails": false,
    "enrichSocials": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("samstorm/lawyer-lead-scraper").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 = {
    "location": "Chicago, IL",
    "searchQuery": "personal injury lawyers in Chicago, IL",
    "maxResults": 3,
    "verifyEmails": False,
    "enrichSocials": False,
}

# Run the Actor and wait for it to finish
run = client.actor("samstorm/lawyer-lead-scraper").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 '{
  "location": "Chicago, IL",
  "searchQuery": "personal injury lawyers in Chicago, IL",
  "maxResults": 3,
  "verifyEmails": false,
  "enrichSocials": false
}' |
apify call samstorm/lawyer-lead-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,samstorm/lawyer-lead-scraper"
        }
    }
}
```

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/f6o4KrbNJeGC0AnqF/builds/3EjfBOfzbJ6SROkwn/openapi.json
