# UK Optician Register Scraper (`crawlerbros/uk-optician-register-scraper`) Actor

Search the General Optical Council's public Specialist Register (str.optical.org) for registered UK optometrists, dispensing opticians, students, and optical businesses - by name, GOC number, specialty, or postcode radius.

- **URL**: https://apify.com/crawlerbros/uk-optician-register-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Lead generation, Automation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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

## UK Optician Register Scraper

Search the General Optical Council's (GOC) public **Specialist Register** at [str.optical.org](https://str.optical.org) — the official, legally-mandated register of every optometrist, dispensing optician, optometry/dispensing student, and registered optical business ("body corporate") permitted to practise in the UK. No login, no API key, no cookies required — the register is public.

### What this actor does

- **Two search modes:** individual registrants (`registrant`) or optical businesses (`bodyCorporate`)
- **Search by name** — surname (with optional phonetic "sounds-like" matching) and first name
- **Search by exact GOC number(s)** — one or many, comma-separated
- **Filter by registration category** — Optometrist, Dispensing Optician, Student (+ dual categories)
- **Filter by specialty** — Contact lens, Independent prescribing, Supplementary prescribing, Additional supply
- **Postcode radius search** — find every registrant/business within N miles of a UK postcode
- **Full-profile enrichment** — optionally visits each result's detail page to also capture qualifications, town, practice address, and fitness-to-practise decisions
- **Empty fields are always omitted** — every emitted field is real, non-null data

### Output fields

#### Registrant records (`recordType: "registrant"`)

| Field | Description |
|---|---|
| `gocNumber` | Official GOC registration number, e.g. `01-12294`, `D-9396` |
| `name` | Full registered name |
| `registerType` | `Optometrist`, `Dispensing Optician`, `Student Optometrist`, etc. |
| `registrationStatus` | e.g. `Registered` |
| `registrationDate` | Date of most recent registration, ISO `yyyy-mm-dd` |
| `specialities` | Specialty annotation, when held (e.g. `Contact Lens Specialty`) |
| `qualifications` | Professional qualifications abbreviation (detail page only) |
| `town` | Registered town (detail page only) |
| `practiceAddresses` | Practice address, when published (detail page only) |
| `ftpDecisions` | Fitness-to-practise decision text, when one exists (detail page only) |
| `sourceUrl` | Canonical GOC register profile URL |
| `recordType` | `"registrant"` |
| `scrapedAt` | UTC ISO timestamp |

#### Body corporate records (`recordType: "bodyCorporate"`)

| Field | Description |
|---|---|
| `gocNumber` | Official GOC business registration number, e.g. `CO-4310` |
| `companyName` | Registered business name |
| `registrationStatus` | e.g. `Registered` |
| `registrationDate` | ISO `yyyy-mm-dd` |
| `town` | Registered town (detail page only) |
| `practiceAddresses` | Practice address (detail page only) |
| `ftpDecisions` | Fitness-to-practise decision text, when one exists (detail page only) |
| `sourceUrl` | Canonical GOC register profile URL |
| `recordType` | `"bodyCorporate"` |
| `scrapedAt` | UTC ISO timestamp |

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | select | `registrant` | `registrant` or `bodyCorporate` |
| `surname` | string | – | (mode=registrant) Surname contains |
| `soundsLikeSurname` | bool | `false` | (mode=registrant) Match `surname` phonetically |
| `forename` | string | – | (mode=registrant) First name contains |
| `registerType` | select | – | (mode=registrant) Registration category |
| `speciality` | select | – | (mode=registrant) Specialty annotation |
| `address` | string | – | (mode=registrant) Address contains |
| `sortBy` | select | – | (mode=registrant) Sort by surname A-Z / Z-A |
| `companyName` | string | – | (mode=bodyCorporate) Company name contains |
| `gocNumbers` | array | – | Exact GOC number(s); overrides name search |
| `postcode` | string | – | UK postcode for a radius search (both modes) |
| `radiusMiles` | int | `50` | Radius in miles from `postcode` (1–100) |
| `fetchDetails` | bool | `true` | Fetch each profile's detail page for extra fields |
| `maxItems` | int | `50` | Hard cap on returned records (1–2000) |

At least one search criterion (`surname`, `forename`, `registerType`, `speciality`, `address`, `companyName`, `gocNumbers`, or `postcode`) is required per mode — the register does not support an unfiltered "list everyone" query.

#### Example: find all Smiths on the register

```json
{
  "mode": "registrant",
  "surname": "Smith",
  "maxItems": 50
}
```

#### Example: registered contact lens specialists

```json
{
  "mode": "registrant",
  "speciality": "Contact lens",
  "maxItems": 100
}
```

#### Example: optical businesses near a postcode

```json
{
  "mode": "bodyCorporate",
  "postcode": "SW1A 1AA",
  "radiusMiles": 10,
  "maxItems": 50
}
```

#### Example: exact GOC number lookup

```json
{
  "mode": "registrant",
  "gocNumbers": ["01-12294", "D-9396"]
}
```

### Use cases

- **Employer / recruiter verification** — confirm an optometrist or dispensing optician holds a current, valid GOC registration before hiring
- **Compliance & due diligence** — check an optical business's registration status before a partnership or supply agreement
- **Market research** — map optical practices and specialists by postcode/region
- **Patient safety** — verify a practitioner's registered specialty (e.g. contact lens fitting, independent prescribing)
- **Directory building** — populate a "find an optician near you" tool with verified GOC data

### FAQ

