# jobs.ac.uk Academic Jobs Scraper - UK University Jobs (`scrapersdelight/jobs-ac-uk-academic-jobs-scraper`) Actor

Scrape every live jobs.ac.uk vacancy - UK and overseas university, research and professional-services roles - with the employer's own ATS apply link, parsed salary, contact email, department and closing date. Filter by discipline, location, contract, salary band. New-vacancy monitor, no start fee.

- **URL**: https://apify.com/scrapersdelight/jobs-ac-uk-academic-jobs-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.30 / 1,000 per vacancy returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 🎓 jobs.ac.uk Academic Jobs Scraper — every UK university vacancy, as structured rows

Turn **jobs.ac.uk** — the board UK higher education actually recruits on — into clean JSON / CSV /
Excel rows: job title, institution, department, location, salary range, hours, contract type,
closing date, the named **contact email**, and the one field that matters most to a recruiter or a
supplier — **the employer's own application URL**, the deep link straight into their ATS.

No login. No API key. No CAPTCHA. Datacenter proxies are enough, and there is **no per-run start
fee** — you pay for rows, and nothing else.

***

### 📊 What you are scraping (measured live 2026-09-04)

| | |
|---|---:|
| Live vacancies on the board | **2,240** |
| New vacancies per weekday (counted off Date Placed) | **~135** |
| Fri 04 Sep · Thu 03 Sep · Wed 02 Sep · Tue 01 Sep | 141 · 142 · 130 · 131 |
| Mon 31 Aug (UK summer bank holiday) · Sun 30 Aug | 3 · 1 |
| United Kingdom | 1,984 |
| Overseas posts carried on the board | 256 (China 68, Ireland 41, Denmark 25, New Zealand 20, Hong Kong 18, Macao 17 …) |
| Hiring universities / HEIs | 2,106 of 2,240 adverts |
| Research organisations · public sector · commercial · FE · charity | 44 · 26 · 25 · 24 · 15 |

That is roughly **2,700 new adverts a month**, which is what makes this a monitoring feed rather
than a one-off download.

***

### 🚀 What does jobs.ac.uk Academic Jobs Scraper do?

- 🔎 **Searches exactly the way the site does.** Keywords plus all twelve of the site's own facets —
  academic discipline (21 values), sub-discipline (89), professional-services discipline (20),
  studentship type / qualification / funding, location (hierarchical: continent → country → region →
  city), workplace, salary band, hours, contract type and employer sector. Every one of them was
  verified against the live board to genuinely narrow the result set, not just relabel it.
- 🔗 **Returns the employer's own apply URL.** `applyUrl` is the link out of jobs.ac.uk and into the
  institution's own recruitment system — `jobs.bradford.ac.uk/HR0203329-2`,
  `manmetjobs.mmu.ac.uk/jobs/vacancy/9313/…`. **100% fill** on standard adverts across 60 measured
  pages, spread over 39 distinct ATS domains. That is the field a staffing agency, an HR-tech SDR or
  an academic-services supplier actually buys on.
- 📧 **Harvests the named contact.** `contactEmails` picks up the hiring manager or department
  administrator the advert names (53% of adverts carry at least one). `onlyWithContactEmail` turns
  that into a hard lead-quality gate.
- 💷 **Parses the salary.** `salaryMin` / `salaryMax` / `salaryCurrency` / `salaryPeriod` from the
  advert's schema.org block — 85% of standard adverts publish a numeric range — plus the raw
  `salaryText` for the 15% that say "Competitive" or attach a grade note.
- 🛡️ **Survives the site's random JavaScript interstitial.** jobs.ac.uk answers a slice of requests
  with a ~2.8 KB packed-JS page **at HTTP 200** instead of the real one. A naive scraper pushes
  short or empty pages and still reports SUCCESS. This actor classifies every response and
  re-requests on a **brand-new proxy session**. Measured on a 600-row crawl: 22 interstitials in
  108 requests, **0 lost pages, 0 lost rows**.
- 🧾 **Never double-bills.** Rows are de-duplicated on the site's own job ID across every search
  term, facet and page **before** anything is charged. Verified on a 600-row contiguous crawl:
  600 rows, 600 unique job IDs, 600 unique advert IDs, 600 unique URLs, 0 duplicates.
