NHS Jobs Scraper icon

NHS Jobs Scraper

Scrape NHS Jobs (nhs.uk) by search filters or URL. Extract 50+ fields including job titles, pay bands, salaries, full descriptions, essential and…

Start using this actor on Apify ↗Need a custom scraper?Contact us ↗

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.

Supported input fields
InputWhat it controls
modestring · requiredSearch 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).

Choices: search, url

keywordsarrayFree-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.
locationstringTown, city, county, or postcode (e.g. "London", "SW1A 1AA", "Manchester"). Leave empty to search nationwide.
distancestringSearch radius around the location in miles. Empty = all of UK.

Choices: , 5, 10, 20, 30, 50, 100

payBandsarrayFilter 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.
staffGroupsarrayFilter by NHS staff group. Multi-select.
contractTypesarrayFilter by contract type. Multi-select.
Show 23 more inputs
More input fields
InputWhat it controls
workingPatternsarrayFilter by working pattern. Multi-select.
payRangesarrayFilter by salary range in thousands of GBP. Multi-select.
employerNamestringFilter by NHS Trust or employer name (e.g. "Royal Marsden", "Guy's and St Thomas"). Partial match.
covidJobsOnlybooleanLimit to roles tagged as COVID-related.
salaryFromintegerLower salary bound in whole pounds, e.g. 30000.
salaryTointegerUpper salary bound in whole pounds, e.g. 40000.
jobReferencestringFind one advert by its reference, e.g. L0014-26-2010.
sortBystringHow jobs.nhs.uk orders the SERP.

Choices: , publicationDateDesc, closingDate, salaryDesc, salaryAsc, distance

urlsarrayOne 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.
startUrlstringAlias for a single entry of Search URLs. Added to the list when set.
maxPagesintegerHow 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.
maxListingsintegerThe 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).
maxItemsintegerAlias of Max listings; used only when Max listings is left at its default.
resumeFromRunIdstringPaste 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.
incrementalModebooleanWhen 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.
stateKeystringManually 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.
emitUnchangedbooleanWhen 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.
emitExpiredbooleanWhen 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.
fetchDetailsbooleanWhen 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.
ignoreFieldsForChangesarrayOutput 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.
mcpConnectorsarrayOptionally 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).
notionParentPageUrlstringURL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.
maxNotifyListingsintegerCap 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.

idID
urlURL (link)
titleTitle
employerEmployer
locationLocation
postcodePostcode
salarySalary
salaryMinMin £
salaryMaxMax £
paySchemePay scheme
gradeGrade
contractTypeContract
Show 45 more fields
workingPatternPattern
datePostedPosted
closingDateCloses
applyUrlApply (link)
contactEmailContact email
contactPhoneContact phone
scrapedAtScraped
changeTypeChange (incremental mode)
changedFieldsChanged fields
firstSeenAtFirst seen (incremental mode)
lastSeenAtLast seen (incremental mode)
detailFetcheddetailFetched
referenceNumberreferenceNumber
employerWebsiteemployerWebsite
addressLine1addressLine1
addressLine2addressLine2
addressTownaddressTown
addressCountyaddressCounty
addressLinesaddressLines
salaryQualifiersalaryQualifier
salaryPeriodsalaryPeriod
contractDurationcontractDuration
datePostedIsodatePostedIso
closingDateIsoclosingDateIso
interviewDateinterviewDate
contactNamecontactName
contactJobTitlecontactJobTitle
contactDetailscontactDetails
employerDetailsemployerDetails
jobOverviewHtmljobOverviewHtml
jobOverviewTextjobOverviewText
mainDutiesHtmlmainDutiesHtml
mainDutiesTextmainDutiesText
aboutOrganisationHtmlaboutOrganisationHtml
aboutOrganisationTextaboutOrganisationText
jobDescriptionHtmljobDescriptionHtml
jobDescriptionTextjobDescriptionText
personSpecificationpersonSpecification
supportingDocumentssupportingDocuments
privacyPolicyUrlprivacyPolicyUrl
sponsorshipAvailablesponsorshipAvailable
ukRegistrationRequiredukRegistrationRequired
dbsCheckRequireddbsCheckRequired
saveJobUrlsaveJobUrl
basicInfobasicInfo

Example 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.

Ready to collect with NHS Jobs Scraper?

Run it on Apify with your own input and export the results as JSON, CSV or Excel.