# FINRA BrokerCheck Scraper (`devilscrapes/finra-brokercheck-scraper`) Actor

Turn FINRA BrokerCheck name and CRD lookups into a bulk, disclosure-merged dataset — registration status, employment history, and disciplinary records for brokers and firms, straight from FINRA's public API.

- **URL**: https://apify.com/devilscrapes/finra-brokercheck-scraper.md
- **Developed by:** [DevilScrapes](https://apify.com/devilscrapes) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.80 / 1,000 result row emitteds

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

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

<img src=".actor/icon.svg" width="160" alt="FINRA BrokerCheck Scraper" />

## FINRA BrokerCheck Scraper — Bulk Broker & Firm Lookups

*We do the dirty work so your dataset stays clean.* 😈

**$3.00 / 1,000 results — pay only for results, no credit card to try.** Look up FINRA-registered brokers and brokerage firms by name or CRD number, in bulk, and get back one merged row per hit — registration status, employment history, and disciplinary disclosures included.

FINRA's public BrokerCheck site is built for one lookup at a time. This Actor takes a list of names or CRD numbers, resolves each search hit against the CRD detail/report endpoint, and merges the disclosure, employment, and registration-history data into a single structured row — turning a manual per-broker chore into a bulk dataset your compliance or due-diligence pipeline can consume directly.

### 🎯 What this scrapes

FINRA BrokerCheck's public, keyless JSON API (`api.brokercheck.finra.org`) — the same data that backs [brokercheck.finra.org](https://brokercheck.finra.org), structured for bulk use:

- **Individual brokers** — name search or direct CRD lookup, with registration scope, disclosure flag, employment history, registered states, and registered SROs.
- **Brokerage firms** — name search or direct CRD lookup, with disclosure flag and firm-level detail.
- **Disclosure merge (the differentiator)** — every hit's disciplinary disclosures, prior/current employments, and registration counts are pulled from the CRD detail endpoint and merged into the same row as the search hit, instead of leaving you to chase a second lookup per name.

| Field | Type | Description |
|---|---|---|
| `sourceType` | string | `individual` or `firm` |
| `crdNumber` | string | FINRA's stable CRD identifier |
| `queryMatched` | string | The input query that produced this row |
| `name` | string | Broker or firm name |
| `hasDisclosure` | boolean | True when a disciplinary disclosure is on file |
| `disclosures` | array | null | Disciplinary disclosure records |
| `currentEmployments` / `previousEmployments` | array | null | Employer history |
| `registeredStates` / `registeredSROs` | array | null | Registration records |
| `fetchedAt` | string | ISO 8601 UTC timestamp |

### 🔥 Features

- **Bulk name-or-CRD input** — mix plain-text names and raw CRD numbers in the same `queries` list; a purely numeric entry is fetched directly, everything else is searched.
- **Disclosure merge, not just a search hit** — every row can carry the full disciplinary, employment, and registration picture from the CRD detail endpoint, not just the thin search-result summary most lookups stop at.
- **Pydantic v2 validation** — input and output are model-validated; a malformed input fails fast before any request goes out.
- **We rotate browser fingerprints** — `curl-cffi` impersonation rotates across Chrome, Firefox, and Safari profiles per request.
- **We retry with exponential backoff** on `408 / 429 / 503` and honour `Retry-After`, up to 5 attempts per request.
- **A search that finds nothing still succeeds** — an unmatched name or invalid CRD finishes the run cleanly with zero rows for that query and a status message explaining what was searched, never a false failure.
- **You pay only for results that land** — no data, no charge beyond the flat per-run start fee.

### 💡 Use cases

- **Registered-rep due diligence** — before onboarding a broker or referral partner, pull their full registration and disclosure history in one call instead of a manual BrokerCheck lookup.
- **Compliance backfill** — bulk-check a roster of reps or a list of firm CRDs against current registration status and disclosure flags.
- **Background-check pipelines** — feed `hasDisclosure` and `disclosures` straight into an existing KYC or vendor-vetting workflow.
- **Firm monitoring** — track a watchlist of brokerage firm CRDs for new disclosures between scheduled runs.
- **Recruiting and M\&A screening** — check a candidate roster or an acquisition target's registered reps before signing.

### ⚙️ How to use it

1. Open the Actor input form.
2. Pick **Search type** — `individual` or `firm`. It applies to every entry in your query list.
3. Fill **Queries** with names to search and/or raw CRD numbers to fetch directly — mix both in the same list.
4. (Optional) Set **Max results per query** — how many search hits to keep per name (CRD lookups always return at most one row).
5. Leave **Fetch full detail** on to merge disclosures, employment, and registration history into every row.
6. Click **Start**. Results stream into the default dataset.

#### Quick examples

**Mixed CRD + name lookup (the QA fixture):**

```json
{
  "searchType": "individual",
  "queries": ["5998211", "john smith"],
  "maxResultsPerQuery": 5,
  "fetchFullDetail": true
}
```

**Firm search only, search-hit summary (no detail merge):**

```json
{
  "searchType": "firm",
  "queries": ["Vanguard", "Fidelity"],
  "maxResultsPerQuery": 10,
  "fetchFullDetail": false
}
```

### 📥 Input

| Field | Type | Required | Default | Description |
|---|---|:--:|---|---|
| `searchType` | string (`individual` | `firm`) | **yes** | — | Index to search for non-numeric queries |
| `queries` | array of string | **yes** | — | Names to search or raw CRD numbers to fetch directly |
| `maxResultsPerQuery` | integer | no | `20` | Cap on search hits fetched per name query (1-100) |
| `fetchFullDetail` | boolean | no | `true` | Merge CRD detail — disclosures, employment, registration history |
| `proxyConfiguration` | object | no | `{"useApifyProxy": true}` | Apify Proxy configuration |

### 📤 Output

One dataset row per hit. Real record from a live run — CRD 5998211, direct CRD lookup with `fetchFullDetail: true` (disclosure and employment arrays trimmed here for length; the live dataset row carries all of them):

```json
{
  "sourceType": "individual",
  "crdNumber": "5998211",
  "queryMatched": "5998211",
  "name": "John Smith",
  "otherNames": ["JAKE  SMITH", "JOHN  SMITH", "John J Smith", "John Jacob Lakin Smith"],
  "scope": "InActive",
  "iaScope": "InActive",
  "hasDisclosure": true,
  "approvedRegistrationCount": null,
  "employmentsCount": null,
  "industryEntryDate": null,
  "currentEmployments": [],
  "previousEmployments": [
    { "firmId": 628, "firmName": "FIFTH THIRD SECURITIES, INC.", "city": "EVERGREEN PARK", "state": "IL" }
  ],
  "registeredStates": [],
  "registeredSROs": [],
  "examsCount": { "stateExamCount": 3, "principalExamCount": 0, "productExamCount": 4 },
  "registrationCount": { "approvedSRORegistrationCount": 0, "approvedFinraRegistrationCount": 0, "approvedStateRegistrationCount": 0 },
  "disclosures": [
    {
      "eventDate": "2/15/2023",
      "disclosureType": "Financial",
      "disclosureResolution": "Final",
      "disclosureDetail": { "Type": "Compromise", "Disposition": "Satisfied/Released" }
    }
  ],
  "fetchedAt": "2026-09-19T08:57:30.725036+00:00"
}
```

A name-search row (e.g. `queries: ["john smith"]`) additionally populates `approvedRegistrationCount`, `employmentsCount`, and `industryEntryDate` from the search hit itself — those three fields only come from the search index, not the CRD-direct detail path shown above.

Download the dataset as JSON, CSV, Excel, or XML from the **Export** button on the run page.

### 💰 Pricing

Pay-Per-Event (PPE) — you pay only for results that land:

| Event | Rate (USD) | Trigger |
|---|---|---|
| `actor-start` | $0.20 | Once per Actor run at boot |
| `result-emitted` | $0.0028 | Per validated broker/firm row pushed |

Per-1,000-row extrapolation: **~$3.00** — bulk name/CRD lookups merged with disclosures, employment, and registration history in one call, on pay-per-result pricing.

No credit card required to try — Apify gives new accounts free test credits.

### 🚧 Limitations

- **Public BrokerCheck data only** — no SEC IAPD (Investment Adviser Public Disclosure) merge in this version; that is a distinct dataset and detail shape.
- **No PDF disclosure reports** — structured JSON fields only, not FINRA's rendered PDF report.
- **Disclosure detail varies by type** — the `disclosureDetail` object is passed through as-is since its shape differs across disclosure types (financial, regulatory, customer dispute, criminal); we do not force it into a fixed schema.
- **Branch offices stay nested** — `branchOfficeLocations` ships inside `currentEmployments`, not as separate rows.
- **No historical tracking** — every run is a fresh lookup. Schedule runs and diff your own exports to track changes over time.

### ❓ FAQ

**Is this legal?**
Yes — this Actor only calls FINRA BrokerCheck's own public, keyless API, the same one that backs the public BrokerCheck website. No login, no scraping of gated content.

**What's the difference between this and a plain BrokerCheck search?**
A plain search gives you a thin hit — name, CRD, and a disclosure flag. This Actor resolves that hit against the CRD detail endpoint and merges the full disclosure, employment, and registration-history picture into the same row, so you get one bulk, analysis-ready dataset instead of a manual follow-up lookup per name.

**What happens if a name doesn't match anything, or a CRD is invalid?**
The run finishes successfully with zero rows for that query and a status message describing what was searched — an unmatched search is not a failure.

**Can I look up a firm and its individual reps in the same run?**
Not in one call — `searchType` applies to the whole `queries` list. Run the Actor twice (once per type) if you need both.

**Does turning off "Fetch full detail" save money?**
Yes — with it off, each row carries only the search-hit summary (no employment/registration/disclosure merge), and no detail-endpoint call is made per hit.

### 💬 Your feedback

Found a field FINRA changed, or need SEC IAPD data merged in too? Open an issue on the Actor's Apify Store page or contact us at [apify.com/DevilScrapes](https://apify.com/DevilScrapes).

# Changelog

This Actor's version history is a separate document: https://apify.com/devilscrapes/finra-brokercheck-scraper/changelog.md

# Actor input Schema

## `searchType` (type: `string`):

Index to search for non-numeric queries. Applies to every entry in <code>queries</code>.

## `queries` (type: `array`):

Names to search, or raw CRD numbers to fetch directly. A trimmed, purely-numeric entry is treated as a CRD number.

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

Cap on search hits fetched per name query (1-100). Ignored for direct CRD lookups, which always return at most one row.

## `fetchFullDetail` (type: `boolean`):

Fetch the CRD detail/report per hit and merge disclosure, employment, and registration-history fields into the row. This is what sets this Actor apart from search-only lookups.

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

Apify Proxy configuration. Mandatory per house policy even though FINRA's public API answered a direct, no-proxy probe with HTTP 200. Pinned to the BUYPROXIES94952 group and US exits — a bare useApifyProxy silently resolves to datacenter with no country guarantee.

## Actor input object example

```json
{
  "searchType": "individual",
  "queries": [
    "5998211",
    "john smith"
  ],
  "maxResultsPerQuery": 5,
  "fetchFullDetail": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `datasetItems` (type: `string`):

All dataset items as JSON.

## `datasetItemsCsv` (type: `string`):

Same data exported to CSV.

## `datasetView` (type: `string`):

Open the run dataset in the Console.

# 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 = {
    "searchType": "individual",
    "queries": [
        "5998211",
        "john smith"
    ],
    "maxResultsPerQuery": 5,
    "fetchFullDetail": true,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "BUYPROXIES94952"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/finra-brokercheck-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 = {
    "searchType": "individual",
    "queries": [
        "5998211",
        "john smith",
    ],
    "maxResultsPerQuery": 5,
    "fetchFullDetail": True,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["BUYPROXIES94952"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/finra-brokercheck-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 '{
  "searchType": "individual",
  "queries": [
    "5998211",
    "john smith"
  ],
  "maxResultsPerQuery": 5,
  "fetchFullDetail": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "BUYPROXIES94952"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call devilscrapes/finra-brokercheck-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,devilscrapes/finra-brokercheck-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/y8SpqeJS0dQmMZsNb/builds/Kzgb6Nw28tPXGsXPV/openapi.json
