NHS UK $0.8💰 URL | Keyword | Deep Search Scraper
Pricing
from $0.08 / 1,000 results
NHS UK $0.8💰 URL | Keyword | Deep Search Scraper
From $0.8/1k. Scrape nhs.uk Jobs listings into a flat dataset. Extract 50+ fields, including pay band, salary, full description, essential and desirable criteria, PDFs, sponsorship, DBS, employer details, contacts, and apply URL. Search by filters or use any URL.
Pricing
from $0.08 / 1,000 results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
8
Total users
2
Monthly active users
8 days ago
Last modified
Categories
Share
NHS Jobs UK Scraper: Salary, Person Specification and Apply Details for Every Listing
NHS Jobs UK Scraper turns jobs.nhs.uk into a clean, structured feed of health service vacancies. Get 50+ fields per listing: salary parsed into numbers, pay band, contract and working pattern, the full person specification with essential and desirable criteria split out, supporting document IDs, sponsorship, UK registration and DBS flags, employer address and contact details, and the direct apply link. Build a search by keyword, location and seven facet filters, or paste any jobs.nhs.uk URL, then run it once, on a schedule with only new and changed listings returned, or straight into your own app through the API.
Why This Scraper?
- 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
resumeFromRunIdat a prior run or dataset and this run skips every listing it already saved, appending only new ones. - Clear failure signals instead of silent gaps: if the connection setup itself fails, or a pasted resume run ID cannot be read, the run stops immediately with an explanation. A run that legitimately finds zero listings still finishes normally, with a status message describing what happened.
Use Cases
- 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.
Data You Get
Sample shape: values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
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" |
location | "London SW1A 1AA" |
postcode | "SW1A 1AA" |
salary | "£25,000 to £30,000 a year" |
salaryMin | 25000 |
salaryMax | 30000 |
salaryPeriod | "per annum" |
payScheme / grade | "Agenda for change" / "Band 0" |
contractType | "Permanent" |
workingPattern | "Full-time" |
datePosted / datePostedIso | "01 January 2026" / "2026-01-01" |
closingDate / closingDateIso | "15 January 2026" / "2026-01-15" |
applyUrl | "https://www.jobs.nhs.uk/candidate/jobadvert/C0000-00-0000/ats-direct-apply" |
contactEmail / contactPhone | "recruitment@example.org" / "+44 0000 000000" |
personSpecification[0] | {"category": "Qualifications", "essentials": ["Sample essential criterion"], "desirables": ["Sample desirable criterion"]} |
supportingDocuments[0] | {"name": "Job Description.pdf", "sizeText": "PDF, 000 KB", "documentId": "0000000"} |
sponsorshipAvailable / ukRegistrationRequired / dbsCheckRequired | true / true / true |
basicInfo | {"id": "...", "title": "...", ...} (a compact summary of the same record) |
scrapedAt | "2026-01-01T00:00:00.000Z" |
Each record also carries the employer's website, logo image, and full address broken into addressLine1, addressLine2, addressTown, addressCounty and addressLines; a salaryQualifier string (for example "inclusive of HCAS"), contractDuration and interviewDate; the contact person's name and job title alongside a combined contactDetails object, plus an employerDetails object; the job overview, main duties, about-organisation and full job description each as both HTML and plain text; a privacyPolicyUrl and a saveJobUrl. When Incremental mode is on, every row additionally carries changeType, changedFields, firstSeenAt and lastSeenAt.
How to Use
- Pick a mode: search (keyword, location and facet filters) or url (paste any jobs.nhs.uk URL).
- Fill in the fields for that mode, and leave Fetch detail pages on if you want the full 50+ field record rather than just the search-result fields.
- Set Max listings (and Max pages, if you want an explicit cap) to control run size and cost, then click Start.
- Download the dataset as JSON, CSV or Excel, or read it through the API.
Search by keyword and location:
{"mode": "search","keywords": ["nurse"],"location": "Manchester","distance": "10","maxPages": 2,"fetchDetails": true}
Search by pay band and contract type:
{"mode": "search","keywords": ["doctor"],"location": "London","distance": "20","payBands": ["CONSULTANT", "SPECIALTY_DOCTOR"],"contractTypes": ["Permanent", "Fixed-Term"],"workingPatterns": ["full-time"],"maxPages": 3,"maxListings": 100}
Browse a whole staff group across the UK:
{"mode": "search","keywords": [],"location": "","distance": "","staffGroups": ["NURSING_AND_MIDWIFERY_REGD"],"sortBy": "publicationDateDesc","maxListings": 200}
Paste a jobs.nhs.uk URL:
{"mode": "url","urls": ["https://www.jobs.nhs.uk/candidate/search/results?keyword=physiotherapist&location=Birmingham&distance=20","https://www.jobs.nhs.uk/candidate/jobadvert/C0000-00-0000"],"maxPages": 2,"fetchDetails": true}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/jobs-nhs-uk-scraper").call(run_input={"mode": "search", "keywords": ["nurse"], "location": "Manchester", "distance": "10"})for job in client.dataset(run["defaultDatasetId"]).iterate_items():print(job["title"], job["employer"], job["salary"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/jobs-nhs-uk-scraper').call({ mode: 'search', keywords: ['nurse'], location: 'Manchester', distance: '10' });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Resume and recurring updates
- Resume (
resumeFromRunId) continues one interrupted run: paste its run or dataset ID and the actor loads the listings it already saved, skipping them, so this run only appends new listings. - Incremental mode (
incrementalMode) is for scheduling this actor to monitor the same search or URL list over time. Turn it on and every pushed row is taggedchangeType(NEW,UPDATEDwithchangedFields,UNCHANGED,REAPPEAREDorEXPIRED), plusfirstSeenAtandlastSeenAt. The baseline is remembered by the actor itself, keyed on your search filters and thefetchDetailssetting, or onstateKeyif you set one.UNCHANGEDrows are suppressed by default (not pushed, not billed); turn onemitUnchangedto get them anyway.EXPIREDonly fires whenemitExpiredis on and the run completed a full scan of the tracked search, meaning noresumeFromRunIdand nomaxListings/maxPagescap reached; a partial run skips EXPIRED detection that time rather than guessing.REAPPEAREDcan only occur after a listing was first marked gone by an earlieremitExpiredrun that itself completed a full scan; only once a listing has been marked gone that way does a later sighting of it come back taggedREAPPEAREDinstead ofNEW. CombiningresumeFromRunIdwith the very first incremental run bootstraps the baseline from that resumed dataset, but the run fails immediately if the chosen state key already has saved state from an earlier run. With Incremental mode off, output is exactly as a normal run.
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape, the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify -> Settings -> Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. If the picker is empty, you haven't authorized a connector yet.
- For Notion, also set
notionParentPageUrlto the page where items should be created.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | enum | search | search or url. Fields for the other mode are ignored. |
keywords | array of strings | none (prefill: ["doctor"]) | Free-text job-title or skill keywords. One scrape per keyword. Empty means browse without a keyword filter. |
location | string | none (prefill: "London") | Town, city, county or postcode. Empty means nationwide. |
distance | enum | 10 | Search radius in miles (5/10/20/30/50/100), or empty for all of the UK. |
payBands | array of enum | none | Multi-select. Agenda for Change bands and medical/dental grades. |
staffGroups | array of enum | none | Multi-select NHS staff group filter. |
contractTypes | array of enum | none | Multi-select contract type filter. |
workingPatterns | array of enum | none | Multi-select working pattern filter. |
payRanges | array of enum | none | Multi-select salary band filter, in thousands of GBP. |
employerName | string | none | Filter by NHS Trust or employer name, partial match. |
covidJobsOnly | boolean | false | Limit to roles tagged as COVID-related. |
sortBy | enum | "" (Best Match) | How jobs.nhs.uk orders the results. |
urls | array of strings | none (prefill: one example search URL) | URL mode only. Any jobs.nhs.uk search-results or single-advert URL. |
maxPages | integer | 0 | Pages walked per keyword or per pasted URL. 0 means unlimited: the walk stops once a page comes back empty, repeats listings already collected, or Max listings is reached. |
maxListings | integer | 20 | Stop after this many listings across all searches. 0 means unlimited. |
resumeFromRunId | string | none | Continue one interrupted run: paste its run or dataset ID. |
incrementalMode | boolean | false | Recurring monitoring of the same search across scheduled runs. |
stateKey | string | none | Manually name the incremental baseline instead of auto-deriving it from the search filters. |
emitUnchanged | boolean | false | Incremental mode only. Also push (and bill) UNCHANGED rows. |
emitExpired | boolean | false | Incremental mode only. Push a listing once as EXPIRED after a complete scan finds it gone. |
fetchDetails | boolean | true | Fetch each advert's detail page for the full 50+ field record. Off returns search-result-only fields. |
proxy | object | Residential, GB (applied automatically when left empty) | Connection settings. |
mcpConnectors | array | none | Optional: send a summary of each record to apps you authorized under Integrations. |
notionParentPageUrl | string | none | Notion connector only: page under which records are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
Output Example
Sample shape: values are illustrative placeholders, not from a live listing.
{"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"}
Plan Requirement
This actor's default connection setting already routes through the UK residential group required for reliable access (groups: ["RESIDENTIAL"], country: "GB"), so most runs need no changes. At startup the actor also checks that setting and automatically falls back to the standard Apify proxy without a group, and finally to a direct connection, if the first choice cannot reach the site from your account. A run that is refused everywhere still finishes normally and explains what to try. If your plan's connection allowance does not include the residential group, uncheck automatic routing under Connection and paste your own UK connection URLs instead.
FAQ
How much does it cost?
You pay per record returned, plus Apify platform compute and connection usage. The Pricing tab on the actor page shows current rates. Use Max listings to cap the size, and cost, of any run.
Is it legal to scrape NHS Jobs?
This actor collects only publicly available job listings from jobs.nhs.uk. You are responsible for how you use the data: follow the site's terms and the laws that apply to you, particularly around handling recruiter and applicant contact details, and get legal advice if you plan commercial redistribution.
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.
🔗 Want more Jobs data?
Pair this actor with these related scrapers from the same team:
| 💼 Jobs.cz Scrape Czech job listings from Jobs.cz. Search builder (keyword, location, field, salary... | 💼 Reed.co.uk Scraper Scrape Reed.co.uk jobs, companies, courses, and reviews. Search by keyword, location... |
| 🏠 Rightmove From $1/1K. Fast, reliable Rightmove.co.uk scraper for sale, rent, and sold-price... | 💼 s1jobs Scraper Scrape jobs from s1jobs.com across Scotland and the UK. Search by keyword, location, or... |
| 🧩 Zoopla Scraper Scrape UK property from Zoopla: for sale, to rent and Land Registry sold prices. Search... | 🏠 Jitty UK AI Property Scraper Scrape residential property listings from jitty.com, extract properties across the UK... |
💬 Support & custom scrapers
- 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- 🛠️ Need another site, extra fields or a private build? Email abotapi@proton.me or message Telegram @abotapi.
- ⭐ Enjoying it? A quick review on the actor page helps other users find it.