NHS Jobs Scraper at a glance
NHS Jobs Scraper is a ready-to-run Apify actor from abotapi for collecting jobs data. Its documented output includes id, url, title, employer and location, which you can export as JSON, CSV or Excel or read through the Apify API. It costs $1.00 per 1,000 results on the Apify Free plan and from $0.08 on paid plans, with smaller per-run or optional feature charges listed on Apify.
- Runs on
- Apify cloud: console, API, schedules and integrations
- Data type
- Jobs, Automation, Developer Tools
- Region
- Europe
- Pricing
- $1.00 per 1,000 results (Free plan), from $0.08 on paid plans
- Output
- Documented below · JSON, CSV, Excel
- Inputs
- 30 options
Scrape NHS Jobs (nhs.uk) by search filters or URL. Extract 50+ fields including job titles, pay bands, salaries, full descriptions, essential and desirable criteria, PDF attachments, visa sponsorship, DBS requirements, employer details, contacts and application URLs.
What NHS Jobs Scraper can do
- 50+ fields per record, including fields many NHS Jobs feeds drop: desirable person-specification criteria kept separate from essential, supporting document IDs, sponsorship eligibility, UK registration requirement, DBS check requirement, salary parsed into salaryMin / salaryMax / salaryPeriod, and the full address split into postcode, town, county and address lines.
- Two ways to start a run: build a search with keyword, location and up to seven facet filters, or paste any jobs.nhs.uk URL (a search-results page or a single advert) and let the actor take it from there.
- Resilient to interruption: each finished record is written to the dataset as soon as it is parsed, so an interrupted run still keeps everything it managed to finish.
- Forward pagination that respects your input: starts from the page number in a pasted URL when given one, walks forward up to Max pages if you set it, and stops naturally once a page repeats already-collected listings or comes back empty, no fixed page ceiling required.
- Incremental mode for recurring monitoring: schedule the same search and get only new and changed rows by default, with unchanged listings suppressed and expired or reappeared listings tracked correctly across runs.
- Resume without re-paying: point resumeFromRunId at a prior run or dataset and this run skips every listing it already saved, appending only new ones.
Choose what to collect
These inputs let you configure NHS Jobs Scraper - Healthcare Jobs, Salaries & Employers. Open the actor on Apify to enter your values and review the current options.
| Input | What it controls |
|---|---|
modestring · required | Search mode = pick keyword + location + filters below. URL mode = paste any jobs.nhs.uk URL refined in a browser (recommended for filter combinations not exposed below). |
keywordsarray | Free-text job-title or skill keywords (e.g. "doctor", "nurse", "radiographer"). One scrape per keyword. Leave empty to browse all jobs filtered only by location + facets. |
locationstring | Town, city, county, or postcode (e.g. "London", "SW1A 1AA", "Manchester"). Leave empty to search nationwide. |
distancestring | Search radius around the location in miles. Empty = all of UK. |
payBandsarray | Filter by pay band. Multi-select. BAND_2 to BAND_9 are Agenda for Change bands; SPECIALTY_DOCTOR / FOUNDATION_DOCTOR / CONSULTANT / SPECIALTY_REGISTRAR / DOCTOR_OTHER / SPECIALIST / VERY_SENIOR_MANAGER are medical-and-dental grades. |
staffGroupsarray | Filter by NHS staff group. Multi-select. |
contractTypesarray | Filter by contract type. Multi-select. |
Show 23 more inputs
| Input | What it controls |
|---|---|
workingPatternsarray | Filter by working pattern. Multi-select. |
payRangesarray | Filter by salary range in thousands of GBP. Multi-select. |
employerNamestring | Filter by NHS Trust or employer name (e.g. "Royal Marsden", "Guy's and St Thomas"). Partial match. |
covidJobsOnlyboolean | Limit to roles tagged as COVID-related. |
salaryFrominteger | Lower salary bound in whole pounds, e.g. 30000. |
salaryTointeger | Upper salary bound in whole pounds, e.g. 40000. |
jobReferencestring | Find one advert by its reference, e.g. L0014-26-2010. |
sortBystring | How jobs.nhs.uk orders the SERP. |
urlsarray | One or more jobs.nhs.uk URLs: search results (any filter, page or limit), the bare search landing page (mapped to nationwide results), or a single job advert. Multi-URL supported; each pages forward independently. Left untouched, URL mode scrapes one example of each search-results shape (capped by Max listings); single-advert, job-reference and landing-page examples are in the prefill only because adverts expire. Filter fields are ignored in URL mode. |
startUrlstring | Alias for a single entry of Search URLs. Added to the list when set. |
maxPagesinteger | How many SERP pages to walk per keyword × location combination (or per pasted URL). Each page returns up to 50 listings. 0 (default) means unlimited: the walk stops naturally once a page comes back empty or repeats listings already collected, or sooner once Max listings is reached. Set a number only for an explicit page cap below that natural stop. |
maxListingsinteger | The sole soft cap on the run: stop after this many listings across all searches. Defaults to 20 so a run stays small unless you raise it. 0 = no explicit cap (still bounded by the natural stop on Max pages and a hard ceiling of 1000000 listings). |
maxItemsinteger | Alias of Max listings; used only when Max listings is left at its default. |
resumeFromRunIdstring | Paste a previous run ID or dataset ID to continue a large walk-all pull: listings already saved by that run are loaded before scraping starts and skipped, so this run only appends new listings. Leave empty for a normal fresh run. Distinct from incrementalMode below. |
incrementalModeboolean | When ON, the actor remembers this search's results between scheduled runs (keyed by Search filters + Fetch detail pages, or by State key below) and tags every row with changeType (NEW/UPDATED/UNCHANGED/REAPPEARED/EXPIRED), changedFields, firstSeenAt, lastSeenAt. UNCHANGED rows are suppressed (not pushed, not billed) unless Emit unchanged is ON. Distinct from resumeFromRunId above (one interrupted run vs. a remembered recurring search); combining them on the FIRST incremental run bootstraps the baseline from that resume dataset, but fails if this state key already has saved state. Default OFF. |
stateKeystring | Manually name the incremental baseline instead of auto-deriving it from Search filters + Fetch detail pages. Use this to run several independent monitoring campaigns against the SAME search scope, or to keep a stable baseline name across an unrelated filter tweak. Ignored unless Incremental mode is ON. |
emitUnchangedboolean | When ON, UNCHANGED rows are pushed (and billed) too, tagged changeType=UNCHANGED. Default OFF: only NEW/UPDATED/REAPPEARED/EXPIRED rows are pushed, so a stable recurring run costs close to nothing. Ignored unless Incremental mode is ON. |
emitExpiredboolean | When ON, listings that were previously tracked but no longer appear are pushed once as changeType=EXPIRED. Only fires after a run that completed a full scan (no resumeFromRunId, no Max listings/Max pages cap hit), a partial run cannot tell "gone" apart from "not reached yet", so EXPIRED detection is skipped that run and logged. Ignored unless Incremental mode is ON. |
fetchDetailsboolean | When ON, also fetch each advert's detail page for full job summary, main duties, about us, job description (HTML + text), person specification (essential + desirable criteria split out), employer address + website, contact email/phone, supporting PDF download IDs, sponsorship + UK registration + DBS flags, and apply URL. SERP cards already carry id, title, employer, location, salary, dates so simple feeds work fine with this OFF. Default ON. Each delivered job whose detail page is read is charged as one detail-page event on top of the per-result charge; jobs whose detail page is refused, and jobs incremental mode suppresses as unchanged, are not charged. |
ignoreFieldsForChangesarray | Output field names (or dotted paths such as contactDetails.email) that must NOT count as a change when Incremental mode compares a listing with its saved baseline. Ignored fields stay in the output rows but never turn a row into UPDATED and never appear in changedFields. By default only scrapedAt is excluded. Ignored unless Incremental mode is ON. |
mcpConnectorsarray | Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON, the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com). |
notionParentPageUrlstring | URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors. |
maxNotifyListingsinteger | Cap on items written to each connector per run. Does not affect the dataset. |
Fields in the output
The documented dataset includes the fields below. Availability can depend on the source page and selected mode.
idIDurlURL (link)titleTitleemployerEmployerlocationLocationpostcodePostcodesalarySalarysalaryMinMin £salaryMaxMax £paySchemePay schemegradeGradecontractTypeContractShow 45 more fields
workingPatternPatterndatePostedPostedclosingDateClosesapplyUrlApply (link)contactEmailContact emailcontactPhoneContact phonescrapedAtScrapedchangeTypeChange (incremental mode)changedFieldsChanged fieldsfirstSeenAtFirst seen (incremental mode)lastSeenAtLast seen (incremental mode)detailFetcheddetailFetchedreferenceNumberreferenceNumberemployerWebsiteemployerWebsiteaddressLine1addressLine1addressLine2addressLine2addressTownaddressTownaddressCountyaddressCountyaddressLinesaddressLinessalaryQualifiersalaryQualifiersalaryPeriodsalaryPeriodcontractDurationcontractDurationdatePostedIsodatePostedIsoclosingDateIsoclosingDateIsointerviewDateinterviewDatecontactNamecontactNamecontactJobTitlecontactJobTitlecontactDetailscontactDetailsemployerDetailsemployerDetailsjobOverviewHtmljobOverviewHtmljobOverviewTextjobOverviewTextmainDutiesHtmlmainDutiesHtmlmainDutiesTextmainDutiesTextaboutOrganisationHtmlaboutOrganisationHtmlaboutOrganisationTextaboutOrganisationTextjobDescriptionHtmljobDescriptionHtmljobDescriptionTextjobDescriptionTextpersonSpecificationpersonSpecificationsupportingDocumentssupportingDocumentsprivacyPolicyUrlprivacyPolicyUrlsponsorshipAvailablesponsorshipAvailableukRegistrationRequiredukRegistrationRequireddbsCheckRequireddbsCheckRequiredsaveJobUrlsaveJobUrlbasicInfobasicInfoExample output
An example from this actor’s documentation. Values are illustrative; this is not a fresh live result.
{
"id": "C0000-00-0000",
"url": "https://www.jobs.nhs.uk/candidate/jobadvert/C0000-00-0000",
"referenceNumber": "000-X000-XXX",
"title": "Sample Job Title",
"employer": "Sample NHS Foundation Trust",
"employerWebsite": "https://example.org/working-with-us",
"employerLogoDataUri": "data:image/png;base64,...",
"location": "London SW1A 1AA",
"postcode": "SW1A 1AA",
"addressLine1": "Sample Hospital",
"addressLine2": null,
"addressTown": "London",
"addressCounty": null,
"addressLines": [
"Sample Hospital",
"London",
"SW1A 1AA"
],
"salary": "£25,000 to £30,000 a year",
"salaryQualifier": "inclusive of HCAS",
"salaryMin": 25000,
"salaryMax": 30000,
"salaryPeriod": "per annum",
"payScheme": "Agenda for change",
"grade": "Band 0",
"contractType": "Fixed-Term",
"contractDuration": "12 months",
"workingPattern": "Full-time",
"datePosted": "01 January 2026",
"datePostedIso": "2026-01-01",
"closingDate": "15 January 2026",
"closingDateIso": "2026-01-15",
"interviewDate": null,
"applyUrl": "https://www.jobs.nhs.uk/candidate/jobadvert/C0000-00-0000/ats-direct-apply",
"contactName": "Jane Doe",
"contactJobTitle": "Recruitment Lead",
"contactEmail": "recruitment@example.org",
"contactPhone": "+44 0000 000000",
"contactDetails": {
"jobTitle": "Recruitment Lead",
"name": "Jane Doe",
"email": "recruitment@example.org",
"phone": "+44 0000 000000"
},
"employerDetails": {
"name": "Sample NHS Foundation Trust",
"website": "https://example.org/working-with-us",
"address": [
"Sample Hospital",
"London",
"SW1A 1AA"
],
"postcode": "SW1A 1AA"
},
"jobOverviewHtml": "<p>Sample short job overview HTML appears here.</p>",
"jobOverviewText": "Sample short job overview text appears here.",
"mainDutiesHtml": "<p>Sample main duties HTML appears here.</p>",
"mainDutiesText": "Sample main duties text appears here.",
"aboutOrganisationHtml": "<p>Sample about-us HTML appears here.</p>",
"aboutOrganisationText": "Sample about-us text appears here.",
"jobDescriptionHtml": "<p>Sample full job description HTML appears here.</p>",
"jobDescriptionText": "Sample full job description text appears here.",
"personSpecification": [
{
"category": "Qualifications",
"essentials": [
"Sample essential criterion"
],
"desirables": [
"Sample desirable criterion"
]
},
{
"category": "Experience",
"essentials": [
"Sample essential criterion"
],
"desirables": []
}
],
"supportingDocuments": [
{
"name": "Job Description.pdf",
"sizeText": "PDF, 000 KB",
"documentId": "0000000",
"downloadFormUrl": "https://www.jobs.nhs.uk/candidate/search/save-job"
}
],
"privacyPolicyUrl": "https://example.org/privacy",
"sponsorshipAvailable": true,
"ukRegistrationRequired": true,
"dbsCheckRequired": true,
"saveJobUrl": "https://www.jobs.nhs.uk/candidate/search/save-job/C0000-00-0000",
"basicInfo": {
"id": "C0000-00-0000",
"title": "Sample Job Title",
"url": "https://www.jobs.nhs.uk/candidate/jobadvert/C0000-00-0000",
"employer": "Sample NHS Foundation Trust",
"location": "London SW1A 1AA",
"salary": "£25,000 to £30,000 a year",
"datePosted": "01 January 2026",
"closingDate": "15 January 2026",
"contractType": "Permanent",
"workingPattern": "Full-time"
},
"scrapedAt": "2026-01-01T00:00:00.000Z"
}Ways to use this data
- Healthcare recruiters and agencies: track new roles across NHS Trusts by staff group, pay band or region to spot hiring surges.
- Job boards and aggregators: mirror jobs.nhs.uk into your own listings pages with salary, contract and address fields already parsed out.
- Workforce and pay research: build a dataset of salary bands, contract types and working patterns across the NHS to analyse pay trends by region or specialty.
- International recruitment and sponsorship desks: filter for roles with sponsorship, UK registration and DBS flags to shortlist positions that fit a candidate's circumstances.
- Compliance and application review: pull the full essential and desirable criteria per category to check applications against requirements before submitting.
Before you run
Start with a small input and check the resulting records against your expected fields. Source content and actor options can change; the current Apify listing is the reference for availability and pricing.
Questions about this actor
What data does NHS Jobs Scraper return?
The documented output for NHS Jobs Scraper includes id, url, title, employer, location, postcode, salary and min £. Availability of each field depends on the source page and the selected mode.
How much does NHS Jobs Scraper cost?
It costs $1.00 per 1,000 results on the Apify Free plan and from $0.08 on paid plans, with smaller per-run or optional feature charges listed on Apify. The Apify listing shows the current rate for every plan.
Can I get only new or changed listings on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only NEW and UPDATED listings by default, and UNCHANGED ones are not billed unless you turn Emit unchanged on.
What's the difference between Resume and Incremental mode?
resumeFromRunId continues ONE specific interrupted run: paste its run or dataset ID and this run skips everything that run already saved. Incremental mode is for scheduling the SAME search repeatedly and getting a change feed across runs. They solve different problems, and can be combined on the very first incremental run to seed its baseline from a resumed dataset.
Why did my run finish with zero listings instead of failing?
If every request this run made was refused, or the search genuinely has no matching listings, the run still finishes normally with zero rows: a status message on the run explains what happened and how to fix a persistent connection problem. The run only fails outright when something is set up wrong before scraping even starts, for example a resumeFromRunId that does not point at a run or dataset this account can read, or a connection configuration error.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push a summary of results into Notion, Linear or Airtable.