- 📡 **Monitors for free.** Monitor mode remembers every job ID it has seen in a NAMED key-value
  store and returns only what is new, with webhook / Slack / email alerts. There is **no
  monitor-run charge and no per-alert charge** on this actor — you pay only for the new rows.
- 🧰 **Filters before it bills.** Title and employer allow/deny lists, salary floor and ceiling,
  posted-within / posted-after / closing-before windows, UK-only or overseas-only, contact-email
  required. A row you filter out is **never charged**.
- 📋 **Paste any jobs.ac.uk search URL.** Build the search in the browser, paste the address bar,
  and every parameter is honoured verbatim — including facet values this schema does not list.

***

### 📦 Output — measured fill rates

**Search-row fields**, measured on a **600-row** contiguous crawl of the whole live board
(2026-09-04), 600 of 600 unique:

| Field | Fill |
|---|---:|
| `jobId` *(the site's own advert code, the dedupe key)* | **100%** |
| `advertId` *(numeric row id, 1:1 with `jobId`)* | **100%** |
| `url` · `title` · `employer` | **100%** |
| `location` · `salaryText` | **100%** |
| `datePlaced` ("04 Sep") · `datePosted` (ISO) | **100%** |
| `closingDate` ("18 Sep") · `validThrough` (ISO) | **100%** |
| `highlighted` *(paid-promotion flag — 73 of 600 rows)* | **100%** |
| `department` | 82% |

**Advert-page fields** (turn on *Scrape job details*), measured on **60 standard advert pages**
from the same crawl:

| Field | Fill |
|---|---:|
| `applyUrl` *(deep link into the employer's own ATS)* · `applyDomain` | **100%** |
| `datePosted` · `validThrough` (ISO 8601) | **100%** |
| `employmentType` · `hours` · `placedOn` · `closes` | **100%** |
| `city` · `country` · `employer` · `employerLogo` | **100%** |
| `descriptionText` / `descriptionHtml` | **100%** |
| `contractType` | 98% |
| `region` | 95% |
| `department` | 85% |
| `salaryMin` · `salaryMax` · `salaryCurrency` · `salaryPeriod` | 85% |
| `employerWebsite` *(the institution's own domain)* | 78% |
| `employerJobRef` *(the institution's internal ATS reference)* | 77% |
| `contactEmails` / `contactEmail` | 53% |
| `advertTable` *(the raw label→value table, incl. studentship rows: Qualification Type, Funding for, Funding amount)* | 100% |

Plus `listingType`, `detailFetched`, `isNew`, `firstSeenAt`, `searchLabel`, `scrapedAt`, and the
untouched schema.org `rawJsonLd` object on request.

#### Sample row

```json
{
  "jobId": "DSV280",
  "advertId": "1086981",
  "url": "https://www.jobs.ac.uk/job/DSV280/early-years-practitioner",
  "title": "Early Years Practitioner",
  "employer": "University of Bradford",
  "department": "Professional Services - Directorate of People and Culture",
  "location": "Bradford",
  "city": "Bradford", "region": "England", "country": "United Kingdom",
  "salaryText": "£25,354 per annum till 19/04/2027",
  "salaryMin": 25354, "salaryMax": 25354,
  "salaryCurrency": "GBP", "salaryPeriod": "YEAR",
  "hours": "Full Time",
  "contractType": "Fixed-Term/Contract",
  "employmentType": "Full Time,Fixed-Term/Contract",
  "datePlaced": "04 Sep", "datePosted": "2026-09-04",
  "closingDate": "18 Sep", "validThrough": "2026-09-18",
  "employerJobRef": "HR0203329-2",
  "employerWebsite": "https://www.bradford.ac.uk/external/",
  "applyUrl": "https://jobs.bradford.ac.uk/HR0203329-2",
  "applyDomain": "jobs.bradford.ac.uk",
  "listingType": "standard"
}
```

***

### 💷 Pricing — pay per event, no start fee

| Event | Price | When it fires |
|---|---:|---|
| **Per vacancy returned** (`job-scraped`) | **$0.0013** | Once per unique vacancy delivered to your dataset. |
| **Per advert page enriched** (`job-detail-enriched`) | **$0.0015** | Only when *Scrape job details* is on, and only for adverts that actually returned structured data. |
| Actor start | **$0.00** | Removed. Every rival in this lane charges one. |

- The whole live board (2,240 vacancies) costs about **$2.91**, or **$6.27** with full advert-page
  enrichment.
- A daily new-vacancy monitor bills about **$0.18/day** (~135 new adverts) — **$5.30 a month** —
  because monitoring, alerting and de-duplication are free.
- Rows dropped by your own filters, duplicate rows, and the ~4% of adverts that are employer
  campaign microsites (no structured data) are **never charged the enrichment fee**.

***

### ⚙️ How this actor reads the site (and the traps it is built around)

Everything below was measured against real bytes on 2026-09-04, not read off documentation.

1. **The list surface is server-rendered HTML.** `GET /search/?…&sortOrder=1&pageSize=25&startIndex=N`.
   No JSON endpoint exists. Rows live at `div.j-search-result__result`; the corpus size at
   `strong.job-count`.
2. **`pageSize` is fixed at 25.** `pageSize=100` returns a byte-identical 25-row page. Page counts
   therefore always move in steps of 25.
3. **Paging is complete — there is no depth ceiling.** `startIndex=2101` → 25 rows,
   `startIndex=2226` → 15, `startIndex=2251` → 0. The whole 2,240-row corpus is reachable.
4. **An empty page past the end is NORMAL, not a failure.** It still carries the job count — which
   is exactly how this actor tells "end of results" apart from the interstitial.
5. **The random interstitial.** A slice of requests answers **HTTP 200** with a packed-JS page
   (`eval(function(p,a,c,k,e,d)…`) instead of the real one. It is session-scoped and random, not a
   per-URL wall, and it is not an anti-abuse control being defeated: the actor simply discards the
   response and asks again on a fresh proxy session, and the site serves normally. Measured rates:
   a light crawl was 100% clean over 37 requests; a heavy 600-row crawl saw 22 interstitials in 108
   requests and recovered **all** of them. Leave *Max retries* at 4 or more.
6. **Salary and dates are read structurally, never by a page-wide regex.** "Date Placed" and the
   salary string each appear 25 times on a results page; a whole-page regex grabs the wrong one.
7. **The free-text `location=` / `distance=` parameters do not work.**
   `location=London&distance=20` returned the full unfiltered 2,240. Only `locationFacet[]` filters.
   This actor never sends the free-text pair.
8. **Advert pages are fetched with zero redirects** — measured across 110 detail fetches — so the
   crawler never lands on the robots-Disallowed `/enhanced/fp/` path.
9. **Three advert page classes.** *Standard* (48 of 50 sampled) carries the schema.org `JobPosting`
   block and the advert table. *Enhanced* (2 of 50, ~4%) is an employer campaign microsite with no
   structured block and no standard version of the same advert — the actor still salvages its title,
   apply link, emails and ad copy, marks it `listingType: "enhanced"`, and **never charges the
   enrichment fee for it**. *Challenge* is the interstitial, and is retried.
10. **The description comes from the structured block, not the page node.** The on-page
    `#job-description` container also holds the apply-button markup.
11. **If the site's markup changes, the run fails loudly and charges nothing.** A results page that
    reports a job count but yields zero parsed rows triggers `Actor.fail()` — a silently empty
    dataset is the one failure mode a "green" run can otherwise hide.

***

### ❓ FAQ

**Does this need a login, an API key or a CAPTCHA solver?**
No. Both surfaces are public server-rendered pages. Nothing is logged into, no token is minted, no
anti-abuse control is bypassed.

**Are Apify datacenter proxies enough?**
Yes — that is the default, and it is what every measurement in this README was taken on. The
residential pool with country GB is the documented fallback if you ever see persistent failures.

**Why do I sometimes see "interstitial retried on a fresh session" in the log?**
That is the actor doing its job. jobs.ac.uk randomly serves a JavaScript holding page at HTTP 200;
the actor detects it and re-requests. You never see a short page or a missing row.

**Can I filter by date?**
Yes, but on this side of the wire — jobs.ac.uk exposes **no date query parameter at all**. Use
*Posted within (days)*, *Posted after/before* and *Closing after/before*, and keep the default
"Date placed" sort so the newest adverts come first. Rows filtered out are never billed.

**How do I get a daily feed of new vacancies?**
Turn on **Monitor mode**, give the run a **Monitor state store name** of its own, and schedule it.
Every run returns only job IDs it has not seen before. Monitoring is not billed.

**What is the difference between `salaryText` and `salaryMin`/`salaryMax`?**
`salaryText` is exactly what the advert says ("£38,784 to £46,049 Grade 7, per annum") and is 100%
filled. The numeric pair comes from the advert's structured block and is filled on 85% of standard
adverts — the rest genuinely do not publish a number.

**Why is `department` only 82% filled?**
Because 18% of adverts do not state one. It is not a parsing gap: the field is simply absent from
the search row and from the structured block on those adverts.

**Does it cover PhD and Masters studentships?**
Yes. Use *Studentship type* (PhDs 98 live, Masters 9), *Qualification type* and *Studentship
funding*. Studentship adverts also carry `qualificationType`, `fundingFor` and `fundingAmount`.

**Does it cover jobs outside the UK?**
Yes — 256 of the 2,240 live adverts were overseas posts on 2026-09-04. Filter with the *Location*
facet (server-side, cheapest) or *Country* (needs advert-page scraping).

**Will overlapping searches charge me twice?**
No. De-duplication happens on the job ID **before** billing, across every search term, facet and
page. Choose Advert ID or Employer+title instead if you prefer.

**What happens if I mistype a facet value?**
The run stops with a clear error, returns zero rows and charges nothing. jobs.ac.uk answers an
unknown facet value with zero results, and running the search unfiltered instead would hand you
(and bill you for) the whole board.

**What happens on a search that legitimately matches nothing?**
The run finishes SUCCEEDED with an empty dataset and a status message saying why. It never fails
for having found nothing.

**Can I limit what I spend?**
Yes, three ways: *Max vacancies to return* (a hard row cap, default 50), *Max advert pages to
fetch*, and Apify's own per-run maximum charge — rows are pushed and billed atomically, so hitting
the cap can never leave you paying for rows you did not receive.

***

### ⚖️ Legal and fair use

`https://www.jobs.ac.uk/robots.txt` (read 2026-09-04) is a single open group:

```
User-agent: *

Disallow: /job/feedback/
Disallow: /enhanced/fp/

Sitemap: https://www.jobs.ac.uk/sitemapindex.xml
```

This actor requests only `/search/` and `/job/<ID>/<slug>` — both explicitly crawlable — and the
site publishes `sitemapindex.xml`. Advert pages are fetched with redirects measured at zero, so the
crawler never lands on `/enhanced/fp/`. There is no login, no CAPTCHA, no signature forgery and no
anti-abuse control involved anywhere in this actor.

**Personal data is your responsibility.** `contactEmails` are the addresses of named individuals
published in a job advert. Using them — in particular for direct marketing — makes you the data
controller under the UK GDPR and PECR. Have a lawful basis, honour opt-outs, and do not process
them for a purpose the advert did not contemplate. This actor gives you a switch
(*Extract contact emails*) to leave them out entirely.

You are responsible for complying with jobs.ac.uk's terms of use and for how you use the data.
Vacancy text is the copyright of the advertising institution; scrape the facts, not their prose,
if you intend to republish.

# Actor input Schema

## `startUrls` (type: `array`):

Build the search you want on jobs.ac.uk, then paste the address bar here. Every filter on the URL is honoured verbatim — including facet values this schema does not list, and any new one the site adds. A <code>/job/\<ID>/\<slug></code> link is treated as a single advert. This is the highest-leverage input: what you see in the browser is what you get in the dataset.

## `searchTerms` (type: `array`):

Free-text keywords, exactly as you would type them into the jobs.ac.uk search box (<code>keywords=</code>). Each term is crawled as its own search and the results are merged and de-duplicated on the job ID before anything is billed. Measured 2026-09-04: "lecturer" 399 vacancies, "machine learning" 129.

## `jobUrls` (type: `array`):

Full <code>https://www.jobs.ac.uk/job/\<ID>/\<slug></code> links. Each one is fetched as a single advert and enriched from its schema.org JobPosting block — use this to re-check or top up adverts you already have. Billed as one row each, plus the enrichment event.

## `academicDiscipline` (type: `array`):

The site's top-level academic subject facet (<code>academicDisciplineFacet\[]</code>). Several values are a union, not an intersection — measured: Computer Sciences 270 + Economics 54 returns 318 together, because 6 adverts sit in both.

## `subDiscipline` (type: `array`):

The narrower subject facet beneath the academic disciplines (<code>subDisciplineFacet\[]</code>) — Artificial Intelligence, Cyber Security, Software Engineering, Nursing, Medicine & Dentistry and 84 more. Measured 2026-09-04: artificial-intelligence 158 vacancies.

## `nonAcademicDiscipline` (type: `array`):

The professional / non-academic side of a university's hiring (<code>nonAcademicDisciplineFacet\[]</code>): IT Services, Estates & Facilities, Finance & Procurement, Fundraising & Alumni, Senior Management and more. This is where the university's own supplier-facing buyers sit.

## `jobType` (type: `array`):

Restrict to studentships rather than jobs (<code>jobTypeFacet\[]</code>). Measured 2026-09-04: PhDs 98, Masters 9.

## `qualificationType` (type: `array`):

Studentship qualification (<code>qualificationTypeFacet\[]</code>). Only meaningful alongside a studentship type. Measured 2026-09-04: PhD 89, Integrated Masters/Doctorate 9, International Doctorate 0, Professional Doctorate 0.

## `fundingType` (type: `array`):

Who the studentship funding is open to (<code>fundingTypeFacet\[]</code>). Measured 2026-09-04: UK students 92, EU students 35, international 32, self-funded 11.

## `location` (type: `array`):

The site's hierarchical location facet (<code>locationFacet\[]</code>) — continent, country, region/county, then city. Pick the level you want; picking two is a union. The list below is the live vocabulary read off the site on 2026-09-04, trimmed to the levels that persist (continent/country/region, plus every place with 4+ live vacancies). For anything finer, paste a search URL instead. <b>Note:</b> the free-text <code>location=</code> and <code>distance=</code> parameters do NOT work on this site — measured, <code>location=London\&distance=20</code> returns the full unfiltered corpus. Only this facet filters.

## `workplace` (type: `array`):

On-site / hybrid / remote (<code>workplaceFacet\[]</code>). Measured 2026-09-04: on-site 1,953, hybrid 278, remote 21.

## `salaryBand` (type: `array`):

The site's own salary bands (<code>salaryBandFacet\[]</code>), applied server-side. Faster and cheaper than filtering after the fact — but note it uses the advertised band, so adverts with no stated salary fall out. For a numeric filter on the parsed range, use Min/Max salary below.

## `hoursType` (type: `array`):

Full time / part time (<code>hoursTypeFacet\[]</code>). Measured 2026-09-04: full time 1,952, part time 376.

## `contractType` (type: `array`):

Permanent / fixed-term / temporary (<code>contractTypeFacet\[]</code>). Measured 2026-09-04: fixed-term 1,184, permanent 1,094, temporary 10.

## `employerSector` (type: `array`):

What kind of organisation is hiring (<code>employerSectorFacet\[]</code>). Universities dominate (2,106 of 2,240 on 2026-09-04) — the other five values are the interesting ones if you sell to research institutes, FE colleges or academic charities.

## `sortOrder` (type: `string`):

How jobs.ac.uk orders the results. <b>Date placed</b> (the default) is a stable newest-first ordering, which is what makes contiguous paging lossless and is the right choice for monitoring.

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

Hard cap on billed rows across the whole run — your spend guard rail. Set 0 for unlimited. The whole live board was 2,240 vacancies on 2026-09-04, so 0 costs about $2.90 at the per-row rate.

## `maxResultsPerSearch` (type: `integer`):

Per-search cap, so one broad keyword cannot eat the whole run budget. 0 = no per-search cap.

## `maxPagesPerSearch` (type: `integer`):

25 vacancies per page, fixed by the site (<code>pageSize=100</code> returns a byte-identical 25-row page — verified). 0 = no page cap.

## `startIndex` (type: `integer`):

Resume or offset a crawl. The site's own <code>startIndex</code> — 1 is the first result, 26 the second page, and so on. Paging is complete to the end of the corpus: startIndex 2,101 returned 25 rows and 2,251 returned 0 with the job count still present, so there is no hidden depth ceiling.

## `postedWithinDays` (type: `integer`):

Keep only adverts placed in the last N days. <b>jobs.ac.uk has no date query parameter</b>, so this is applied by this actor after reading each row — it saves you billing, not requests. Combine with the "Date placed" sort so the newest adverts come first. 0 = no date filter.

## `postedAfter` (type: `string`):

ISO date, e.g. <code>2026-08-01</code>. Client-side.

## `postedBefore` (type: `string`):

ISO date. Client-side.

## `closingAfter` (type: `string`):

ISO date. Client-side. Use it to drop adverts that are about to close.

## `closingBefore` (type: `string`):

ISO date. Client-side. The "closing soon" prospecting filter — pair it with today's date + 14 days to find the roles a recruiter still has time to influence.

## `onlyNewSince` (type: `string`):

ISO date. Keeps only adverts posted on or after this date AND not already recorded in the monitor state store. Set this instead of Monitor mode when you want a one-off catch-up rather than a running diff.

## `scrapeJobDetails` (type: `boolean`):

Fetch each advert page for its schema.org JobPosting block, the advert table and — the one field a recruiter actually pays for — <b>the employer's own application URL</b>, the deep link into their ATS. Also unlocks the parsed salary range, ISO dates, the employer's website, the institution's internal job reference, the full description and any contact email. One extra request per vacancy, billed as a second event.

## `maxDetailPages` (type: `integer`):

Cap on enrichment requests so the second event cannot outrun your budget. 0 = enrich every returned vacancy.

## `extractContactEmails` (type: `boolean`):

Harvest <code>mailto:</code> addresses from the advert — usually the named hiring manager or department administrator. Measured fill: 16 of 25 adverts carried at least one. These are named individuals: handling them is your responsibility under UK GDPR.

## `includeDescriptionText` (type: `boolean`):

The full advert body rendered to plain text. Taken from the schema.org block, not the page node — the on-page description container also holds the apply-button markup.

## `includeDescriptionHtml` (type: `boolean`):

The advert body as the employer wrote it, with its original markup.

## `includeRawJsonLd` (type: `boolean`):

Attach the untouched schema.org <code>JobPosting</code> JSON under <code>rawJsonLd</code>, for any field this actor does not map.

## `minSalary` (type: `integer`):

Numeric filter on the parsed salary range from the advert page (needs "Scrape job details"). An advert whose top of range is below this is dropped. Adverts with no parsed salary are kept unless "Only vacancies with a salary range" is on. 0 = off.

## `maxSalary` (type: `integer`):

Numeric filter on the parsed salary range (needs "Scrape job details"). 0 = off.

## `onlyWithSalaryRange` (type: `boolean`):

Drop "Competitive" / "Not specified" adverts. Needs "Scrape job details" — the numeric range only exists in the advert page's structured block. Measured fill on standard adverts: 17 of 25 carried a min and max.

## `onlyWithContactEmail` (type: `boolean`):

The lead-generation quality gate: keep only adverts that expose a named contact address. Needs "Scrape job details" and "Extract contact emails". Measured: this keeps roughly 6 in 10 adverts.

## `countryFilter` (type: `string`):

jobs.ac.uk carries overseas academic posts as well as UK ones (measured 2026-09-04: 1,984 UK of 2,240, plus China 68, Ireland 41, Denmark 25, New Zealand 20 and more). Reads the advert page's country, so it needs "Scrape job details" — to filter server-side instead, use the Location facet.

## `employerInclude` (type: `array`):

Keep only adverts whose employer name contains one of these (case-insensitive substring), e.g. <code>Russell</code>, <code>University of Manchester</code>. Applied on the search row, so it costs you nothing.

## `employerExclude` (type: `array`):

Drop adverts whose employer name contains one of these. Useful for excluding an agency or a client you already cover.

## `titleIncludeKeywords` (type: `array`):

Case-insensitive substring filter on the job title, applied on the search row before anything is billed. Sharper than the site's keyword search, which also matches the advert body.

## `titleExcludeKeywords` (type: `array`):

Drop titles containing any of these, e.g. <code>PhD</code>, <code>Studentship</code>, <code>Casual</code>.

## `excludeEnhancedListings` (type: `boolean`):

About 4% of adverts are an employer-branded campaign page with no structured data block and no standard version. They still yield a title, employer, apply link and emails, and are never charged the enrichment fee — turn this on if you only want rows with the full structured field set. Needs "Scrape job details" to be detectable.

## `excludeHighlighted` (type: `boolean`):

Drop the paid-promotion rows the site highlights (7 of 25 on the first page measured 2026-09-04). They are ordinary vacancies — this is only here if you are analysing organic posting behaviour.

## `deduplicateBy` (type: `string`):

What counts as the same vacancy. <b>Job ID</b> is the site's own stable advert code from the URL and is the right answer for almost everyone — verified unique and lossless over a 300-row contiguous crawl (300 rows, 300 unique, 0 duplicates). Advert ID is the numeric row attribute, 1:1 with the job ID. Employer + title collapses the same role re-advertised under a new ID.

## `fields` (type: `array`):

Return only these columns, in this order — e.g. <code>title, employer, location, salaryText, applyUrl, contactEmail, url</code>. Leave empty for every field. The job ID is always kept, because it is the de-duplication key.

## `monitorMode` (type: `boolean`):

Remember every job ID seen in previous runs and return ONLY the ones that are new. The state lives in a NAMED key-value store so it survives between scheduled runs. Monitoring itself is free — there is no per-run or per-alert charge on this actor; you pay only for the new rows it hands you. jobs.ac.uk adds roughly 124 vacancies per weekday, so a daily monitor bills around $0.16 a day at the per-row rate.

## `stateKeyValueStoreName` (type: `string`):

Named key-value store holding the seen-job-ID state. Give each independent saved search its own name so they do not shadow each other.

## `notificationWebhookUrl` (type: `string`):

POSTed a JSON payload with the new vacancies whenever a run finds any. Not billed.

## `slackWebhookUrl` (type: `string`):

Posts a formatted card per new vacancy to a Slack channel. Not billed.

## `notificationEmail` (type: `string`):

Sends a digest of the new vacancies through Apify's own mail actor. Not billed by this actor.

## `proxyConfiguration` (type: `object`):

Apify <b>datacenter</b> proxies are enough and are the default — measured over 180+ calls with no CAPTCHA and no rate limiting. The site does randomly serve a JavaScript interstitial instead of the page (about 5% of requests); this actor detects it and re-requests on a fresh proxy session, which cleared every one of them. If you ever see persistent failures, switch to RESIDENTIAL with country GB.

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

Kept deliberately low — this is a small specialist job board, not a hyperscaler.

## `maxRequestRetries` (type: `integer`):

Each retry uses a BRAND-NEW proxy session, which is what clears the interstitial. Leave at 4 or more: the interstitial arrives as an HTTP 200, so a build that does not retry it silently returns short pages.

## `minDelayBetweenRequestsMs` (type: `integer`):

Politeness throttle. 0 = no artificial delay.

## `maxRunTimeSecs` (type: `integer`):

Stop cleanly after this many seconds and push whatever was collected. 0 = only the platform run timeout applies.

## Actor input object example

```json
{
  "searchTerms": [
    "research fellow"
  ],
  "academicDiscipline": [],
  "subDiscipline": [],
  "nonAcademicDiscipline": [],
  "jobType": [],
  "qualificationType": [],
  "fundingType": [],
  "location": [],
  "workplace": [],
  "salaryBand": [],
  "hoursType": [],
  "contractType": [],
  "employerSector": [],
  "sortOrder": "datePlaced",
  "maxResults": 50,
  "maxResultsPerSearch": 0,
  "maxPagesPerSearch": 0,
  "startIndex": 1,
  "postedWithinDays": 0,
  "scrapeJobDetails": false,
  "maxDetailPages": 0,
  "extractContactEmails": true,
  "includeDescriptionText": true,
  "includeDescriptionHtml": false,
  "includeRawJsonLd": false,
  "minSalary": 0,
  "maxSalary": 0,
  "onlyWithSalaryRange": false,
  "onlyWithContactEmail": false,
  "countryFilter": "any",
  "excludeEnhancedListings": false,
  "excludeHighlighted": false,
  "deduplicateBy": "jobId",
  "monitorMode": false,
  "stateKeyValueStoreName": "jobs-ac-uk-monitor-state",
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 3,
  "maxRequestRetries": 4,
  "minDelayBetweenRequestsMs": 0,
  "maxRunTimeSecs": 0
}
```

# Actor output Schema

## `items` (type: `string`):

The dataset of scraped jobs.ac.uk vacancies (one vacancy per row).

# 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 = {
    "searchTerms": [
        "research fellow"
    ],
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/jobs-ac-uk-academic-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 = {
    "searchTerms": ["research fellow"],
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/jobs-ac-uk-academic-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 '{
  "searchTerms": [
    "research fellow"
  ],
  "maxResults": 50
}' |
apify call scrapersdelight/jobs-ac-uk-academic-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/jobs-ac-uk-academic-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/fcEDK1SXsaOMmggeC/builds/swY8dreE0UockvRgx/openapi.json
