# NHS Jobs Scraper (`santamaria-automations/nhs-jobs-scraper`) Actor

Scrape live UK healthcare jobs from every NHS trust via the official NHS Jobs public XML API. No API key needed. Returns job title, trust, location, pay band (min/max GBP), contract type, hours, closing date, and full description (text, HTML, Markdown). Pay-per-result.

- **URL**: https://apify.com/santamaria-automations/nhs-jobs-scraper.md
- **Developed by:** [NanoScrape](https://apify.com/santamaria-automations) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 actor starts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## NHS Jobs Scraper - UK Healthcare Vacancies, Pay Bands, Trusts

Pull live UK healthcare vacancies from every NHS trust via the official [NHS Jobs](https://www.jobs.nhs.uk) public XML feed. Each record includes job title, trust name, location, pay band with GBP min and max, contract type, working pattern, closing date, and the full description body (text, HTML and Markdown). With detail mode on, you also get the recruiter contact person (name, position, email, phone), the employer website and address, pay scheme and band, reference number, essential and desirable requirements, and the DBS flag. No API key, no registration, no third-party dependency.

**Contact person:** most listings name a recruiter or hiring manager. You get their name, position and email straight from the vacancy page, ready for outreach.

### Sample output

```json
[
  {
    "id": "C9188-25-1234",
    "title": "Staff Nurse - Cardiology",
    "company_name": "Guy's and St Thomas' NHS Foundation Trust",
    "company": "Guy's and St Thomas' NHS Foundation Trust",
    "location": "London",
    "country": "UK",
    "employment_type": "permanent",
    "hours": "Full-time, Flexible working",
    "salary_min": 34089,
    "salary_max": 41498,
    "salary_currency": "GBP",
    "salary_period": "yearly",
    "salary_text": "£34,089 to £41,498 per annum inc. HCAS",
    "description_full": "We are recruiting a Band 5 Staff Nurse to join our busy Cardiology ward. You will provide safe, evidence-based care...",
    "description_snippet": "We are recruiting a Band 5 Staff Nurse to join our busy Cardiology ward. You will provide safe, evidence-based care...",
    "description_html": "<p>We are recruiting a Band 5 Staff Nurse to join our busy Cardiology ward...</p>",
    "description_md": "We are recruiting a Band 5 Staff Nurse to join our busy Cardiology ward...",
    "posted_at": "2026-08-01",
    "expires_at": "2026-08-15",
    "job_url": "https://www.jobs.nhs.uk/candidate/jobadvert/C9188-25-1234",
    "source_url": "https://www.jobs.nhs.uk/candidate/jobadvert/C9188-25-1234",
    "apply_url": "https://www.jobs.nhs.uk/candidate/jobadvert/C9188-25-1234",
    "source_platform": "nhs-jobs",
    "search_query": "nurse in London",
    "scraped_at": "2026-08-06T10:30:00Z",
    "contact_name": "Ceri Cole",
    "contact_firstname": "Ceri",
    "contact_lastname": "Cole",
    "contact_position": "Maternity Triage Manager",
    "contact_email": "ceri.cole@nhs.net",
    "contact_emails": ["ceri.cole@nhs.net"],
    "contact_phone": null,
    "contact_phones": null,
    "company_website": "https://www.pah.nhs.uk",
    "company_address": "The Princess Alexandra Hospital, Harlow, Essex, CM20 1QX, United Kingdom",
    "pay_scheme": "Agenda for change",
    "pay_band": "Band 6",
    "working_pattern": "Full-time",
    "reference_number": "001076",
    "requirements_must": [{"category": "Qualifications", "text": "Registered NMC Midwife"}],
    "requirements_nice": null,
    "dbs_check_required": true
  },
  {
    "id": "C9235-25-0891",
    "title": "Consultant Radiologist",
    "company": "University Hospitals Birmingham NHS Foundation Trust",
    "location": "Birmingham",
    "country": "UK",
    "employment_type": "permanent",
    "hours": "Full-time",
    "salary_min": 99532,
    "salary_max": 131964,
    "salary_currency": "GBP",
    "salary_period": "yearly",
    "posted_at": "2026-07-28",
    "expires_at": "2026-08-25"
  },
  {
    "id": "C9421-25-4402",
    "title": "Band 3 Healthcare Assistant",
    "company": "Manchester University NHS Foundation Trust",
    "location": "Manchester",
    "country": "UK",
    "employment_type": "fixed-term",
    "hours": "Part-time",
    "salary_min": 24071,
    "salary_max": 25674,
    "salary_currency": "GBP",
    "salary_period": "yearly"
  }
]
```

### Pricing

| Event | Price |
|-------|-------|
| Actor start | $0.001 |
| SERP result (per job) | $0.003 |
| Detail result (per job, with full description) | $0.005 |

Around $3 per 1000 jobs without descriptions, or $8 per 1000 jobs with the full description body. If a description fetch fails, you are not charged for the detail event.

New to Apify? Every account gets a $5 free monthly platform credit, enough for around 5,000 results on this actor before you commit to paying anything. Test extensively first.

### Input

No API key required. Just pick your keywords and (optionally) a location.

| Field | Type | Required | Description | Example |
|-------|------|----------|-------------|---------|
| `keywords` | array | No | NHS role keywords. Each runs as a separate search. Results are deduplicated by vacancy ID. | `["nurse", "midwife", "radiographer"]` |
| `location` | string | No | UK city, town, or postcode. | `London` or `SW1A 1AA` |
| `distanceMiles` | integer | No | Radius around the location in miles (0 to 100). Only meaningful when a location is set. | `10` |
| `payMin` | integer | No | Filter to jobs paying at least this many GBP per year. | `30000` |
| `payMax` | integer | No | Filter to jobs paying at most this many GBP per year. | `80000` |
| `sortBy` | string | No | Result order. `"newest"` returns the most recently published vacancies first and keeps only rows whose title or summary mention your keyword (the NHS API widens matching when sorting by date), paging until your cap is reached. `"relevance"` uses the platform default. Default `"newest"`. | `"newest"` |
| `postedWithinDays` | number | No | Only jobs published within the last N days (decimals allowed). Empty means no limit. | `7` |
| `maxResults` | integer | No | Total cap across all keywords. Default 100. | `500` |
| `maxResultsPerQuery` | integer | No | Cap per keyword. Default 250. | `250` |
| `includeJobDetails` | boolean | No | Fetch each vacancy's detail page for the full description body plus contact person, employer, pay scheme, requirements and DBS fields. Default true. | `false` |
| `maxConcurrency` | integer | No | Concurrent detail-page requests (1 to 20). Default 5. | `5` |
| `titleFilter` | string | No | Comma-separated title terms (OR-match, case-insensitive). Only jobs whose title contains one of the terms are returned. Non-matching rows are dropped before any detail fetch, so you don't pay for them. Leave empty to disable. | `"nurse, doctor"` |

#### Precision title filter

```json
{
  "keywords": ["healthcare"],
  "titleFilter": "nurse, doctor",
  "maxResults": 50
}
```

Only jobs with "nurse" or "doctor" in the title are returned. Non-matching rows are dropped before any detail fetch.

### Output fields

**Core**

- `id`: Vacancy ID (stable dedup key across the site).
- `title`: Job title.
- `company_name`: NHS trust or organisation name (canonical).
- `company`: Deprecated duplicate of `company_name`, see Deprecation notes below.
- `location`: City, town, or postcode as published.
- `country`: Always "UK".

**Employment**

- `employment_type`: Permanent, fixed-term, bank, locum, secondment, or apprenticeship.
- `hours`: Working pattern from the vacancy page, for example `Full-time, Flexible working` or `Part-time`. Filled when `includeJobDetails` is true.

**Salary**

- `salary_min`, `salary_max`: Numeric GBP bounds.
- `salary_currency`: Always "GBP".
- `salary_period`: yearly, hourly, daily, or sessional.
- `salary_text`: Raw pay string as the trust published it.

**Description**

- `description_snippet`: SERP-level teaser.
- `description_full`: Full plain-text body (job summary plus main duties) from the vacancy detail page. Only when `includeJobDetails: true`.
- `description_html`: The same body as cleaned HTML.
- `description_md`: The same body as Markdown.

**Contact person** (only when `includeJobDetails` is true; `null` when the listing does not publish it)

- `contact_name`: Recruiter or hiring manager named on the listing. Sometimes a team name.
- `contact_firstname`, `contact_lastname`: Split of the name when it looks like a person (skipped for team or organisation names).
- `contact_position`: Their job title.
- `contact_email`, `contact_emails`: Primary email and the full list.
- `contact_phone`, `contact_phones`: Primary phone and the full list. Often empty; placeholder numbers such as `0000000000` are dropped.

**Employer** (detail mode)

- `company_website`: Employer website URL.
- `company_about`: The employer's "About us" text.
- `region`: County of the job location.
- `company_address`: Joined job-location address, plus `company_address_street`, `company_address_city`, `company_address_region`, `company_address_postcode`, `company_address_country`.
- `employer_privacy_notice_url`: Link to the employer's privacy notice.

**Pay and role details** (detail mode)

- `pay_scheme`: For example "Agenda for change" or "Other".
- `pay_band`: For example "Band 6". Empty for non-Agenda-for-change schemes.
- `working_pattern`: For example "Full-time" or "Part-time".
- `reference_number`: The employer's own job reference.
- `job_summary`: The "Job summary" block as plain text (also included in the description).
- `requirements_must`, `requirements_nice`: Essential and desirable person-specification criteria as arrays of `{category, text}`.
- `requirements`: The same criteria as one flat text block.
- `dbs_check_required`: `true` when the listing carries a Disclosure and Barring Service check notice.

**Dates and links**

- `posted_at`: When the vacancy was published.
- `expires_at`: Application closing date.
- `job_url`: Canonical NHS Jobs vacancy page URL. `source_url` carries the same value.
- `source_url`: Original NHS Jobs listing URL.
- `apply_url`: Where to apply. For vacancies hosted on an external applicant tracking system (detail mode) this is the employer's own ATS link; otherwise the NHS Jobs vacancy page.
- `source_platform`: Always "nhs-jobs".
- `search_query`: The keyword + location string that produced this hit.
- `scraped_at`: ISO timestamp when the record was written.

#### Deprecation notes

The following field name is deprecated and will be removed after **2027-09-29** (one-year backward-compatibility window):

- `company` -> use `company_name` (canonical since 2026-09-29)

Both fields are emitted in every dataset row during the window. Please migrate downstream consumers to the canonical name before the removal date.

### Use with AI Agents (MCP)

Connect this actor to any MCP-compatible AI client: Claude Desktop, Claude.ai, Cursor, VS Code, LangChain, LlamaIndex, or custom agents.

Configure MCP server with NHS Jobs Scraper. Get a ready-to-use configuration for your MCP client with the NHS Jobs Scraper preconfigured at `mcp.apify.com?tools=santamaria-automations/nhs-jobs-scraper`. You can connect to the Apify MCP Server using clients like Tester MCP Client, or any other MCP client of your choice. See the Apify MCP docs for implementation details, the MCP protocol introduction for an overview, or the Apify blog post for a walkthrough.

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=santamaria-automations/nhs-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}
```

For per-tool schemas and copy-paste snippets for MCP clients, see the [MCP tab](https://console.apify.com/actors/OXmm1l7hlj2dubRNX/info/api/mcp?build=latest).

See the [auto-generated API tab](https://console.apify.com/actors/OXmm1l7hlj2dubRNX/info/api) for language-specific examples (cURL, JS, Python, .NET, Ruby, PHP).

### Why this actor

- No API key to manage, no rate-limit tier to worry about, no third-party sign-up. NHS Jobs is a public feed.
- Structured pay bands (`salary_min` / `salary_max` / `salary_period`) parsed out of the raw NHS pay strings, which include HCAS, London weighting, and sessional consultant rates.
- Every NHS trust in one query. You do not need to enumerate trusts or run 200 separate searches.
- Recruiter contact person, employer website and address, pay scheme and band, reference number and person-specification requirements on every detail row.
- Full description body (with `includeJobDetails: true`) so downstream LLM parsing or JD matching has something to work with.
- Failed detail fetches are not charged, so you never pay for empty data.

### Common use cases

- Refresh a Southampton private-hospital nursing pipeline daily so the recruiter has a live list of NHS staff nurses within 15 miles.
- Alert on consultant radiology vacancies at any of the top-20 English trusts paying above £110k.
- Feed every newly-posted Band 5 to Band 7 midwife role into a Slack channel for a maternity-agency shift-fill workflow.
- Track vacancy count per trust per week to spot which trusts are struggling to hire and warrant an agency conversation.

### Notes and limits

- With `sortBy: "newest"`, off-topic rows the NHS API returns for a keyword are dropped, so more pages are read to reach your cap. Very rare keywords can return fewer results than `maxResults`.
- The public NHS Jobs feed caps deep pagination at a few hundred pages per query. Very broad searches ("nurse" with no location) may hit that ceiling before your `maxResults`.
- Some vacancies do not publish a structured pay band. `salary_min` and `salary_max` will be null; `salary_text` still holds the raw string.
- Sessional pay (typical for consultants) surfaces as `salary_period: "sessional"` with the numeric value per session.
- The vacancy page does not publish a reliable NHS region, so there is no region field. Detail-page HTML can shift when NHS Jobs updates their template. If a description cannot be parsed, the job is still emitted with SERP-level data and you are not charged for the detail event.

### Related actors

Feed the `company_website` field from this actor's output into one of the two below to enrich rows with contact data.

- [Website Email & Phone Scraper](https://apify.com/santamaria-automations/website-email-scraper): fast regex-based email and phone extraction from a website.
- [Website Contact Extractor](https://apify.com/santamaria-automations/website-contact-extractor): LLM-based, higher precision on names, roles and department emails.
- [Reed UK Scraper](https://apify.com/santamaria-automations/reed-uk-scraper): general UK job board listings.
- [Totaljobs Scraper](https://apify.com/santamaria-automations/totaljobs-scraper): UK jobs across sectors.
- [Guardian Jobs Scraper](https://apify.com/santamaria-automations/guardianjobs-scraper): UK public sector, health and charity roles.
- [CharityJob UK Scraper](https://apify.com/santamaria-automations/charityjob-uk-scraper): UK charity and non-profit roles.
- [Jobs.ac.uk Scraper](https://apify.com/santamaria-automations/jobs-ac-uk-scraper): UK academic and research roles.

### Support

- **Questions or issues?** Email contact@nanoscrape.com, we typically reply within 6 hours. You can also open a ticket in the [Issues tab](https://console.apify.com/actors/OXmm1l7hlj2dubRNX/info/issues) on the actor page.
- **Feature request for this actor?** Open a ticket in the [Issues tab](https://console.apify.com/actors/OXmm1l7hlj2dubRNX/info/issues) or email contact@nanoscrape.com with the platform field you need and (if possible) a URL that shows the data live.
- **Need a scraper for a job board we don't cover yet?** Email contact@nanoscrape.com with the target site + your rough usage volume. We ship one-off boards regularly.

# Actor input Schema

## `keywords` (type: `array`):

One or more NHS role keywords (e.g. 'nurse', 'doctor', 'radiographer', 'admin'). Each runs as a separate search. Results are deduplicated across queries.

## `location` (type: `string`):

UK city, town, or postcode to filter by (e.g. 'London', 'Manchester', 'SW1A 1AA'). Optional.

## `distanceMiles` (type: `integer`):

Radius in miles around the location. Only meaningful when 'Location' is set.

## `payMin` (type: `integer`):

Filter to jobs paying at least this many GBP per year. Optional.

## `payMax` (type: `integer`):

Filter to jobs paying at most this many GBP per year. Optional.

## `sortBy` (type: `string`):

Order of results. 'newest' returns the most recently published vacancies first, and drops rows whose title or summary does not mention your keyword (the NHS API widens keyword matching when sorting by date), so the pages are walked until your result cap is reached. 'relevance' uses the platform's default relevance ranking.

## `postedWithinDays` (type: `number`):

Only return jobs published within the last N days (decimals allowed, e.g. 0.5 for 12 hours). Leave empty for no limit. With 'newest' sort, paging stops at the first older job.

## `titleFilter` (type: `string`):

Comma-separated list of terms to match against job titles (case-insensitive OR-match). Only jobs whose title contains one of the terms are emitted. Non-matching rows are dropped before the detail-fetch event, so you don't pay for filtered-out jobs. Leave empty to disable.

## `maxResults` (type: `integer`):

Total cap across all keywords.

## `maxResultsPerQuery` (type: `integer`):

Maximum results per keyword.

## `includeJobDetails` (type: `boolean`):

When enabled, each vacancy's detail page is fetched and the full description is added to the record. Adds one extra HTTP request per job.

## `maxConcurrency` (type: `integer`):

Maximum number of concurrent detail-page requests.

## Actor input object example

```json
{
  "keywords": [
    "nurse"
  ],
  "location": "London",
  "distanceMiles": 10,
  "payMin": 30000,
  "payMax": 80000,
  "sortBy": "newest",
  "postedWithinDays": 7,
  "titleFilter": "",
  "maxResults": 100,
  "maxResultsPerQuery": 250,
  "includeJobDetails": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

## `jobListings` (type: `string`):

Dataset containing scraped UK NHS job listings. Each record includes: id, title, company_name (NHS trust; `company` is a deprecated duplicate removed after 2027-09-29), location, country (UK), employment_type, hours (working pattern), salary (min/max/currency/period/text), description (full + snippet + html + md), posted_at, expires_at, job_url, source_url, apply_url, search_query, scraped_at. With includeJobDetails: contact_name, contact_firstname, contact_lastname, contact_position, contact_email, contact_emails, contact_phone, contact_phones, company_website, company_about, region, company_address (+ \_street, \_city, \_region, \_postcode, \_country), pay_scheme, pay_band, working_pattern, reference_number, job_summary, requirements_must, requirements_nice, requirements, dbs_check_required, employer_privacy_notice_url.

## `company_name` (type: `string`):

Employer name (canonical since 2026-09-29).

## `company` (type: `string`):

DEPRECATED (2026-09-29): use `company_name`. This field will be removed after 2027-09-29.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [
        "nurse"
    ],
    "sortBy": "newest",
    "maxResults": 100,
    "maxResultsPerQuery": 250,
    "maxConcurrency": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("santamaria-automations/nhs-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": ["nurse"],
    "sortBy": "newest",
    "maxResults": 100,
    "maxResultsPerQuery": 250,
    "maxConcurrency": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("santamaria-automations/nhs-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [
    "nurse"
  ],
  "sortBy": "newest",
  "maxResults": 100,
  "maxResultsPerQuery": 250,
  "maxConcurrency": 5
}' |
apify call santamaria-automations/nhs-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,santamaria-automations/nhs-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/OXmm1l7hlj2dubRNX/builds/JjGMRxOcn4pMr6CPC/openapi.json
