GOC Optician & Optometrist Register Scraper
Search the General Optical Council's Specialist Register (str.optical.org) for UK optometrists, dispensing opticians, students and optical businesses - by name, exact GOC number, specialty, company or postcode radius, with practice addresses and FTP decision documents.
Input schema
Input fields, types, required flags, descriptions, defaults, and prefills come from the published definition. Prefill examples are distinct from defaults.
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | required | Choose the task. `registrant` searches individuals by name, register type, specialty, address, postcode, or GOC number. `bodyCorporate` searches registered businesses. `byGocNumber` checks a list of exact GOC numbers and detects registrants vs businesses automatically. `allBusinesses` exports the full registered-business list (~3,000 records). |
surnames | array | optional | One or more surnames to search. Each surname runs its own search and results are merged without duplicates. Leave empty if you search by company, register type, specialty, address, postcode, or GOC number instead. Latin-script values only: the GOC search ignores non-Latin text. |
forename | string | optional | Optional first-name filter applied to every surname search in registrant mode. |
Show all input fields
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | required | Choose the task. `registrant` searches individuals by name, register type, specialty, address, postcode, or GOC number. `bodyCorporate` searches registered businesses. `byGocNumber` checks a list of exact GOC numbers and detects registrants vs businesses automatically. `allBusinesses` exports the full registered-business list (~3,000 records). |
surnames | array | optional | One or more surnames to search. Each surname runs its own search and results are merged without duplicates. Leave empty if you search by company, register type, specialty, address, postcode, or GOC number instead. Latin-script values only: the GOC search ignores non-Latin text. |
forename | string | optional | Optional first-name filter applied to every surname search in registrant mode. |
soundsLikeSurname | boolean | optional | Match surnames phonetically (e.g. 'Smyth' also finds 'Smith') instead of as a literal substring. |
registerType | string | optional | Restrict registrant searches to one registration category. |
speciality | string | optional | Restrict registrant searches to one specialty annotation. |
addresses | array | optional | One or more address, town, or postcode-area text searches for registrant mode. Each value runs its own search and results are merged. Latin-script values only. |
companyNames | array | optional | One or more optical-business names to search in bodyCorporate mode. Each value runs its own search and results are merged. Latin-script values only. |
gocNumbers | array | optional | Exact GOC registration numbers, e.g. 01-12294, D-9396, SO-44231, CO-4310. In byGocNumber mode this is the whole lookup list (registrants and businesses can be mixed). Missing hyphens, lowercase, and a missing leading zero are normalised. In search modes the numbers are combined (AND) with the other filters. |
postcode | string | optional | UK postcode or outward code (SW1A 1AA or EH1) used with radiusMiles. Works for registrant and business searches. Only real postcodes return results. |
radiusMiles | integer | optional | Distance in miles from the postcode. Only used when postcode is set. |
sortBy | string | optional | Sort registrant results by surname. The GOC register ignores this for business searches. |
fetchDetails | boolean | optional | Visit each result's detail page to capture qualifications, town, every practice address, postcodes, and FTP decision documents. Slower and dependent on the GOC's flaky detail pages; disable for a fast list-only run. Ignored in allBusinesses mode, which is always list-only. |
maxDetailedItems | integer | optional | Only used when fetchDetails is on. The first N records are enriched; the rest are emitted with list-level fields. Keep this modest for large runs. Ignored in allBusinesses mode. |
maxItems | integer | optional | Hard cap on records emitted across all searches. The registrant register is about 33,359 rows at 5 per page, so a complete registrant export cannot finish in one default run. The full business register is about 2,973 rows and allBusinesses is list-only. |
Download the published input schema
View complete JSON
{
"description": "Search the General Optical Council's public Specialist Register (str.optical.org) for registered optometrists, dispensing opticians, students, and optical businesses across the UK. Look up exact GOC numbers, search by name, specialty, company, or postcode, or export the full business register.",
"properties": {
"addresses": {
"default": [],
"description": "One or more address, town, or postcode-area text searches for registrant mode. Each value runs its own search and results are merged. Latin-script values only.",
"editor": "stringList",
"prefill": [],
"title": "Address or town contains (one or more)",
"type": "array"
},
"companyNames": {
"default": [],
"description": "One or more optical-business names to search in bodyCorporate mode. Each value runs its own search and results are merged. Latin-script values only.",
"editor": "stringList",
"prefill": [],
"title": "Company names (one or more)",
"type": "array"
},
"fetchDetails": {
"default": true,
"description": "Visit each result's detail page to capture qualifications, town, every practice address, postcodes, and FTP decision documents. Slower and dependent on the GOC's flaky detail pages; disable for a fast list-only run. Ignored in allBusinesses mode, which is always list-only.",
"prefill": false,
"sectionCaption": "Output options",
"sectionDescription": "Control detail enrichment and the size of the run.",
"title": "Fetch full profile details",
"type": "boolean"
},
"forename": {
"default": "",
"description": "Optional first-name filter applied to every surname search in registrant mode.",
"editor": "textfield",
"prefill": "",
"title": "First name contains",
"type": "string"
},
"gocNumbers": {
"default": [],
"description": "Exact GOC registration numbers, e.g. 01-12294, D-9396, SO-44231, CO-4310. In byGocNumber mode this is the whole lookup list (registrants and businesses can be mixed). Missing hyphens, lowercase, and a missing leading zero are normalised. In search modes the numbers are combined (AND) with the other filters.",
"editor": "stringList",
"prefill": [
"01-12294"
],
"title": "GOC number(s)",
"type": "array"
},
"maxDetailedItems": {
"default": 2000,
"description": "Only used when fetchDetails is on. The first N records are enriched; the rest are emitted with list-level fields. Keep this modest for large runs. Ignored in allBusinesses mode.",
"maximum": 3000,
"minimum": 1,
"prefill": 1,
"title": "Maximum records with full details",
"type": "integer"
},
"maxItems": {
"default": 50,
"description": "Hard cap on records emitted across all searches. The registrant register is about 33,359 rows at 5 per page, so a complete registrant export cannot finish in one default run. The full business register is about 2,973 rows and allBusinesses is list-only.",
"maximum": 25000,
"minimum": 1,
"prefill": 1,
"title": "Maximum records",
"type": "integer"
},
"mode": {
"default": "registrant",
"description": "Choose the task. `registrant` searches individuals by name, register type, specialty, address, postcode, or GOC number. `bodyCorporate` searches registered businesses. `byGocNumber` checks a list of exact GOC numbers and detects registrants vs businesses automatically. `allBusinesses` exports the full registered-business list (~3,000 records).",
"editor": "select",
"enum": [
"registrant",
"bodyCorporate",
"byGocNumber",
"allBusinesses"
],
"enumTitles": [
"Search individual registrants (name, register type, specialty, location)",
"Search optical businesses (company name, GOC number, location)",
"Look up exact GOC number(s) — registrants and/or businesses",
"All registered optical businesses (full business register, list-only)"
],
"prefill": "byGocNumber",
"title": "What do you want to collect?",
"type": "string"
},
"postcode": {
"default": "",
"description": "UK postcode or outward code (SW1A 1AA or EH1) used with radiusMiles. Works for registrant and business searches. Only real postcodes return results.",
"editor": "textfield",
"prefill": "",
"title": "Postcode for a distance search",
"type": "string"
},
"radiusMiles": {
"default": 50,
"description": "Distance in miles from the postcode. Only used when postcode is set.",
"maximum": 100,
"minimum": 1,
"prefill": 50,
"title": "Distance radius (miles)",
"type": "integer"
},
"registerType": {
"default": "",
"description": "Restrict registrant searches to one registration category.",
"editor": "select",
"enum": [
"",
"Optometrist",
"Dispensing Optician",
"Student Optometrist",
"Student Dispensing Optician",
"Dispensing Optician, Student Optometrist",
"Student Dispensing Optician, Student Optometrist"
],
"enumTitles": [
"Any register type",
"Optometrist",
"Dispensing Optician",
"Student Optometrist",
"Student Dispensing Optician",
"Dispensing Optician + Student Optometrist",
"Student Dispensing Optician + Student Optometrist"
],
"prefill": "",
"title": "Registered as",
"type": "string"
},
"sortBy": {
"default": "",
"description": "Sort registrant results by surname. The GOC register ignores this for business searches.",
"editor": "select",
"enum": [
"",
"Registrant Surname (a - z)",
"Registrant Surname (z - a)"
],
"enumTitles": [
"Default register order",
"Surname A-Z",
"Surname Z-A"
],
"prefill": "",
"title": "Sort order (registrant searches)",
"type": "string"
},
"soundsLikeSurname": {
"default": false,
"description": "Match surnames phonetically (e.g. 'Smyth' also finds 'Smith') instead of as a literal substring.",
"prefill": false,
"title": "Sounds-like surname matching",
"type": "boolean"
},
"speciality": {
"default": "",
"description": "Restrict registrant searches to one specialty annotation.",
"editor": "select",
"enum": [
"",
"Additional supply",
"Contact lens",
"Independent prescribing",
"Supplementary prescribing"
],
"enumTitles": [
"Any specialty",
"Additional supply",
"Contact lens",
"Independent prescribing",
"Supplementary prescribing"
],
"prefill": "",
"title": "Specialty",
"type": "string"
},
"surnames": {
"default": [],
"description": "One or more surnames to search. Each surname runs its own search and results are merged without duplicates. Leave empty if you search by company, register type, specialty, address, postcode, or GOC number instead. Latin-script values only: the GOC search ignores non-Latin text.",
"editor": "stringList",
"prefill": [],
"sectionCaption": "Search filters",
"sectionDescription": "Provide at least one filter for the search modes. Multiple surnames or addresses run separate searches.",
"title": "Surnames (one or more)",
"type": "array"
}
},
"required": [
"mode"
],
"schemaVersion": 1,
"title": "GOC Optician & Optometrist Register Scraper Input",
"type": "object"
}
Output schema
Published result links and storage locations. This schema is not an exhaustive contract for every field in a dataset record.
| Field | Type | Required | Description |
|---|---|---|---|
registrants | string | optional | Dataset containing all scraped GOC register records. |
Download the published output schema
View complete JSON
{
"actorOutputSchemaVersion": 1,
"description": "Registrants and optical businesses from the General Optical Council's public Specialist Register — GOC number, name or company, register type, status, registration date, specialties, qualifications, town, every practice address and postcode, FTP decision documents, and source URL.",
"properties": {
"registrants": {
"description": "Dataset containing all scraped GOC register records.",
"template": "{{links.apiDefaultDatasetUrl}}/items",
"title": "GOC registrants / businesses",
"type": "string"
}
},
"title": "GOC Optician & Optometrist Register Scraper Output"
}
Dataset schema
Published dataset views and any declared field definitions. A view can select only a subset of the returned record fields.
Download the published dataset schema
View complete JSON
{
"actorSpecification": 1,
"views": {
"overview": {
"display": {
"component": "table",
"properties": {}
},
"title": "Optical registrants / businesses",
"transformation": {
"fields": [
"name",
"companyName",
"gocNumber",
"registerType",
"registrationStatus",
"registrationDate",
"specialities",
"qualifications",
"town",
"practiceAddresses",
"ftpDecisions",
"hasConditions",
"isSuspended",
"searchTerm",
"sourceUrl"
]
}
}
}
}
Key-value schema
Published key-value store definitions, when provided by this build.
This definition is not present in the default published build. See the published README for available documentation.
Published README
Read the full published documentation
GOC Optician & Optometrist Register Scraper
Search and export the General Optical Council's (GOC) public Specialist Register at str.optical.org — the statutory register of every optometrist, dispensing optician, student optometrist/student dispensing optician, and registered optical business permitted to practise in the UK. No login, API key, cookie, or proxy is required. The actor selects its own network route automatically; you do not configure a proxy.
What this actor does
- Search individual registrants by surname, first name, register type, specialty, address/town, postcode radius, or exact GOC number.
- Search optical businesses by company name, GOC number, or postcode radius.
- Check a list of exact GOC numbers — registrants and businesses can be mixed in one list; the actor detects the prefix, batches the lookups, and reports numbers that were not found.
- Export the full registered-business list (~3,000 businesses) in one run.
- Enrich with profile data — qualifications, town, every published practice address, practice postcodes, and FTP decision PDF links.
- Use multiple surnames, addresses, or company names in one run; each value is searched separately and results are de-duplicated by GOC number.
- Omit empty fields — every emitted field carries real, non-null data.
Modes
| Mode | Use it for |
|---|---|
registrant |
Individuals: name, register type, specialty, address, postcode radius, GOC number |
bodyCorporate |
Registered optical businesses: company name, GOC number, postcode radius |
byGocNumber |
Bulk verification of exact GOC numbers; registrants and businesses mixed |
allBusinesses |
Full registered-business export (~3,000 rows) |
registrant and bodyCorporate need at least one search filter. byGocNumber needs gocNumbers. allBusinesses needs no filter.
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 |
Registered, Registered with Conditions, Suspended |
registrationDate |
Date of most recent registration, ISO yyyy-mm-dd |
specialities |
Raw specialty text, e.g. Contact Lens Specialty |
specialityList |
Normalised specialty values, e.g. ["Contact lens"] |
specialityCount |
Number of specialty annotations |
qualifications |
Professional qualifications, when published (detail page) |
town |
Registered town (detail page) |
practiceAddresses |
First published practice address (detail page) |
otherPracticeAddresses |
Additional practice addresses (detail page) |
practiceAddressCount |
Total published practice addresses |
practicePostcodes |
UK postcodes extracted from all practice addresses |
primaryPracticePostcode |
First practice postcode |
ftpDecisions |
FTP decision title(s), separated by ; |
ftpDecisionDocuments |
{title, url} for each published FTP decision PDF |
ftpDecisionCount |
Number of published FTP decisions |
hasFtpDecision |
true when an FTP decision is published |
hasConditions |
true when the status contains Conditions |
isSuspended |
true when the status contains Suspended |
isStudent |
true for student register types |
searchTerm |
The surname/address that found the record, when the run used more than one search term |
sourceUrl |
Canonical GOC register profile URL |
recordType |
"registrant" |
scrapedAt |
UTC ISO timestamp |
Body corporate records (recordType: "bodyCorporate")
The same common fields where applicable, with companyName instead of name. registerType, specialities, and qualifications are omitted because the GOC does not publish them for businesses.
Input
| Field | Type | Default | Description |
|---|---|---|---|
mode |
select | registrant |
registrant, bodyCorporate, byGocNumber, or allBusinesses |
surnames |
array | [] |
One or more surnames (registrant mode) |
forename |
string | – | First-name contains filter (registrant mode) |
soundsLikeSurname |
bool | false |
Phonetic surname matching |
registerType |
select | – | Registration category |
speciality |
select | – | Specialty annotation |
addresses |
array | [] |
Address/town text searches (registrant mode) |
companyNames |
array | [] |
Company-name searches (business mode) |
gocNumbers |
array | [] |
Exact GOC numbers; mixed registrant/business in byGocNumber |
postcode |
string | – | UK postcode or outward code for a distance search |
radiusMiles |
int | 50 |
Radius in miles (1–100) |
sortBy |
select | – | Surname A-Z or Z-A (registrant searches) |
fetchDetails |
bool | true |
Fetch profile details; off is much faster. Ignored in allBusinesses, which is always list-only |
maxDetailedItems |
int | 2000 |
Cap on records with detail enrichment |
maxItems |
int | 50 |
Hard cap on emitted records (1–25,000) |
Legacy tasks that send surname, companyName, or address still work: those keys are mapped in code to the new arrays.
Examples
Find optometrists named Smith or Jones, with full details:
{
"mode": "registrant",
"surnames": ["Smith", "Jones"],
"registerType": "Optometrist",
"fetchDetails": true,
"maxDetailedItems": 100
}
Check a mixed list of GOC numbers:
{
"mode": "byGocNumber",
"gocNumbers": ["01-12294", "D-9396", "CO-4310"],
"fetchDetails": false,
"maxItems": 10
}
Find contact-lens dispensing opticians near Edinburgh:
{
"mode": "registrant",
"speciality": "Contact lens",
"postcode": "EH1",
"radiusMiles": 25,
"fetchDetails": true
}
Export every registered optical business:
{
"mode": "allBusinesses",
"fetchDetails": false,
"maxItems": 3000
}
How it works
The actor sends the same JSON search request as the public register (POST /registrant/list) and parses the returned HTML fragment. Detail enrichment fetches GET /Registrant/{guid}. FTP decision links point at public PDFs on mygoc.azurewebsites.net.
The register returns 5 results per page. Large runs are bounded by maxItems and the platform timeout. The actor streams records, retries transient list errors, caps detail fetches, and falls back to list-only records when the detail pages keep failing.
Limits and accuracy
- The GOC register is live. Counts and records change between runs.
- Detail pages are flaky. The GOC's own detail pages intermittently return HTTP 500 or take tens of seconds. The actor uses short timeouts, a circuit breaker, and list-only fallback, so a run can still complete with list-level fields for the records whose detail page failed.
- A full registrant export does not fit one default run. The register returned 33,359 registrants on 2026-11-05 at a fixed 5 records per page: 6,672 page requests. At the measured cloud rate of about 1.36 s/page that is roughly 151 minutes, far above the default 3,600 s timeout. Even one register type (
Optometrist, 19,501) is 3,901 pages, about 88 minutes. RaisingmaxItemsdoes not remove the page ceiling, and the actor does not ship a chunk/resume cursor; use narrow filters or accept a partial export. The full business register is about 2,973 rows (595 pages, roughly 13.5 minutes) andallBusinessesis deliberately list-only. - No contact details are published. The GOC register does not expose email, phone, website, gender, languages, home visits, registration expiry, first-registration date, or conditions text. Those fields are not emitted.
- No status filter exists upstream. The actor returns the raw
registrationStatusand the derivedhasConditions/isSuspendedflags. - Non-Latin search text is rejected. The GOC search silently ignores CJK/emoji/non-Latin text and would return the whole register; the actor refuses those values before sending a request.
- Body corporate sorting is ignored upstream.
sortByaffects registrant searches only. - Historical/erased registrants are not in the public search. They cannot be retrieved from this source.
FAQ
Do I need cookies or an API key? No. The register is public and the actor uses the same public endpoint as the website.
Why are some fields missing? Qualifications, town, practice addresses, and FTP decisions are published conditionally. fetchDetails must be on for those fields, except in allBusinesses, which is list-only. Detail pages can also fail; the actor then emits list-level fields for the affected records. Empty values are omitted, never emitted as null or "None".
What does ftpDecisions mean? Fitness to Practise — a published regulatory decision against the registrant or business. Most records have none.
Can I combine filters? Yes. Surnames, address text, register type, specialty, and postcode are combined with AND on the register. Multiple surnames or addresses run separate searches and are merged.
Why did my GOC number not appear? byGocNumber reports missing numbers in the run status. A number that is not on the current register, or is formatted in a way the actor cannot normalise, will not return a record.
Why does a sourceUrl sometimes show a server error? It points at the live GOC page, which is intermittently unavailable. Reload it; the scraped data is unaffected.
Data source
General Optical Council public Specialist Register: https://str.optical.org. This actor is independent and not affiliated with the GOC.
Python quickstart
Use a schema-derived input and the Python standard library to start a run. An Apify account and token are required for execution.
Source build UvNLj8gOebSn7KfpY. Documentation is regenerated from the default published build; private drafts are not included.