--- name: orcid-researcher-scraper description: Read public ORCID researcher records as structured data via the Apify Actor arman-bd/orcid-researcher-scraper. Returns names and aliases, biography, self-declared keywords, countries, employment and education history with organisation identifiers, funding awards with grant numbers, the claimed works list with DOIs, plus works, funding and peer-review counts. Takes ORCID iDs directly or discovers them with a fielded search on name, institution, keyword or DOI. Use when a task needs credential and affiliation verification, expert finding, institution mapping or a DOI-level bridge into bibliometrics. Not for citation counts, full text, or anything a researcher has kept private. --- # ORCID Scraper: Researcher Profiles & Affiliations Apify Actor `arman-bd/orcid-researcher-scraper`. Give it ORCID iDs, a search query, or both, and it returns one dataset record per researcher, unwrapped from ORCID's nested JSON into flat fields and clean arrays. You supply no credentials. ## When to use it - Verifying a claimed affiliation or degree against the researcher's own registry entry. - Expert finding: search an institution plus a keyword, then rank the hits by output. - Institution mapping using organisation identifiers rather than fuzzy name strings. - Building a scholarly identity base with DOI-level joins into citation sources. - Turning a DOI into the ORCID iDs of the people who claimed the paper. ## When not to use it - Citation counts, h-index, altmetrics or impact factors. ORCID records identity and claims, not citations. Join `works[].doi` to a bibliometric source for that. - Full text, abstracts or the papers themselves. - Private or trusted-party fields. Only what the researcher published is visible. - Complete publication lists. A record contains what the researcher has claimed, which for many people is a fraction of what they have written. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/orcid-researcher-scraper').call({ orcidIds: ['0000-0002-1825-0097'], searchQuery: '', includeAffiliations: true, includeWorks: true, includeFunding: true, maxWorks: 50, contactEmail: 'you@example.com', }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~orcid-researcher-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"orcidIds":["0000-0002-1825-0097"],"maxWorks":25,"contactEmail":"you@example.com"}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/orcid-researcher-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input Supply `orcidIds`, `searchQuery`, or both. With neither, the run fails immediately. | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `orcidIds` | string[] | no | `[]` | iDs as `0000-0000-0000-0000`. A profile URL is accepted and trimmed to the iD, and the trailing `X` checksum form is handled. Anything that is not 16 characters of iD is dropped; if every entry is malformed the run throws before any request. | | `searchQuery` | string | no | `""` | Fielded search over the public index. Bare words match the whole record; prefixes narrow it, e.g. `family-name:Hopper`, `affiliation-org-name:"ETH Zurich"`, `keyword:bioinformatics`, `digital-object-ids:"10.1038/nature12373"`, `ringgold-org-id:5468`. Combine with `AND` / `OR`, and quote multi-word values. Every iD found is then fetched in full. | | `maxSearchResults` | integer | no | `25` | Ceiling on iDs the search may contribute, `1` to `1000`. Ignored without a query. Search pages are 200 iDs per request. | | `includeAffiliations` | boolean | no | `true` | Attach `employments` and `educations`. Off makes both `null`. | | `includeWorks` | boolean | no | `true` | Attach `works`. Off makes it `null`. This is the flag that decides record size. | | `includeFunding` | boolean | no | `true` | Attach `fundings`. Off makes it `null`. Most records have none regardless, since the section is optional. | | `maxWorks` | integer | no | `200` | Cap on works attached per researcher, `1` to `5000`, newest first. Only read when `includeWorks` is on. | | `contactEmail` | string | no | `""` | Folded into the User-Agent the Actor sends. Optional, and it costs nothing: identified traffic is treated better by scholarly infrastructure than anonymous traffic. | **`maxSearchResults` is the run-time dial, `maxWorks` is the record-size dial.** Every discovered iD costs one full record request, spaced 200 ms apart, so the search ceiling sets how long the run takes almost by itself. The include flags and `maxWorks` change nothing about request count; they only change how much lands in the dataset. A prolific researcher can carry hundreds of works, so for a wide sweep set a high `maxSearchResults` with `includeWorks: false`, then re-run the shortlist with works on. ## Output One record per iD that resolved to a public record. | Field | Type | Notes | |---|---|---| | `orcid` | string | The iD in canonical `0000-0000-0000-0000` form. The join key. | | `url` | string | Public profile page. | | `givenNames` | string \| null | `null` when the researcher has hidden their name. | | `familyName` | string \| null | Same. | | `creditName` | string \| null | Published name, when it differs from given plus family. | | `otherNames` | string[] | Registered aliases, including transliterations. | | `biography` | string \| null | Free text, when public. | | `emails` | string[] | Public addresses only. Almost always empty. | | `researcherUrls` | object[] | Named links, each `{ name, url }`. Entries with no URL are dropped. | | `keywords` | string[] | Self-declared research keywords. | | `countries` | string[] | Country codes from registered addresses. | | `currentAffiliation` | string \| null | Derived: the employment with no end date and the most recent start, falling back to the most recent employment overall. Present even when `includeAffiliations` is off. | | `employments` | object[] \| null | `null` when `includeAffiliations` is off. Keys: `type`, `organization`, `department`, `roleTitle`, `startDate`, `endDate`, `city`, `region`, `country`, `organizationId`, `organizationIdSource`, `url`. | | `educations` | object[] \| null | Same shape, `type` is `education`. | | `fundings` | object[] \| null | `null` when `includeFunding` is off. Keys: `title`, `type`, `organization`, `country`, `startDate`, `endDate`, `grantNumber`, `url`. | | `fundingCount` | number | True total, always present, never affected by the flag. | | `works` | object[] \| null | `null` when `includeWorks` is off. Keys: `title`, `subtitle`, `type`, `publicationDate`, `journalTitle`, `doi`, `url`, `putCode`, `source`. Newest first, truncated to `maxWorks`. | | `worksCount` | number | True total of work groups, not the length of `works`. | | `peerReviewCount` | number | Reviews recorded against the record. | | `createdAt` | string \| null | When the ORCID record was registered, ISO 8601. | | `lastModified` | string \| null | When it was last edited, ISO 8601. The staleness signal. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, arrays cut to one entry each: ```json { "orcid": "0000-0001-5109-3700", "url": "https://orcid.org/0000-0001-5109-3700", "givenNames": "Laurel", "familyName": "Haak", "creditName": "Laurel L Haak", "otherNames": ["L Haak", "Laure Haak"], "biography": "Laurel (Laure) L. Haak (she/they) is an entrepreneur, strategist, researcher …", "emails": [], "researcherUrls": [ { "name": "LinkedIn", "url": "https://www.linkedin.com/in/laurellhaak/" } ], "keywords": ["persistent identifiers", "research policy"], "countries": ["US"], "currentAffiliation": "Ronin Institute for Independent Scholarship 2.0", "employments": [ { "type": "employment", "organization": "Ronin Institute for Independent Scholarship 2.0", "department": null, "roleTitle": "Research Scholar", "startDate": "2025-11", "endDate": null, "city": "Sacramento", "region": "California", "country": "US", "organizationId": "https://ror.org/000xkb537", "organizationIdSource": "ROR", "url": null } ], "educations": [ { "type": "education", "organization": "Stanford University School of Medicine", "department": "Neurosciences", "roleTitle": "PhD", "startDate": null, "endDate": "1997", "city": "Stanford", "region": "California", "country": "US", "organizationId": null, "organizationIdSource": null, "url": null } ], "fundings": [ { "title": "ADVANCE Leadership Award: Women in Science and Engineering", "type": "grant", "organization": "National Science Foundation - Directorate for Education and Human Resources", "country": "US", "startDate": "2006-04-01", "endDate": "2007-03-31", "grantNumber": "0536999", "url": null } ], "fundingCount": 4, "works": [ { "title": "FIELD DAY - Open Research Infrastructure for Education Research with Digital Games", "subtitle": null, "type": "report", "publicationDate": "2025-11-14", "journalTitle": null, "doi": "10.17613/BMHWG-WXZ66", "url": "https://works.hcommons.org/doi/10.17613/bmhwg-wxz66", "putCode": 222309679, "source": "Laurel L Haak" } ], "worksCount": 227, "peerReviewCount": 2, "createdAt": "2012-09-28T17:19:08.336Z", "lastModified": "2026-07-30T19:09:40.855Z", "scrapedAt": "2026-08-06T12:00:00.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** With a search query it is the only place that says how many iDs the query actually produced. ```json { "idsRequested": 27, "idsFromSearch": 25, "idsFailed": 1, "failures": [ { "orcid": "0000-0002-0000-0000", "error": "no public record (404)" } ], "researchersSaved": 26, "filters": { "orcidIds": ["0000-0002-1825-0097", "0000-0001-5109-3700"], "searchQuery": "affiliation-org-name:\"ETH Zurich\" AND keyword:genomics", "maxSearchResults": 25, "includeAffiliations": true, "includeWorks": true, "includeFunding": true, "maxWorks": 200 }, "finishedAt": "2026-08-06T12:03:41.220Z" } ``` `idsRequested` is the de-duplicated union of your explicit iDs and the search hits, so it is usually less than `orcidIds.length + idsFromSearch`. `idsFromSearch` equal to `maxSearchResults` means the search was truncated and there are more matches: raise the ceiling or narrow the query. Every gap between `idsRequested` and `researchersSaved` is itemised in `failures`. ## Behaviour to plan around - **Dates are often partial, and are passed through as written.** `startDate` and `publicationDate` can be `1997`, `2025-11` or `2025-11-14`. Never hand them straight to a date parser that assumes full precision. They sort correctly as strings. - **`worksCount` is not `works.length`.** It counts every work group on the record, before `maxWorks` truncates and regardless of `includeWorks`. Use it as the output measure and `works` as the sample. - **Works are grouped, so one row is one publication.** The same paper claimed by several indexing sources collapses into one entry built from the preferred summary, with the DOI taken from the group's shared identifiers first. A count of rows is a count of distinct works, not of claims. - **The cap keeps the newest.** Works are sorted by `publicationDate` descending before truncation, so `maxWorks: 20` gives the 20 most recent, not an arbitrary slice. - **`null` and `[]` differ for the four optional arrays.** `null` means the flag was off; `[]` means the researcher has none of that thing. `currentAffiliation` is the exception: it is derived internally and is populated even when `employments` is null. - **The data is self-reported and often thin.** An absent work, employment or email is not evidence that it does not exist, only that the researcher did not publish it. `emails` is nearly always empty by default. Treat a sparse record as low information, never as a negative finding. - **`organizationId` is present only when the organisation is disambiguated**, and `organizationIdSource` names which registry it belongs to. Match on the pair, not on the id alone, and fall back to `organization` text when it is null. - **A dead iD never aborts the run.** An unknown iD and a deactivated or locked record are both final for that iD, land in `failures` and the run continues. The Actor only throws when every iD failed. - **A malformed search query fails fast.** Bad query syntax is reported as such rather than retried, so check `failures` before assuming the institution has no researchers. - **Requests are paced 200 ms apart** and transient failures are retried three times with linear backoff. Roughly four records a second is the practical throughput. ## Recipes **Verify a claimed affiliation.** Identity and affiliations only, no works, so the record stays small. ```json { "orcidIds": ["0000-0002-1825-0097", "https://orcid.org/0000-0001-5109-3700"], "includeWorks": false, "includeFunding": false, "contactEmail": "you@example.com" } ``` Compare the claim against `employments[].organization` plus `startDate` and `endDate`, and prefer `organizationId` when it is present. **Expert finding at an institution.** Discover, then rank. ```json { "searchQuery": "affiliation-org-name:\"ETH Zurich\" AND keyword:genomics", "maxSearchResults": 100, "maxWorks": 20, "contactEmail": "you@example.com" } ``` Rank on `worksCount` and on the most recent `works[].publicationDate`, then check `idsFromSearch` against `maxSearchResults` to see whether you saw the whole pool. **From a DOI to its authors' registry entries.** ```json { "searchQuery": "digital-object-ids:\"10.1038/nature12373\"", "maxSearchResults": 50, "includeWorks": false, "contactEmail": "you@example.com" } ``` Each record is one claimant of that paper, with `currentAffiliation` already resolved. **Bibliometric bridge.** Full works for a shortlist, capped at what you will actually join. ```json { "orcidIds": ["0000-0001-5109-3700"], "includeWorks": true, "maxWorks": 500, "contactEmail": "you@example.com" } ``` Take `works[].doi`, drop the nulls, and join against a citation source. Expect a material share of works to carry no DOI at all, especially reports and datasets.