**What is the GOC Specialist Register?** The UK General Optical Council is the statutory regulator for optometry and dispensing opticians. Its public "Specialist Register" (branded `str.optical.org`) lets anyone verify whether an individual or business is currently permitted to practise/trade.

**Do I need cookies, a login, or an API key?** No. The register is a public search tool; this actor talks to the same public search endpoint the website itself uses.

**Why do some records have fewer fields than others?** `qualifications`, `town`, `practiceAddresses`, and `ftpDecisions` are only published for some registrants/businesses, and only appear when `fetchDetails` is enabled (visits the individual profile page). Fields the register doesn't publish for a given record are omitted rather than emitted as blank/null.

**What does `ftpDecisions` mean?** "Fitness to Practise" — any published regulatory decision against that registrant/business. Most registrants show no decision, so the field is simply absent.

**Can I search by both name and postcode at once?** Yes — all provided filters are combined (AND logic) by the register's own search, e.g. `surname=Smith` + `postcode=SW1A 1AA` finds only Smiths near that postcode.

**What's the difference between `registrant` and `bodyCorporate` mode?** `registrant` searches individual practitioners (optometrists, dispensing opticians, students). `bodyCorporate` searches registered optical businesses/companies — a separate GOC register requirement for any entity providing optical services.

**How current is the data?** The GOC register is the live, authoritative source — the same one used for official verification — so results reflect the register's current state at the time of the run.

**Does it support non-Latin characters in name searches?** UK optician names are virtually always Latin-script, so this is rarely a practical limitation. Note that the GOC's own search backend silently ignores non-Latin/CJK text in `surname`/`forename`/`address`/`companyName` (it returns its default unfiltered result set rather than zero matches) — this is upstream behavior on the register itself, not something this actor can change. Stick to Latin-script queries for predictable results.

**Why does a record's `sourceUrl` occasionally return a server error when I click it?** The GOC's register (str.optical.org) is a known-flaky public government site — its own detail pages intermittently return HTTP 500 (roughly 1 in 5 requests, independent of query), even for a plain browser visit. The actor retries its own detail-page fetches automatically (3 attempts with backoff) so this rarely affects scraped output, but the emitted `sourceUrl` itself points at the live GOC page and inherits that same intermittent flakiness — simply reload the link if it happens.

# Actor input Schema

## `mode` (type: `string`):

Search individual registrants (optometrists/dispensing opticians) or registered optical businesses (body corporates).

## `surname` (type: `string`):

Filter registrants whose surname contains this text. Case-insensitive, matches anywhere in the surname.

## `soundsLikeSurname` (type: `boolean`):

When enabled, `surname` is matched phonetically (e.g. 'Smyth' also matches 'Smith') instead of as a literal substring.

## `forename` (type: `string`):

Filter registrants by first name (contains match).

## `registerType` (type: `string`):

Restrict to a specific registration category.

## `speciality` (type: `string`):

Restrict to registrants holding a specific specialty annotation.

## `address` (type: `string`):

Filter registrants by address text (contains match).

## `sortBy` (type: `string`):

Sort registrant results by surname.

## `companyName` (type: `string`):

Filter optical businesses whose registered name contains this text.

## `gocNumbers` (type: `array`):

Exact-match one or more GOC registration numbers (e.g. `01-12294` for a registrant or `CO-4310` for a business). Overrides name-based search when provided.

## `postcode` (type: `string`):

UK postcode to search around. Combined with `radiusMiles`. Works for both registrants and businesses.

## `radiusMiles` (type: `integer`):

Distance in miles from `postcode` to search within. Only used when `postcode` is set.

## `fetchDetails` (type: `boolean`):

Visit each registrant/business detail page to also capture qualifications, town, practice address, and fitness-to-practise decisions. Slower but much richer records. Disable for a faster, list-only run.

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

Hard cap on the number of records returned.

## Actor input object example

```json
{
  "mode": "registrant",
  "surname": "Smith",
  "soundsLikeSurname": false,
  "forename": "",
  "registerType": "",
  "speciality": "",
  "address": "",
  "sortBy": "",
  "companyName": "",
  "gocNumbers": [],
  "postcode": "",
  "radiusMiles": 50,
  "fetchDetails": true,
  "maxItems": 50
}
```

# Actor output Schema

## `registrants` (type: `string`):

Dataset containing all scraped GOC register records.

# 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 = {
    "mode": "registrant",
    "surname": "Smith",
    "soundsLikeSurname": false,
    "forename": "",
    "registerType": "",
    "speciality": "",
    "address": "",
    "sortBy": "",
    "companyName": "",
    "gocNumbers": [],
    "postcode": "",
    "radiusMiles": 50,
    "fetchDetails": true,
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/uk-optician-register-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 = {
    "mode": "registrant",
    "surname": "Smith",
    "soundsLikeSurname": False,
    "forename": "",
    "registerType": "",
    "speciality": "",
    "address": "",
    "sortBy": "",
    "companyName": "",
    "gocNumbers": [],
    "postcode": "",
    "radiusMiles": 50,
    "fetchDetails": True,
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/uk-optician-register-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 '{
  "mode": "registrant",
  "surname": "Smith",
  "soundsLikeSurname": false,
  "forename": "",
  "registerType": "",
  "speciality": "",
  "address": "",
  "sortBy": "",
  "companyName": "",
  "gocNumbers": [],
  "postcode": "",
  "radiusMiles": 50,
  "fetchDetails": true,
  "maxItems": 50
}' |
apify call crawlerbros/uk-optician-register-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/uk-optician-register-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/JoARzqSn8pMzeHsDN/builds/SNQEk09lWqIvZ6I2o/openapi.json
