# Justia Lawyer Directory Scraper — Attorney Leads & Phones (`scrapersdelight/justia-lawyer-scraper`) Actor

One row per attorney from the Justia lawyer directory: firm, practice areas with sub-areas, every listed phone, website, office address with lat/lon, admission year, law school, badges and client rating. Pick state, city and practice area from dropdowns, or paste profile URLs. No login, no API key.

- **URL**: https://apify.com/scrapersdelight/justia-lawyer-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Lead generation, Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 per attorney returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Justia Lawyer Directory Scraper — Attorney Leads & Phones

One row per attorney from the [Justia lawyer directory](https://lawyers.justia.com), carrying
**fullName, firmName, practiceAreas with sub-areas, phone (every listed number), website,
contactFormUrl, street / city / state / zip, latitude / longitude, yearAdmitted, yearsLicensed,
lawSchool and degree, badges, listingTier, client rating, languages, fee terms, bar associations,
awards and the attorney's Avvo / Martindale / Lawyers.com profile links**.

Filter by **state, city and practice area** — all picked from dropdowns, no slugs to type — or
paste attorney profile URLs and scrape exactly those.

**Every row is a claimed, self-maintained profile.** Measured, not assumed: **360 of 360 rows
across two independent samples carried the "Claimed Lawyer Profile" badge**, with phone fill at
**99.5–100%**. An unclaimed directory stub is a name and a city. A claimed profile is the firm, the
phone, the website, the practice-area tree and the credentials below.

No login. No API key. No CAPTCHA-solving service. **Justia publishes no email address for any
attorney, and none is invented here** — see the FAQ for what you get instead.

### 🚀 Quick start

The Actor ships prefilled with a working demo — **Texas → Austin → 100 attorneys**. Press **Start**
and you get rows without typing anything. Measured on the Apify platform with exactly that input,
2026-08-13: **100 attorneys in 2 min 8 s** — 20 listing pages plus 102 profile pages, **0 failed
requests**, 1 challenge-token mint, 0 rate-limit pauses, and **100 rows billed at $0.30**.

```jsonc
{
  "states": ["texas"],     // picked from a dropdown of the directory's own 52 state slugs
  "cities": ["austin"],    // leave empty to crawl the whole state
  "maxItems": 100          // 100 × $0.003 = $0.30, your hard cost ceiling
}
```

Three more inputs worth copying:

```jsonc
// A whole practice area, statewide — the classic lead pull
{ "states": ["florida"], "practiceAreas": ["personal-injury"], "maxItems": 2000 }

// Several metros at once. Pair each city with its state, or every city is tried in every state
{ "states": ["texas", "new-york"], "cities": ["texas/houston", "new-york/new-york"],
  "practiceAreas": ["estate-planning"], "withWebsiteOnly": true, "maxItems": 1000 }

// Refresh a known list of attorneys — no listing crawl, one fetch per URL
{ "profileUrls": ["https://lawyers.justia.com/lawyer/richard-m-ursha-207812"],
  "includeBiography": true }
```

### ✨ Why use this scraper

- **Claimed profiles only, measured at 360/360.** The rows come from the side of the directory
  attorneys maintain themselves, which is why the contact fields are actually filled in.
- **The practice-area *tree*, not a tag list.** Each row carries `practiceAreaDetail` — the area
  plus its sub-areas ("Criminal Law → Drug Crimes, Expungement, Fraud, Gun Crimes, Theft, Violent
  Crimes"). Attorneys average **2.9 areas** in a metro crawl and **3.7** in a whole-state crawl.
- **You can see who already buys legal marketing.** `listingTier` and `badges` expose the paid
  placement level (PREMIUM / LII Platinum / Gold / Silver / Claimed). The metro sample was
  **203 of 220 Premium**; the whole-state sample **55 of 140**. If you sell to law firms, that
  flag *is* the qualified/unqualified split.
- **Cross-directory identity for free.** `otherDirectoryProfiles` returns the same attorney's
  Avvo, Martindale, Lawyers.com and Super Lawyers URLs — **74.1%** of metro rows, **35.7%** of
  whole-state rows — so you can join this dataset to those directories without a fuzzy match.
- **Every phone number, not just the first.** `phones` is the full list, deduplicated on digits so
  the same number formatted two ways does not look like two numbers. `phone` is the primary.
- **Geocoded offices.** `latitude`/`longitude` at **100%** in both samples, plus `offices[]` for
  attorneys who list more than one address.
- **It handles the site's edge challenge itself.** One headless page load lets the site's own
  challenge script issue a token, then the crawl runs on plain HTTP with it. Measured on the last
  full validation run: **235 of 235 requests clean, 0 failures**. Runs **direct, with no proxy**.
- **Client rating is on a 0–10 scale, and `ratingScale` says so on every row** — no silent
  five-star/ten-point mixups when you load it into a warehouse.

### 🎯 Use cases

| You are a… | Use it to… |
|---|---|
| Legal-SaaS / practice-management sales (Clio, MyCase, Filevine, Smokeball, Lawmatics) | Segment by `practiceAreas` + firm, call the phone, enrich the `website` domain |
| Legal marketing / SEO agency | Target the attorneys already paying for placement — filter your export on `listingTier` = PREMIUM and the LII badges |
| Malpractice & professional-liability broker | Cut by `jurisdictions`, `yearAdmitted` and practice mix; `minYearsLicensed` does the tenure filter in-run |
| Legal recruiter | Filter on `lawSchool`, `lawSchoolGraduationYear`, `yearsOfExperience` and sub-areas after export |
| Expert-witness / co-counsel desk | Find niche sub-areas, `languages` and multi-jurisdiction admissions in a named metro |
| Legal-data / directory product | Join to Avvo, Martindale, Lawyers.com and Super Lawyers on `otherDirectoryProfiles` instead of name-matching |

### 📥 Supported inputs

| Input | Example | What it does |
|---|---|---|
| State | `texas` (picked from a dropdown) | Crawls that whole state's listing |
| State + city | `texas` + `austin`, or `texas/austin` | Crawls the city listing (attorneys **serving** that city) |
| State (+ city) + practice area | `florida` + `personal-injury` | One listing per state × city × area |
| Attorney profile URL | `https://lawyers.justia.com/lawyer/jane-doe-123456` | Scrapes that attorney directly, no listing crawl. The LII host URL works too |

**Not supported:** email addresses (the directory publishes none — see the FAQ), law-firm
`/organization/…` pages, case law, dockets, client reviews as individual rows, and anything behind
a login. A practice area on its own is also not a valid scope: the directory only lists attorneys
under a state, or a state plus a city.

### Read this before you buy rows

1. **A city listing returns attorneys *serving* that city**, drawn from the surrounding region —
   not only attorneys with an office in it. A crawl of `texas/austin` legitimately returns Houston
   and San Antonio firms. Each row's own `city`/`state` is the attorney's actual office, so filter
   on that if you need strict geography.
2. **Fill depends on the market you crawl, and the gap is large.** Big-metro listings are dominated
   by attorneys paying for Premium placement, who fill their profiles in completely; a whole-state
   crawl reaches ordinary claimed profiles. Both samples are published below — plan against the one
   that matches your scope, not against the better-looking one.
3. **`rating` is out of 10, not out of 5**, and only a minority of profiles have one: **30.9%** in
   the metro sample, **4.3%** in the whole-state sample. `ratingScale` is on every row. This is not
   a ratings product.
4. **`yearAdmitted` is the earliest admission year printed on the profile**, across all listed
   jurisdictions including federal courts, so an attorney who lists only a federal admission
   under-states their tenure. Roughly **1 in 4** profiles print the bar organisation ("State Bar of
   Montana") in that slot instead of a year; those come back in `jurisdictions[].barOrganization`
   with `yearAdmitted: null` rather than a guess.
5. **The listing repeats itself; the dataset does not.** Every listing page reprints its sponsored
   block. Deduplication happens on the numeric profile id **before any profile is fetched**, so
   duplicates never reach the dataset and never reach your bill.

### 📦 Output sample

One real row, from a real run:

```jsonc
{
  "profileId": "207812",
  "fullName": "Richard M. Ursha",
  "firmName": "Richard Ursha, Attorney at Law PLLC",
  "jobTitle": "Attorney",

  "yearsOfExperience": 24,
  "yearAdmitted": 2002,
  "yearsLicensed": 24,

  "practiceAreas": ["Criminal Law", "DUI & DWI", "Domestic Violence", "Juvenile Law", "White Collar Crime"],
  "practiceAreaDetail": [
    { "area": "Criminal Law", "subAreas": ["Drug Crimes", "Expungement", "Fraud", "Gun Crimes", "Theft", "Violent Crimes"] }
  ],

  "phone": "(512) 847-1717",
  "phones": ["(512) 847-1717", "(512) 512-1179", "(512) 738-1266"],
  "website": "http://www.hillcountrylawyers.com/Attorney-Profile.aspx",
  "contactFormUrl": "https://lawyers.law.cornell.edu/lawyer/richard-m-ursha-207812/contact",

  "street": "2401 Hunter Road, Ste. 200",
  "city": "San Marcos", "state": "TX", "zip": "78666", "country": "US",
  "latitude": 29.8594032, "longitude": -97.9665581,

  "lawSchool": "South Texas College of Law Houston",
  "lawSchoolDegree": "J.D.", "lawSchoolGraduationYear": 2002,
  "jurisdictions": [{ "jurisdiction": "Texas", "admittedYear": 2002, "barOrganization": null }],

  "badges": ["Claimed Lawyer Profile", "LII Platinum", "Social Media"],
  "isClaimed": true, "listingTier": "PREMIUM",
  "rating": 10, "ratingScale": 10, "reviewCount": 2,

  "languages": ["English"],
  "fees": ["Free Consultation", "Credit Cards Accepted"],
  "freeConsultation": true, "creditCardsAccepted": true,
  "associations": [{ "organization": "Texas State Bar # 24037624", "role": "Member" }],

  "socialProfiles": { "facebook": "…", "linkedin": "…", "twitter": "…" },
  "otherDirectoryProfiles": ["https://www.avvo.com/…", "https://www.martindale.com/…"],
  "justiaProfileUrl": "https://lawyers.justia.com/lawyer/richard-m-ursha-207812"
}
```

Three fields people misread:

- `rating: 10` is **ten out of ten**, not a percentage and not a 0–5 star score. `ratingScale`
  carries the denominator.
- `reviewCount: 0` means Justia published a rating block with no reviews; `null` means it published
  no rating block at all. They are different states and are not merged.
- `yearsOfExperience` is **self-reported by the attorney**; `yearsLicensed` is **computed** from the
  earliest admission year on the profile. They disagree for some attorneys, and both are shipped.

### 🗂 Key output fields

| Field | Type | Notes |
|---|---|---|
| `profileId` / `fullName` | string | The numeric id is the dedupe key and the stable join key |
| `firmName` / `jobTitle` | string | Firm from the profile's structured data |
| `practiceAreas` / `practiceAreaDetail` | array | Flat list, and the area → sub-area tree. Both of the directory blocks are read — the main one *and* "Additional Practice Areas" — so nothing the attorney listed is dropped |
| `phone` / `phones` / `fax` | string / array | Primary number, then every number listed |
| `website` / `websites` | string / array | Firm site and blog |
| `contactFormUrl` | string | On **every** row — the directory's own contact channel for that attorney |
| `street` / `city` / `state` / `zip` / `country` | string | Office address; ALL-CAPS city names are title-cased, two-letter states upper-cased |
| `latitude` / `longitude` | number | 100% fill in both samples |
| `officeCount` / `offices[]` | number / array | Every office when the attorney lists more than one |
| `yearAdmitted` / `yearsLicensed` / `yearsOfExperience` | number | Computed, computed, self-reported |
| `jurisdictions[]` | array | `jurisdiction`, `admittedYear`, `barOrganization` |
| `lawSchool` / `lawSchoolDegree` / `lawSchoolGraduationYear` / `education[]` | string / array | Degree parsed out of "J.D. (2012) | Degree of Law" |
| `badges` / `isClaimed` / `listingTier` | array / bool / string | Paid-placement and claimed-profile signals |
| `rating` / `ratingScale` / `reviewCount` | number | **Only with "Include rating and review count"** (on by default) |
| `languages` / `fees` / `freeConsultation` / `creditCardsAccepted` | array / bool | Self-reported service terms |
| `associations[]` / `awards[]` / `certifications[]` / `experience[]` | array | **Only with "Include credentials"** (on by default) |
| `biography` | string | **Only with "Include the biography text"** (off by default) |
| `socialProfiles` / `otherDirectoryProfiles` | object / array | LinkedIn, Facebook, X, YouTube, Instagram; Avvo / Martindale / Lawyers.com / Super Lawyers |
| `photoUrl` / `profileUrl` / `justiaProfileUrl` / `scrapedAt` | string | Provenance. `scrapedAt` is ISO-8601 UTC |

### 📊 Field fill — measured on 220 metro rows and 140 whole-state rows

**Fill depends heavily on the market you crawl, so both samples are published.** The metro sample
was **203/220 Premium** listings; the whole-state sample **55/140**. Plan against the column that
matches the scope you are going to run.

| Field | Metro sample (220 rows) | Whole-state sample (140 rows) |
|---|---:|---:|
| `fullName`, `profileId`, `badges`, `isClaimed` | **100%** | **100%** |
| `firmName` | **100%** | **100%** |
| `latitude` / `longitude` | **100%** | **100%** |
| **`phone`** | **99.5%** | **100%** |
| `street` / `city` / `state` / `zip` | 99.5% | 99.3% |
| **`website`** | **99.5%** | **67.9%** |
| `lawSchool` | **100%** | 70.0% |
| `associations` | 95.9% | 84.3% |
| `biography` | 96.8% | 64.3% |
| `languages` | 96.4% | 37.1% |
| social profiles (any) | 92.7% | 49.3% |
| `yearsOfExperience` | 91.4% | 75.7% |
| `yearAdmitted` / `yearsLicensed` | 85.9% | 71.4% |
| **`practiceAreas`** | **99–100%** | **100%** |
| `otherDirectoryProfiles` | 74.1% | 35.7% |
| `awards` | 73.2% | 17.1% |
| `freeConsultation` | 66.4% | 33.6% |
| `lawSchoolGraduationYear` | 86.0% | 88.6% |
| `fax` | 29.1% | 23.6% |
| **`rating` / `reviewCount`** | **30.9%** | **4.3%** |
| **email** | **0%** | **0%** |

**The headline that could mislead you: this is a phone-and-firm product, not a ratings product and
not an email product.** Every run prints its own measured fill in the log, on its own rows.

**Re-measured 2026-08-19 (audit).** `practiceAreas` used to read 77.7% / 79.3% because the parser
matched only the plural block heading "Practice Areas", while the directory titles the block
"Practice Area" for an attorney with exactly one — and published a second block, "Additional
Practice Areas", that was never read at all. Both are now read and merged: **314 of 316** rows
across five fresh runs (two Austin runs of 100 and three whole-state runs), and 114 of 114 across
nine statewide listings. The two rows that still have none were pulled and read by hand: the
directory publishes no practice-area block of any kind on them — the strings "practice area" and
"practice areas" do not appear anywhere in the 134 KB page. `lawSchoolGraduationYear` moved the
same way (the year is often published only as the entry's end date, not inside the degree line):
**275 of 316**. The other columns in the table are unchanged.

**The three columns that are genuinely thin are the source's own gaps, verified against the raw
profile HTML on 2026-08-19.** Across the same 316 rows: `rating` / `ratingScale` / `reviewCount`
**123 of 316 (38.9%)**, `certifications` **140 of 316 (44.3%)**, `fax` **96 of 316 (30.4%)**. Five
profiles were pulled for each of the three and read directly: **0 of 5** empty-`rating` profiles
contained an `AggregateRating` or a `Review` block anywhere on the page, **0 of 5** empty-`fax`
profiles carried anything but `"faxNumber": []` in their own structured data, and **0 of 5**
empty-`certifications` profiles had a certifications block at all. Every profile that *does* publish
one was extracted (checked the other way too: all 25 rows in the Austin run that list a fax anywhere
in `offices[]` also carry the top-level `fax`). None of the three is a scrape failure, and none can
be filled from anywhere else on the page. Note that `rating`, `reviewCount`, `fax` and
`certifications` are featured columns in the dataset's table views, so those columns will look
sparse in the preview — that is the directory, not a dropped field.

### ⚙️ Input parameters

| Field | Type | Default | What it does |
|---|---|---|---|
| `states` | multi-select | `["texas"]` | 52 options, the directory's own slugs — 50 states plus District of Columbia and Puerto Rico |
| `cities` | string list | `["austin"]` | `austin`, or **`texas/austin`** to pin a city to its state when you list several. Empty = whole state |
| `practiceAreas` | multi-select | — | The directory's 56 areas, picked by name. Each area is a separate listing |
| `profileUrls` | string list | — | Scrape named attorneys and skip the listing crawl. Either host works |
| `withPhoneOnly` | boolean | `true` | On by default — there is no email here, so a row with no phone is not contactable. Costs ~0–0.5% of rows |
| `withWebsiteOnly` | boolean | `false` | For enrichment workflows. Costs ~32% of a whole-state crawl, ~0.5% of a metro one |
| `minYearsLicensed` | integer | — | Falls back to self-reported `yearsOfExperience` when the profile prints no admission year |
| `includeCredentials` | boolean | `true` | Education, experience, awards, associations, certifications. No extra requests |
| `includeReviewsMeta` | boolean | `true` | Rating (out of 10) and review count. No extra requests |
| `includeBiography` | boolean | `false` | The full self-written bio — long, but it is the copy for personalised outreach |
| `maxItems` | integer | `100` | **Your hard cost ceiling.** 100 = $0.30, 1,000 = $3.00 |
| `maxPagesPerListing` | integer | `20` | ~12–15 **new** attorneys per page after duplicates are removed |
| `maxConcurrency` | integer | `2` | Will not speed the run up — the crawl is paced. 2 is measured-optimal |
| `proxyConfiguration` | proxy | *(none)* | Leave empty. The Actor runs direct and clears the edge challenge itself |

### Pricing

**$0.003 per attorney returned — $3 per 1,000.** Charged on the `attorney-scraped` event. No
monthly platform fee from this Actor.

| Run | Attorneys | Cost |
|---|---:|---:|
| The prefilled demo | 100 | **$0.30** |
| One practice area in one metro | 500 | **$1.50** |
| A statewide practice-area pull | 2,000 | **$6.00** |
| A multi-state campaign list | 10,000 | **$30.00** |

What you are **not** charged for:

- **Duplicates.** Profile ids are deduplicated before anything is fetched, so the dataset — and
  your bill — never holds the same attorney twice.
- **Rows your filters removed.** `withPhoneOnly`, `withWebsiteOnly` and `minYearsLicensed` are
  applied after the profile page loads. A filtered-out attorney costs the Actor a fetch and costs
  you nothing.
- **Retries, challenge-token mints and rate-limit pauses.** Those are the Actor's problem.

Rows are charged as they are pushed (`Actor.pushData(items, 'attorney-scraped')`), so if you hit a
budget cap you get whole rows and stop, never a half-billed dataset. `maxItems` is the cost dial:
`maxItems × $0.003` is the most a run can cost you.

### Honest limits

- **No email addresses. Anywhere.** Justia publishes none, for any attorney — its contact form is
  the only email channel. Measured **0%** on both samples. Anyone selling you "Justia emails"
  generated them somewhere else, usually by guessing patterns against the firm domain. What you get
  instead: `contactFormUrl` on **100%** of rows, `phone` at **99.5–100%**, and `website` at
  **99.5% / 67.9%** to enrich from.
- **This is the claimed-profile subset of the directory, not the whole bar.** It is a
  self-selected population — attorneys who registered and filled their profile in. If you need
  every admitted attorney including the ones who never marketed themselves, that is a bar-registry
  product, not a directory product; see the sibling table below.
- **`rating` is 30.9% / 4.3% filled.** A missing rating means Justia published none, not that the
  scrape failed. Do not build a "top-rated attorney" product on this column.
- **`awards` is 73.2% in a metro and 17.1% statewide**, `languages` 96.4% / 37.1%,
  `otherDirectoryProfiles` 74.1% / 35.7%. The metro numbers are a Premium-advertiser sample. The
  whole-state numbers are what an ordinary claimed profile looks like.
- **Roughly 1 in 4 profiles print no admission year at all** (they print the bar organisation in
  that slot instead). `yearAdmitted` is `null` for those rather than guessed, and
  `minYearsLicensed` falls back to self-reported experience so the filter does not silently discard
  them.
- **It is paced, and you cannot buy your way out of it.** The directory answers a burst with an
  HTTP 429 that carries no `retry-after` header and lasts minutes. Expect roughly **1,000
  attorneys per 20 minutes**. Raising `maxConcurrency` does not help.
- **No email, no login, no CAPTCHA-solving service, and no attempt at `/organization/…` firm
  pages.** A profile URL without the trailing numeric id is rejected with an error naming it,
  rather than silently skipped.
- **US only.** The directory lists US attorneys; there is no international coverage to return.

### ⚙️ How it works (technical)

1. Your states, cities and practice areas are turned into directory listing URLs
   (`/lawyers/{area}/{state}/{city}`), one per combination.
2. Each listing is paged until two consecutive pages add no new attorney, or `maxPagesPerListing`
   is reached — the pager keeps serving results rather than ending, so the streak is the real stop
   condition.
3. Every `/lawyer/{slug}-{id}` link is collected and **deduplicated on the numeric id before any
   profile is fetched**.
4. Each profile page is read twice over: the six `ld+json` blocks (name, firm, full address, every
   phone, lat/lon, socials, aggregate rating) and the HTML body (practice-area tree, jurisdictions,
   education, awards, associations, languages, fees, badges, websites).
5. Filters are applied, rows are pushed and charged in the same call, and the run prints its own
   measured field fill.

#### Where the rows come from

The rows are served from **`lawyers.law.cornell.edu`**, the Justia-operated lawyer directory
published with **Cornell Law School's Legal Information Institute** — the same Justia database and
the same profile pages, with the same numeric profile ids. Every row carries its matching
`lawyers.justia.com` URL in `justiaProfileUrl`, so you can open the Justia page for any lead.

`lawyers.justia.com` itself is behind a **Cloudflare managed Turnstile challenge on every path**,
including `/robots.txt` and `/sitemap.xml`. Measured 2026-08-12: **0/15** fresh Apify RESIDENTIAL
sessions, **0/4** Apify datacenter, **0/1** direct, **0/5** other proxy countries, and **0/1** with
a real headless Chromium. This route is not a shortcut around a rate limit — it is where the data
is actually reachable.

#### The transport ladder, measured

The LII host answers plain HTTP with an **AWS WAF `challenge`** (HTTP 202 + a 2 KB shell) on every
path, including `/` and `/robots.txt`. Measured 2026-08-12, through Apify:

| Attempt | Result |
|---|---|
| got-scraping, no token | **202 challenge on every path** |
| Solving the proof-of-work in pure Node | token **issued but rejected** — the site's own script posts a different payload, so a Node-minted token is treated exactly like no token at all |
| Headless Chromium, default user-agent | **403** (CloudFront rejects the `HeadlessChrome` UA outright, as it does `curl/8.4.0` and the Googlebot UA) |
| **Headless Chromium + a real Chrome UA** | **200**, full page, **~1.1 s to mint** |

So the Actor loads one page in a headless browser, lets the site's own challenge script issue the
`aws-waf-token` cookie, and then crawls on ordinary HTTP requests carrying it. The token is bound
to the exit IP and the user-agent, which is why the user-agent is pinned and why the browser and
the crawl share one proxy session.

- **Sustained load: 18/18 = 100%** over 18 contiguous listing pages on a single token.
- **Full validation run: 235/235 requests = 100%** (14 listing pages + 221 profile pages) in 240 s,
  0 failures, 0 rate-limit pauses, 1 token mint.
- **Token lifetime ~5 minutes** (verified 200 at t+4 min, challenged at t+5 min). The Actor
  re-mints automatically whenever it is challenged, so long runs are unaffected.
- **Pacing:** ~325 requests at concurrency 4 with no pacing earned a 429. The crawl therefore runs
  at about one request per second, a 429 pauses every worker rather than one, and the pace then
  eases permanently.

Because the challenge is cleared in a browser page load, the Actor needs **2 GB of memory**. The
browser is closed immediately afterwards; the crawl itself is plain HTTP, and runs **direct with no
proxy** by default.

#### Duplicates — measured on contiguous page walks

Every city listing reprints its **sponsored block on each page**. Measured over pages 1–18 of
`/lawyers/texas/austin`: **495 listed rows, 261 unique — a 47.3% duplicate rate** at the listing
level (30–39% on shorter walks). A second contiguous walk, on the Apify platform on 2026-08-13,
over 20 pages of the same listing: **554 listed rows, 303 unique — 45.3%**.

None of that reaches the dataset. Profile ids are deduplicated globally **before anything is
fetched**, verified on both validation runs: **220/220 and 140/140 unique profile ids, 0.0%
duplicates**, unique names matching unique ids exactly.

Justia's own address casing is inconsistent, so `city` is title-cased **only when Justia shouts it**
(`SAN MARCOS` → `San Marcos`, measured at 3.5% of rows; `MCALLEN` → `McAllen`, `O'FALLON` →
`O'Fallon`; `Macon` is deliberately left alone), and two-letter `state` codes are upper-cased
(`Mt` → `MT`, measured 132 vs 3 on one state crawl). Anything already mixed case is passed through
exactly as published.

### When a run fails

- **Zero rows throws.** A run that quietly succeeds with no data is a bug, not a result. The error
  names the counts: profiles fetched, network failures, dead profiles, unparseable pages, and how
  many your own filters removed.
- **A markup change throws.** If more than 20% of profile pages return HTTP 200 but no readable
  profile, the run fails rather than shipping a degraded extraction.
- **A listing that finds no attorney links at all throws**, because that means the directory
  changed how it renders result cards — not that your filters were too narrow.
- **A bad city slug warns by name.** The directory renders a page for a city that does not exist
  instead of returning 404, so a typo shows up as an empty first page and the log says which
  listing it was. States and practice areas cannot be misspelt — they are dropdowns.
- **A profile URL with no numeric id stops the run** with the offending URL in the message.
- **Running out of candidates warns** with how many were delivered against how many you asked for,
  and what to widen.

### 🔎 How this differs from our other attorney Actors

We build several attorney scrapers, and they are not substitutes. The ones already live are on
[our Apify Store profile](https://apify.com/scrapersdelight); the rest are rolling out.

| Actor | What it is | Why you would use it instead |
|---|---|---|
| **This one** | Justia directory, all 50 states + DC + PR, claimed profiles | Marketing-ready leads: phone at 99.5–100%, website, practice-area tree, paid-placement flag |
| [`ca-attorney-scraper`](https://apify.com/scrapersdelight/ca-attorney-scraper) | The **California State Bar** registry | Licence status and bar-number verification for California — every admitted attorney, not the self-selected slice |
| `state-bar-attorney-scraper` | The **New York** Office of Court Administration registry — 293,648 registered attorneys plus 49,199 deceased | You need *every* NY attorney, a licence status, or a firm-size cut (`firmAttorneyCount`). It carries no websites and no practice areas |
| `martindale-lawyer-scraper` | Martindale-Hubbell | The AV Preeminent **peer rating** and its sub-scores — a credential no directory listing carries |
| `superlawyers-attorney-scraper` | Super Lawyers | **Selection years** — who is on the list right now, and who just made it |

Justia is where you go for the phone number and the practice areas. A state bar registry is where
you go for completeness and licence status. Martindale is where you go for the peer rating. Running
this Actor and a registry, then joining on name + city, gives you the registry's completeness with
this one's contact depth.

### ❓ FAQ

**Can I get the lawyer's email?** No — and nobody honestly can from this source. Justia's public
directory does not expose attorney email addresses for anyone; its contact form is the only email
channel. Measured **0%** across both samples. What you get instead is `contactFormUrl` on every
row, `phone` at 99.5–100% and the firm `website` at 99.5% / 67.9%, which is the domain most buyers
enrich against. Anyone selling "Justia emails" generated them elsewhere.

**Does it need an account, a login or an API key?** No. It reads public directory and profile pages
only, and it does not log in anywhere.

**Do I need a proxy?** No. The Actor runs direct by default and clears the site's edge challenge
itself — 235/235 requests clean on the last full validation run. Set Apify RESIDENTIAL only if
token minting starts failing on your egress IP.

**How many attorneys can I get from one state?** More than you will want to pay for. The practical
limit is your `maxItems` and your patience: the crawl is paced at about one request per second, so
budget roughly 1,000 attorneys per 20 minutes.

**Will two runs give me duplicates?** Within a run, no — profile ids are deduplicated before
fetching (220/220 and 140/140 unique on the validation runs). Across runs, overlapping scopes will
overlap; dedupe on `profileId`, which is stable.

**Do I get charged for rows my filters removed?** No. Filters are applied after the profile page
loads, so a filtered-out attorney costs the Actor a fetch and costs you nothing. You are billed per
attorney delivered.

**Why does an Austin crawl return Houston attorneys?** Because the directory lists attorneys
*serving* a city, drawn from the surrounding region. Each row's `city`/`state` is the attorney's
real office — filter on that if you need strict geography.

**Why is the rating out of 10?** Because that is the scale Justia publishes. `ratingScale` is on
every row so you never have to infer it, and only 30.9% (metro) / 4.3% (statewide) of profiles have
a rating at all.

**Can I scrape one specific attorney?** Yes — paste their profile URL into **Attorney profile
URLs**. Either `lawyers.justia.com/lawyer/…` or `lawyers.law.cornell.edu/lawyer/…` works; the
trailing numeric id is what identifies them.

**Can I schedule it?** Yes, with an Apify Schedule. Attorney directory data moves slowly — a
monthly or quarterly refresh is usually enough, and `profileUrls` lets you re-check a known list
cheaply.

**Will a run ever succeed with zero rows?** No. Zero rows throws, with the scope and the HTTP
counts in the message. A green-but-empty dataset is treated as a bug.

**Something looks wrong — how do I debug it?** Every run logs its own measured field fill, its
duplicate rate, the number of challenge-token mints and the number of rate-limit pauses. Compare
those to the numbers on this page; if they diverge, open an issue.

### 💬 Support & custom work

Found a missing field, or want a filter that is not here? Open an issue on the **Issues** tab.
Custom fields, extra directories and scheduled feeds into your CRM or warehouse can be built on
request.

### ⚠️ Disclaimer

This Actor is an independent tool and is **not affiliated with, endorsed by, or sponsored by**
Justia Inc., Cornell Law School or the Legal Information Institute. Justia® is a trademark of its
owner. All trademarks are the property of their respective owners.

It reads only publicly available lawyer directory and profile pages — no authenticated endpoints,
no login-only or private data, and no CAPTCHA-solving service. Attorney directory listings are
published as public professional information. Rows describe real people: you are responsible for
complying with Justia's terms, with applicable data-protection law (GDPR, CCPA and equivalents),
with CAN-SPAM and telemarketing rules, and with the attorney advertising and solicitation rules of
any bar you operate under. Honour opt-out and do-not-contact requests.

### SEO Keywords

justia scraper, justia lawyer scraper, justia attorney scraper, justia lawyer directory scraper,
lawyer directory scraper, attorney directory scraper, attorney leads scraper, lawyer leads scraper,
attorney contact data, lawyer phone numbers, law firm leads, legal lead generation, scrape lawyers,
scrape attorneys, attorney data extraction, practice area lawyer scraper, personal injury lawyer
leads, criminal defense attorney list, us attorney dataset, law firm database, attorney phone list,
legal directory data, lawyer prospecting list, attorney marketing data, justia lawyers export

# Actor input Schema

## `states` (type: `array`):

Pick one or more states. Every option here is a slug the directory itself publishes, so a picked state always exists. Each state is crawled as its own listing (×  each city, × each practice area), so three states with two practice areas is six listings.

## `cities` (type: `array`):

City slugs — lower case and hyphenated: austin, houston, new-york, los-angeles, san-francisco. Leave empty to crawl the whole state. Write a city as state/city — texas/austin, new-york/new-york — when you list several states, otherwise every city is tried in every state. A city listing returns attorneys SERVING that city, drawn from the surrounding region, so a texas/austin crawl legitimately returns Houston firms; each row's own city/state fields are the attorney's actual office. The directory renders a page for a city that does not exist instead of a 404, so a typo shows up as an empty first page and the run warns you by name.

## `practiceAreas` (type: `array`):

Pick one or more of the directory's 56 practice areas — these are its own slugs and titles, so nothing here can be misspelt. Leave empty to take every attorney in the state or city. Each area is crawled as a separate listing, so picking five areas in one city means five listings, not one; attorneys average 2.9 practice areas in a metro crawl and 3.7 in a whole-state crawl, so the areas overlap heavily and the deduplicator drops the repeats before you are billed.

## `profileUrls` (type: `array`):

One attorney per URL. Either host works — https://lawyers.justia.com/lawyer/jane-doe-123456 or https://lawyers.law.cornell.edu/lawyer/jane-doe-123456 — because the trailing numeric profile id is what identifies the attorney. A URL without that numeric id stops the run with an error naming the bad URL rather than silently skipping it. Duplicate ids are collapsed before anything is fetched, so pasting the same attorney twice costs you once.

## `withPhoneOnly` (type: `boolean`):

Keep only attorneys who publish a phone number. On by default because a lead with no phone and no email is not contactable — Justia publishes NO email address for any attorney, and its contact form is the only email channel (each row carries that form's URL). This filter costs you almost nothing: phone fill was measured at 99.5% on a 220-row metro sample and 100% on a 140-row whole-state sample.

## `withWebsiteOnly` (type: `boolean`):

Keep only attorneys who link a firm website or blog — the domain you would enrich or run outbound against. Priced the same either way, but it costs coverage: website fill was 99.5% on the metro sample and 67.9% on the whole-state sample, so on a state crawl this discards about a third of the directory and the run fetches roughly three profiles for every two it keeps.

## `minYearsLicensed` (type: `integer`):

Keep only attorneys admitted to a bar at least this many years ago, computed from the earliest year in "Jurisdictions Admitted to Practice". Set 20+ for succession, retirement and M\&A outreach; leave empty for everyone. About 1 in 4 profiles print the bar organisation ("State Bar of Montana") in that slot instead of a year, so this filter falls back to the attorney's self-reported years of experience rather than silently discarding them.

## `includeCredentials` (type: `boolean`):

Include the education, professional experience, awards, bar associations and certifications arrays. Turn off for a lean contact-only export. Measured fill on the metro sample: associations 95.9%, awards 73.2%; on the whole-state sample 84.3% and 17.1%.

## `includeReviewsMeta` (type: `boolean`):

Include the aggregate client rating and its review count. The rating is out of 10, not out of 5 — ratingScale is on every row so you never have to guess. Only a minority of profiles carry one: measured 30.9% on the metro sample and 4.3% on the whole-state sample, so do not plan a run around it.

## `includeBiography` (type: `boolean`):

Include the attorney's full self-written biography — the copy you would personalise outreach from. It is long, so it is off by default. Measured fill: 96.8% on the metro sample, 64.3% on the whole-state sample.

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

Stop the run after this many attorneys have been delivered. 100 = $0.30, 1,000 = $3.00, 10,000 = $30.00. The crawl is paced at about one request per second (the directory rate-limits per IP), so budget roughly 1,000 attorneys per 20 minutes of run time.

## `maxPagesPerListing` (type: `integer`):

How deep to page through each listing before moving to the next. Roughly 12-15 NEW attorneys per page after duplicates are removed — every listing page reprints its sponsored block, measured at a 47.3% duplicate rate over pages 1-18 of texas/austin. Raise it to go deeper in one city; add more cities or practice areas to go broader.

## `maxConcurrency` (type: `integer`):

How many profile fetches may be in flight. Raising it will NOT make the run faster. The directory answers a burst with an HTTP 429 that carries no retry-after header and lasts minutes (measured: ~325 requests at concurrency 4 with no pacing tripped it), and the WAF token is pinned to one exit IP so we cannot rotate away from it — the Actor therefore paces itself at about one request per second whatever you put here. 2 is measured-optimal.

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

Leave empty. This Actor runs direct, with no proxy, and clears the directory's edge challenge itself — measured 235/235 requests clean on a full validation run. Set Apify RESIDENTIAL only if your runs start failing to mint a token, in which case one pinned session is used for both the browser and the crawl, because the challenge token is bound to a single exit IP.

## Actor input object example

```json
{
  "states": [
    "texas"
  ],
  "cities": [
    "austin"
  ],
  "withPhoneOnly": true,
  "withWebsiteOnly": false,
  "includeCredentials": true,
  "includeReviewsMeta": true,
  "includeBiography": false,
  "maxItems": 100,
  "maxPagesPerListing": 20,
  "maxConcurrency": 2
}
```

# Actor output Schema

## `items` (type: `string`):

One row per attorney: name, firm, practice areas, phone, website, office address, year admitted and years licensed, law school, badges, rating and credentials.

# 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 = {
    "states": [
        "texas"
    ],
    "cities": [
        "austin"
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/justia-lawyer-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 = {
    "states": ["texas"],
    "cities": ["austin"],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/justia-lawyer-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 '{
  "states": [
    "texas"
  ],
  "cities": [
    "austin"
  ],
  "maxItems": 100
}' |
apify call scrapersdelight/justia-lawyer-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/justia-lawyer-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/ZVhgRtaVYKS1DPXZz/builds/STW8MpCVonumPQnwU/openapi.json
