{
  "openapi": "3.0.1",
  "info": {
    "title": "NHS Jobs Scraper — Vacancies & Recruiter Contacts",
    "description": "Scrape jobs.nhs.uk: every live NHS and private-healthcare vacancy with band, pay scheme, salary, contract type, employer address & website — plus the recruiter contact block (name, job title, email, phone) that 9 in 10 adverts publish. Filter by band, staff group, locum/bank, location & date.",
    "version": "0.1",
    "x-build-id": "sDPeBRqJj0Tuo5Qc9"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/scrapersdelight~nhs-jobs-scraper/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-scrapersdelight-nhs-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/scrapersdelight~nhs-jobs-scraper/runs": {
      "post": {
        "operationId": "runs-sync-scrapersdelight-nhs-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/scrapersdelight~nhs-jobs-scraper/run-sync": {
      "post": {
        "operationId": "run-sync-scrapersdelight-nhs-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "properties": {
          "startUrls": {
            "title": "Start URLs (paste searches built on the site)",
            "type": "array",
            "description": "Paste jobs.nhs.uk URLs — either search-results URLs (build the search in the site's own UI, copy the address bar) or single job-advert URLs (/candidate/jobadvert/REF). Every filter in a pasted URL is preserved; only &page= is dropped so the Actor can paginate. IMPORTANT: when this list is non-empty it IS the search, and the keyword / location / band / staff-group / contract / pattern / salary fields below are ignored for that run (the row-level options — max jobs, details, contact details, posted/closing windows, output format — still apply).",
            "items": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "url": {
                  "type": "string",
                  "title": "URL of a web page",
                  "format": "uri"
                }
              }
            }
          },
          "keyword": {
            "title": "Keyword",
            "type": "string",
            "description": "Free-text job title, skill or speciality, exactly as typed into the site's 'What?' box (e.g. 'nurse', 'radiographer', 'consultant psychiatrist'). Leave empty to sweep every live advert (~11,600)."
          },
          "location": {
            "title": "Location",
            "type": "string",
            "description": "UK town, city, county or postcode (e.g. 'Leeds', 'Manchester', 'SW1A'). Leave empty for the whole UK."
          },
          "distance": {
            "title": "Search radius (miles)",
            "enum": [
              "5",
              "10",
              "20",
              "30",
              "50",
              "100"
            ],
            "type": "string",
            "description": "Radius around the Location. Only these six values exist on jobs.nhs.uk. Ignored (and logged) when no Location is set — the site itself ignores it there.",
            "default": "10"
          },
          "employer": {
            "title": "Employer name contains",
            "type": "string",
            "description": "MEASURED WARNING — this is a fuzzy OR-token match, NOT an exact employer filter. Searching 'Guys and St Thomas NHS Foundation Trust' returns 5,118 of 11,612 adverts because it matches any advert containing any of those words. Use a distinctive single word ('Barchester', 'Barts') and filter the output yourself if you need one exact employer."
          },
          "jobReference": {
            "title": "Job reference (single advert lookup)",
            "type": "string",
            "description": "A jobs.nhs.uk advert reference, e.g. 'C9426-26-0318'. Returns exactly that one advert. Combine with 'Fetch job-advert details' for a full single-record pull."
          },
          "payBand": {
            "title": "Pay band / grade",
            "type": "array",
            "description": "Agenda for Change bands and medical grades. Pick as many as you like — they are OR'd together (verified additive: BAND_7 = 850 adverts + BAND_8A = 397 -> 1,247 together).",
            "items": {
              "type": "string",
              "enum": [
                "BAND_2",
                "BAND_3",
                "BAND_4",
                "BAND_5",
                "BAND_6",
                "BAND_7",
                "BAND_8A",
                "BAND_8B",
                "BAND_8C",
                "BAND_8D",
                "BAND_9",
                "SPECIALTY_DOCTOR",
                "FOUNDATION_DOCTOR",
                "CONSULTANT",
                "SPECIALTY_REGISTRAR",
                "DOCTOR_OTHER",
                "SPECIALIST",
                "VERY_SENIOR_MANAGER"
              ],
              "enumTitles": [
                "Band 2",
                "Band 3",
                "Band 4",
                "Band 5",
                "Band 6",
                "Band 7",
                "Band 8a",
                "Band 8b",
                "Band 8c",
                "Band 8d",
                "Band 9",
                "Specialty doctor",
                "Foundation doctor",
                "Consultant",
                "Specialty registrar",
                "Doctor - other",
                "Specialist",
                "Very senior manager"
              ]
            }
          },
          "staffGroup": {
            "title": "Staff group",
            "type": "array",
            "description": "The NHS occupational staff groups. OR'd together (verified additive: 2,490 + 1,911 -> 4,401).",
            "items": {
              "type": "string",
              "enum": [
                "CLINICAL_SERVICES",
                "PROF_SCIENTIFIC_AND_TECHNICAL",
                "ADMINISTRATIVE_AND_CLERICAL",
                "ALLIED_HEALTH_PROF",
                "ESTATES_AND_ANCILLARY",
                "HEALTHCARE_SCIENTISTS",
                "MEDICAL_AND_DENTAL",
                "NURSING_AND_MIDWIFERY_REGD",
                "STUDENTS"
              ],
              "enumTitles": [
                "Additional clinical services",
                "Professional, scientific & technical",
                "Administrative & clerical",
                "Allied health professionals",
                "Estates & ancillary",
                "Healthcare scientists",
                "Medical & dental",
                "Nursing & midwifery (registered)",
                "Students"
              ]
            }
          },
          "contractType": {
            "title": "Contract type",
            "type": "array",
            "description": "The locum / bank / fixed-term lever a staffing agency actually wants. OR'd together (verified additive: Locum 187 + Bank 1,125 -> 1,312).",
            "items": {
              "type": "string",
              "enum": [
                "Permanent",
                "Fixed-Term",
                "Apprenticeship",
                "Voluntary",
                "Locum",
                "Training",
                "Secondment",
                "Honorary",
                "Bank",
                "NHS Reservist"
              ],
              "enumTitles": [
                "Permanent",
                "Fixed-term",
                "Apprenticeship",
                "Voluntary",
                "Locum",
                "Training",
                "Secondment",
                "Honorary",
                "Bank",
                "NHS Reservist"
              ]
            }
          },
          "workingPattern": {
            "title": "Working pattern",
            "type": "array",
            "description": "Full-time / part-time / remote and the flexible-working patterns. OR'd together.",
            "items": {
              "type": "string",
              "enum": [
                "full-time",
                "part-time",
                "job-share",
                "flexible-working",
                "remote-working",
                "compressed-hours",
                "term-time-hours",
                "annualised-hours"
              ],
              "enumTitles": [
                "Full time",
                "Part time",
                "Job share",
                "Flexible working",
                "Home or remote working",
                "Compressed hours",
                "Term-time hours",
                "Annualised hours"
              ]
            }
          },
          "payRange": {
            "title": "Pay range (£ thousands per year)",
            "type": "array",
            "description": "The site's banded salary filter, in thousands of GBP per year. OR'd together. Independent of the exact Min/Max salary fields below — use either.",
            "items": {
              "type": "string",
              "enum": [
                "0-10",
                "10-20",
                "20-30",
                "30-40",
                "40-50",
                "50-60",
                "60-70",
                "70-80",
                "80-90",
                "90-100",
                "100"
              ],
              "enumTitles": [
                "£0-10k",
                "£10-20k",
                "£20-30k",
                "£30-40k",
                "£40-50k",
                "£50-60k",
                "£60-70k",
                "£70-80k",
                "£80-90k",
                "£90-100k",
                "£100k+"
              ]
            }
          },
          "salaryFrom": {
            "title": "Min salary (£/year)",
            "minimum": 0,
            "type": "integer",
            "description": "Exact annual GBP floor, applied server-side (verified: salaryFrom=60000 -> 2,130 adverts)."
          },
          "salaryTo": {
            "title": "Max salary (£/year)",
            "minimum": 0,
            "type": "integer",
            "description": "Exact annual GBP ceiling, applied server-side (verified: salaryTo=20000 -> 232 adverts)."
          },
          "sort": {
            "title": "Sort order",
            "enum": [
              "",
              "publicationDateDesc",
              "closingDate",
              "salaryDesc",
              "salaryAsc"
            ],
            "type": "string",
            "description": "How jobs.nhs.uk orders the result set. 'Date posted (newest)' is what makes a daily delta crawl cheap — pair it with 'Posted within (days)'.",
            "default": ""
          },
          "postedWithinDays": {
            "title": "Posted within (days)",
            "minimum": 1,
            "maximum": 365,
            "type": "integer",
            "description": "Keep only adverts posted in the last N days. jobs.nhs.uk has no server-side date filter, so the Actor sorts newest-first and STOPS as soon as a whole page falls outside the window — that is what makes a daily delta run cheap (~1,000 new adverts land per day). Leave empty for no date limit."
          },
          "closingWithinDays": {
            "title": "Closing within (days)",
            "minimum": 1,
            "maximum": 365,
            "type": "integer",
            "description": "Keep only adverts whose closing date is within the next N days (already-closed adverts are dropped). Set Sort to 'Closing date' and the Actor also stops paging once it passes the window. Built for locum/agency fill-rate outreach on roles about to expire."
          },
          "covidJobsOnly": {
            "title": "COVID-19 adverts only",
            "type": "boolean",
            "description": "The site's legacy COVID-19 flag. Still functional but tiny (9 live adverts when last measured).",
            "default": false
          },
          "language": {
            "title": "Site language",
            "enum": [
              "en",
              "cy"
            ],
            "type": "string",
            "description": "jobs.nhs.uk serves a full Welsh UI. This switches the pages the Actor requests, so free-text the employer wrote in Welsh comes back in Welsh. It does NOT change WHICH adverts match — the corpus is identical (11,613 either way).",
            "default": "en"
          },
          "maxItems": {
            "title": "Max jobs per run",
            "minimum": 0,
            "type": "integer",
            "description": "Your primary cost control: a hard cap on adverts returned (and therefore billed) this run. 0 = no cap (the whole result set, up to 20,000). Defaults to 50 for a fast, cheap first run.",
            "default": 50
          },
          "maxPagesPerSearch": {
            "title": "Max search pages per search",
            "minimum": 0,
            "type": "integer",
            "description": "Secondary cap for wide or unfiltered crawls — 10 adverts per page. 0 = follow the result set to its end (verified lossless: keyword=nurse ends at page 495, which returns its final 2 rows).",
            "default": 0
          },
          "scrapeJobDetails": {
            "title": "Fetch job-advert details (recruiter contact, band, person spec)",
            "type": "boolean",
            "description": "One extra request per advert. Unlocks the recruiter contact block (name, job title, email, phone), Agenda-for-Change band and pay scheme, structured salary, employer address + website, the essential/desirable person specification, DBS and sponsorship flags, supporting documents and the apply link. Billed as the separate 'Job detail enriched' event, and never charged when the detail fetch fails.",
            "default": true
          },
          "includeContactDetails": {
            "title": "Include recruiter contact details (personal data)",
            "type": "boolean",
            "description": "Independent switch for the named-person fields — contact name, job title, email and direct phone. These are PERSONAL DATA under UK GDPR: employers publish them so candidates can ask about that specific vacancy. Turn this off if your use case does not need them. See the README's Legal section.",
            "default": true
          },
          "includeFullDescription": {
            "title": "Include the long description fields",
            "type": "boolean",
            "description": "Adds jobDescription (main duties), jobResponsibilities and aboutOrganisation. These run to several KB per row; off by default keeps datasets small and CSV exports readable. The short jobOverview (job summary) is always included when details are on.",
            "default": false
          },
          "includeCriteria": {
            "title": "Include the person specification",
            "type": "boolean",
            "description": "Adds the structured person spec: personSpecification (grouped by category) plus flat essentialCriteria / desirableCriteria arrays.",
            "default": true
          },
          "outputFormat": {
            "title": "Output shape",
            "enum": [
              "full",
              "lead",
              "minimal"
            ],
            "type": "string",
            "description": "'Full' = every field. 'Lead' = the contact-centric row a staffing or healthcare-vendor buyer imports straight into a CRM (employer, role, band, contact name/title/email/phone, closing date, website). 'Minimal' = the nine search-card fields only.",
            "default": "full"
          },
          "deduplicateByReference": {
            "title": "Deduplicate across searches",
            "type": "boolean",
            "description": "On (default): one row per advert reference for the whole run, so overlapping searches never bill you twice. Off: one row per (search, advert) pair with sourceSearchUrl set, so each pasted search gets its own copy — an advert matched by two searches is then delivered AND BILLED twice. Page overlap inside a single search is always collapsed either way.",
            "default": true
          },
          "proxyConfiguration": {
            "title": "Proxy",
            "type": "object",
            "description": "MEASURED: jobs.nhs.uk sits behind CloudFront, which 403s Apify datacenter IPs (60% first-try pass, 87% with retries). Apify Proxy RESIDENTIAL with country GB passes 100% (25/25 sustained, 50/50 at concurrency 20). Keep this default unless you are supplying your own UK residential proxy.",
            "default": {
              "useApifyProxy": true,
              "apifyProxyGroups": [
                "RESIDENTIAL"
              ],
              "apifyProxyCountry": "GB"
            }
          },
          "tryDatacenterFirst": {
            "title": "Try the cheap datacenter proxy first",
            "type": "boolean",
            "description": "Cost optimisation: attempt each request on a datacenter IP once and fall back to residential only when CloudFront blocks it. Roughly 60% of requests are then served without spending residential bandwidth. Automatically skipped when you supply your own proxy configuration. Turn off for the lowest possible latency.",
            "default": true
          },
          "maxConcurrency": {
            "title": "Max concurrent requests",
            "minimum": 1,
            "maximum": 20,
            "type": "integer",
            "description": "Parallel requests for search pages and advert pages. Measured clean at 20 (50/50 successes at 5/10/15/20 parallel); 10 is the polite default.",
            "default": 10
          },
          "maxRequestRetries": {
            "title": "Max retries per request",
            "minimum": 0,
            "maximum": 10,
            "type": "integer",
            "description": "Residential retries after a block or transport flake. Measured need: ~1% of residential calls flake once and every one recovered on a single retry.",
            "default": 4
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}