# Totaljobs Scraper - UK Jobs, Salaries & Employers (`abotapi/totaljobs-com-scraper`) Actor

Scrape UK job listings from Totaljobs.com by keyword, location, filters, or URL. Extract 40+ fields including title, parsed salary range and period, employer, logo, GPS location, skills, employment type, full description, posting date, and apply link.

- **URL**: https://apify.com/abotapi/totaljobs-com-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 listing results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Totaljobs.com Scraper: UK Job Listings, Parsed Salaries, and Full Job Details

Totaljobs.com Scraper turns Totaljobs.com into a clean UK jobs data feed. Search by keyword, location, job type, salary, and posting age, or paste any Totaljobs.com search link and let it paginate forward on its own. Every job comes back with 35+ structured fields straight from the listing page, and an optional detail pass adds the full description, GPS coordinates, a structured address, employment type, a valid-through date, and a company profile. Export to JSON, CSV, or Excel, pull it through the API, or run it on a schedule and get only new and changed jobs back.

### Why This Scraper?

- **35+ fields per job from the listing page alone**, no detail fetch required: title, parsed salary, employer, logo, location, posting source, and more.
- **Two ways to run.** Build a search from keyword, location, and filters, or paste any Totaljobs.com search link and walk it forward.
- **Salary parsed for you.** The free-text salary string on each listing is split into minimum, maximum, currency, and period (year, day, or hour), covering the daily and hourly rates common in UK job ads.
- **Real server-side filters.** Job type, advertiser type (direct employer vs. agency), minimum salary by period, posted-within window, and sort order, all sent to the site itself rather than filtered after the fact.
- **Optional full detail pass.** GPS coordinates, a structured address, the full description, employment type, a valid-through date, an apply type, a company profile, and any recruiter phone number or email visible on the listing itself.
- **One clear cap.** `maxListings` is the single limit on how many jobs a run collects; leave `maxPages` unset to walk a search until Max jobs is reached, the site's last page, or the built-in ceiling of 1000 pages (25,000 jobs) per search or link, whichever comes first.
- **Resumable and repeatable.** Continue an interrupted run with `resumeFromRunId`, or turn on incremental mode to run the same search on a schedule and receive only new, updated, or reappeared jobs, with unchanged jobs suppressed and not billed.

### Use Cases

- **Recruitment agencies and job boards:** aggregate live postings from Totaljobs.com to cross-post, compare, or fill gaps in your own listings.
- **Salary benchmarking and workforce research:** build a dataset of parsed salary ranges by role, location, and job type across a whole market.
- **Market and competitor monitoring:** track which employers are hiring, for which roles, and how listings change over time with incremental mode.
- **Recruitment and HR-tech lead generation:** identify direct employers vs. agencies posting in a sector, with company profiles when the detail pass is on.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `jobId` | 100234567 |
| `title` | Warehouse Operative |
| `jobUrl` | `"https://www.totaljobs.com/job/warehouse-operative/sample-employer-job100234567"` |
| `applyUrl` | `"https://www.totaljobs.com/job/warehouse-operative/sample-employer-job100234567"` |
| `brand` | totaljobs |
| `sourceSite` | Totaljobs.com |
| `datePosted` | 2026-03-14T00:00:00.000Z |
| `employer.name` | Sample Employer |
| `employer.logoUrl` | `"https://www.totaljobs.com/CompanyLogos/00000000000000000000000000000000.png"` |
| `location.text` | Slough, Berkshire, SL1 |
| `location.postalCode` | ZZ1 1ZZ |
| `salary.rawText` | £24,000 - £27,500 per annum |
| `salary.min` / `salary.max` | 24000 / 27500 |
| `salary.currency` | GBP |
| `salary.period` | year |
| `textSnippet` | Join our busy distribution centre team on a full time basis, no experience necessary. |
| `isSponsored` | false |
| `skills` | \[] (returned for every job; populated only when the listing itself includes skill tags) |
| `description` (detail) | Full HTML job description appears here when Fetch full job details is enabled. |
| `employmentType` (detail) | \["FULL_TIME"] |
| `industry` (detail) | Logistics |
| `validThrough` (detail) | 2026-04-14T00:00:00.000Z |
| `applyType` (detail) | internal |
| `location.latitude` / `location.longitude` (detail) | 51.5100 / -0.5900 |
| `contactPhones` / `contactEmails` (detail) | \["0000000000"] / \["careers@sample-employer.example"] |

Every record also carries a numeric employer id and company link, a `crossPostedCount` of how many other employers currently have a duplicate of the same ad, a handful of ad-placement flags (`isHighlighted`, `isTopJob`, `isTrafficFromPartner`) set by the site's own ranking rather than by the employer, and internal bookkeeping fields (`scrapedAt`, `detailFetched`, `sourceSearchUrl`). The detail pass additionally fills a structured address (`locality`, `region`, `country`, `streetAddress`), the working arrangement (`jobLocationType`, `contractType`, `workType`), and a company profile object with benefits, size, founding year, and job count when the employer has published one. When incremental mode is on, every record also carries `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`, and `previousSeenAt` (the prior sighting, absent on `NEW` rows) (see How to Use below); an `EXPIRED` row repeats the last known shape of a job that is no longer found and adds `expiredAt`, the time this run found it gone.

### How to Use

1. Pick a **mode**: `search` (build a search from filters) or `url` (paste Totaljobs.com search links).
2. For search mode, fill in locations, keywords, and any filters. For URL mode, paste one or more search-result links.
3. Turn on **Fetch full job details** if you need the full description, GPS, or company profile.
4. Set **Max jobs** to control run size and cost, then click **Start**.

**Search by keyword:**

```json
{
  "mode": "search",
  "locations": ["London"],
  "keywords": "nurse",
  "maxListings": 20,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

**Filtered search:**

```json
{
  "mode": "search",
  "locations": ["London"],
  "keywords": "nurse",
  "jobType": "permanent",
  "minSalary": 30000,
  "salaryType": "annual",
  "postedWithin": "7",
  "sortBy": "date",
  "maxListings": 100,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

**Full details:**

```json
{
  "mode": "search",
  "locations": ["London"],
  "keywords": "nurse",
  "fetchDetails": true,
  "maxListings": 50,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

Paginate pasted search links, or add single job pages (filter fields above are ignored in URL mode). Note: URL mode with no edits to the links now scrapes one example of each supported link shape, so clear the list first if you only want your own links. Single job-detail links are always enriched with the job's own page and bill the detail enrichment event for each job page fetched. A job-detail link whose page cannot be loaded is skipped (a warning is logged) and is not returned or billed, and a job pasted twice, or also present in a pasted search link, is returned once.

**Paste links:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.totaljobs.com/jobs/in-london",
    "https://www.totaljobs.com/jobs/in-manchester"
  ],
  "maxPages": 5,
  "maxListings": 200,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/totaljobs-com-scraper").call(run_input={"mode": "search", "locations": ["London"], "keywords": "nurse"})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["title"], job["salary"]["rawText"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/totaljobs-com-scraper').call({ mode: 'search', locations: ['London'], keywords: 'nurse' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets, or webhooks from the **Integrations** tab.

#### Caps: Max jobs and Max pages

`maxListings` is the one limit that matters for cost: it is the total number of jobs a run scans across every location or URL, and it defaults to 20 so a first run stays small. Set it to `0` to remove the limit. `maxPages` is an optional safety limit on how many result pages (25 jobs each) a single search walks; leave it empty or `0` and the run instead stops on its own once it reaches the site's own reported page count, a page comes back with no new jobs, or the search is otherwise exhausted. A built-in ceiling of 1000 pages (25,000 jobs) per search or link always applies; if it stops a walk that still had results, the run logs a warning and skips `EXPIRED` detection for that search, and `maxListings` accepts up to 100000. Under incremental mode, `maxListings` counts jobs scanned and classified, not jobs actually returned, so a quiet recurring run that suppresses most of a page still stops once it has looked at that many jobs, instead of paging deeper trying to make up the quota with fresh jobs.

#### Detail enrichment and its surcharge

Turning on `fetchDetails` fetches each job's own page and adds the full job description, GPS coordinates, a structured address, employment type, a valid-through date, an apply type, a company profile, and any recruiter phone number or email visible on the listing itself. This needs a residential connection routed through the UK; other connection types are refused on job pages.

Each detail-enriched job that is actually pushed to the dataset incurs a detail-enrichment charge on top of the base per-result price. The charge is counted only when the record reaches the dataset, never merely because its detail page was fetched: a job whose detail page was fetched to check for changes and then suppressed by incremental mode, because nothing about it had actually changed, is never charged for that detail pass.

#### Alternate input names, extra filters and output options

Every new input is optional and additive. Existing inputs win when both are set.

- Aliases: `query` (Keywords), `location` (Locations), `startUrls` (URLs; replaces the pre-filled example links and switches to URL mode when no keywords are set), `age` (Posted within, rounded up to 1, 3, 7 or 14 days), `maxResults` (Max jobs), `includeDetails` (Fetch details), `sort` (`relevance`, `date`, `salary_low`, `salary_high`), `proxyConfiguration` (Proxy).
- Filters: `remote` (same as job type Work from home), `contractType` (CONTRACT, FREELANCE, TEMPORARY and PART_TIME map to the matching job type), `excludeSponsored` (sponsored rows are not pushed and not charged). Other filters such as radius, experience, work type, company id, application method and language are not offered because Totaljobs has no verified matching search parameter; if supplied they are ignored and a warning is logged.
- Output: `descriptionFormat` (html, text or markdown), `descriptionMaxLength`, `outputFields`, `compact`, `excludeEmptyFields`.
- Extra row fields (added only when known): `jobKey`, `url`, `postCode`, `postedDaysAgo`, `companyId`, `companyUrl`, `companyLogoUrl`, `unifiedSalary`, `salaryMin`, `salaryMax`, `salaryDetail`, `locationDetail`, `geo`, `portalUrl`, `query`, `detailsFetched`, `descriptionHtml`, `descriptionMarkdown`, `contentHash`, `contactEmail`, `contactPhone`, `applyEmail`, `extractedEmails`, `extractedPhones`, `extractedUrls`, `socialProfiles`, `benefits`. They are copies of data already in the row and do not change what is charged.

#### Supported link shapes and dataset views

In URL mode the actor accepts search-result links (`/jobs/in-london`, `/jobs/nurse/in-london`, `/jobs/permanent/in-manchester`, region links such as `/jobs/in-south-east`, company links with `cmpId`, and links carrying `page`, `sort`, `salary`, `salarytypeid`, `postedWithin` or `companytypes`) and single job links (`/job/<slug>-job<id>` or `/job/<id>`). Anything else is reported as unsupported in the log with a reason instead of being skipped silently. The **URLs** field now ships pre-filled with one example of each supported shape, so a URL-mode run with the list untouched scrapes all of them.

The dataset has four views: **Jobs Overview**, **Full Details**, **Changes (incremental mode)** and **Contacts and apply**.

#### Resume and recurring updates

There are two different tools here. `resumeFromRunId` continues one specific earlier run: point it at that run's id (or its dataset id) and this run skips every job already collected there, returning only the ones it doesn't already have. Because that skip check runs before anything is counted against `maxListings`, already-seen jobs never eat into this run's cap, and a page that is entirely already-collected does not stop the walk before it has found anything new. But once the walk has found at least one new job this search, an all-duplicate page still ends that search, the same as a normal run, so the walk keeps going through pages that contain only jobs the resume dataset already holds (up to 200 in a row), and stops early only when a page repeats jobs collected in this same walk. An automatic same-run checkpoint also protects a long run against a platform migration or a Resurrect of a failed run, with no input needed.

Incremental mode (`incrementalMode`) is for running the exact same search again and again, for example on a daily schedule. The actor remembers the previous run of that search itself, so no run id is ever pasted. The first run for a given search returns every job as `NEW`. Later runs return only `NEW` and `UPDATED` jobs by default; a job that is unchanged since the last run is suppressed and not billed, unless `emitUnchanged` is turned on. `REAPPEARED` can only ever be produced for a job that an earlier run had already marked as no longer present, and that marking itself only happens as part of a completed `EXPIRED` pass: a previous run needs to have had `emitExpired` on and to have reached a genuine, uncapped, non-resumed end of the search. If `emitExpired` has never been on, or no run has ever completed that full a scan, a job that stops appearing is never marked gone, so it can't later come back as `REAPPEARED` either; it would simply look `UNCHANGED` or `UPDATED` once seen again. `EXPIRED` rows follow the same gate: they are only produced, and only billed, on a run that has `emitExpired` on and completes a full scan of the tracked search, never on a run capped by `maxListings` or `maxPages`, a `resumeFromRunId` run, or one resumed from a mid-scan checkpoint. Jobs hidden by `excludeSponsored` still count as present, so they are never marked `EXPIRED`. An earlier version stored some job links with a doubled site prefix; those stored links are repaired automatically on the next run, so the fix does not turn every tracked job into an `UPDATED` row. State is isolated automatically per mode, locations, keywords, job type, advertiser type, salary filter, posted-within window, sort order, URLs, and the detail-fetch setting; `maxListings`, `maxPages`, and `maxNotifyListings` are cost knobs, not filters, so raising a limit on a later run never re-bills the whole catalogue. Set `stateKey` to name or deliberately share a monitoring campaign. `ignoreFieldsForChanges` lists fields (plain names match at any depth, dotted paths one nested location) that are left out of change detection; adding one never causes a one-time wave of `UPDATED` rows, because stored jobs are re-compared with the current ignore set.

### 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 job, 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**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. If the picker is empty, you haven't authorized a connector yet.
3. For **Notion**, also set `notionParentPageUrl` to 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` | string | `search` | `search` builds a search from filters; `url` paginates search links you paste. |
| `locations` | array | (empty; prefilled with `["London"]`) | Towns, cities, counties, or regions (search mode). One search runs per location; leave empty to default to London. |
| `keywords` | string | (empty) | Job title, skill, or employer to search for. Empty lists all jobs in the location. |
| `jobType` | string | `any` | `any` / `permanent` / `contract` / `temporary` / `part-time` / `work-from-home`. |
| `companyType` | string | `any` | `any` / `employer` (direct) / `agency`. |
| `minSalary` | integer | (empty) | Lowest salary to include, paired with the salary period below. |
| `salaryType` | string | `annual` | `annual` / `daily` / `hourly`, the unit for the minimum salary filter. |
| `postedWithin` | string | `0` | `0` (any time) / `1` / `3` / `7` / `14` days. |
| `sortBy` | string | `relevance` | `relevance` / `date` / `salary-desc` / `salary-asc` / `distance`. |
| `remote` | boolean | `false` | Same as job type Work from home. |
| `contractType` | string | (empty) | `CONTRACT`, `FREELANCE`, `TEMPORARY` or `PART_TIME`, mapped to the matching job type. |
| `excludeSponsored` | boolean | `false` | Do not push (or charge) sponsored rows. |
| `query` | string | (empty) | Alias of Keywords. |
| `location` | string | (empty) | Alias of Locations. |
| `startUrls` | array | (empty) | Alias of URLs; switches to URL mode when no keywords are set. |
| `age` | integer | (empty) | Alias of Posted within, rounded up to 1, 3, 7 or 14 days. |
| `sort` | string | (empty) | Alias of Sort: `relevance`, `date`, `salary_low`, `salary_high`. |
| `maxResults` | integer | (empty) | Alias of Max jobs. |
| `includeDetails` | boolean | `false` | Alias of Fetch full job details. |
| `descriptionFormat` | string | `html` | `html`, `text` or `markdown`. |
| `descriptionMaxLength` | integer | (empty) | Truncate descriptions to this many characters. |
| `outputFields` | array | (empty) | Keep only these fields in each row. |
| `compact` | boolean | `false` | Smaller rows with the main fields only. |
| `excludeEmptyFields` | boolean | `false` | Drop fields that have no value. |
| `proxyConfiguration` | object | (empty) | Alias of Proxy. |
| `urls` | array | one example per supported link shape (12 links) | Totaljobs.com search-result links and single job pages (`/job/...`) to scrape (URL mode). Filter fields above are ignored. Unsupported links (bare `/jobs`, keyword + job type in one path, other sites) are skipped with a clear log message. |
| `fetchDetails` | boolean | `false` | Fetch each job's own page to add the full description, GPS, structured address, employment type, valid-through date, and company profile. |
| `maxListings` | integer | `20` | Total jobs to collect (or scan, in incremental mode) across all searches. `0` = unlimited. |
| `maxPages` | integer | (empty) | Optional safety limit on result pages (25 jobs each) walked per search or URL. Leave empty or `0` to walk until Max jobs, the site's own last page, a repeat-page guard, or the built-in ceiling of 1000 pages (25,000 jobs) per search or URL, whichever comes first. |
| `resumeFromRunId` | string | (empty) | Id of a previous run of this actor, or a dataset id. Jobs already in that dataset are skipped, so this run returns only the new ones. |
| `incrementalMode` | boolean | `false` | Turn on for daily or recurring monitoring of the same search. Returns only `NEW` and `UPDATED` jobs after the first run by default. |
| `stateKey` | string | (empty) | Optional. Name a monitoring campaign explicitly, or share state across differently configured runs. |
| `emitUnchanged` | boolean | `false` | Also return jobs unchanged since the last run, marked `UNCHANGED` (extra billed rows). |
| `emitExpired` | boolean | `false` | Also return jobs no longer found, marked `EXPIRED`. Only produced once a run fully scans the tracked search. |
| `proxy` | object | (empty; prefilled with Residential, country GB) | Connection settings. |
| `mcpConnectors` | array | (empty) | Optionally pipe results into the apps you already use via Model Context Protocol (MCP) connectors (Notion, Linear, Airtable, Apify). Authorize a connector once under Apify, Settings, Integrations, then select it here. |
| `ignoreFieldsForChanges` | array | (empty) | Incremental mode only. Field names (or dotted paths such as `location.postalCode`) that must not make a job count as `UPDATED`. They stay in the output and are only left out of the comparison and `changedFields`. |
| `notionParentPageUrl` | string | (empty) | URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors. |
| `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.

```json
{
  "rowType": "job",
  "brand": "totaljobs",
  "jobId": 100234567,
  "jobUrl": "https://www.totaljobs.com/job/warehouse-operative/sample-employer-job100234567",
  "applyUrl": "https://www.totaljobs.com/job/warehouse-operative/sample-employer-job100234567",
  "sourceSite": "Totaljobs.com",
  "title": "Warehouse Operative",
  "datePosted": "2026-03-14T00:00:00.000Z",
  "employer": {
    "id": 4820001,
    "name": "Sample Employer",
    "url": "https://www.totaljobs.com/jobs/sample-employer-ltd?cmpId=4820001",
    "logoUrl": "https://www.totaljobs.com/CompanyLogos/00000000000000000000000000000000.png",
    "isAnonymous": false
  },
  "location": {
    "text": "Slough, Berkshire, SL1",
    "postalCode": "ZZ1 1ZZ",
    "locality": "Slough",
    "region": "Berkshire",
    "country": "GB",
    "latitude": 51.51,
    "longitude": -0.59,
    "lat": 51.51,
    "lng": -0.59,
    "streetAddress": "1 Acme Way"
  },
  "salary": { "rawText": "£24,000 - £27,500 per annum", "min": 24000, "max": 27500, "currency": "GBP", "period": "year" },
  "skills": [],
  "textSnippet": "Join our busy distribution centre team on a full time basis, no experience necessary.",
  "isSponsored": false,
  "crossPostedCount": 0,
  "description": "Full HTML job description appears here when Fetch full job details is enabled.",
  "descriptionText": "Full job description text appears here when Fetch full job details is enabled.",
  "employmentType": ["FULL_TIME"],
  "industry": "Logistics",
  "validThrough": "2026-04-14T00:00:00.000Z",
  "directApply": true,
  "applyType": "internal",
  "contactPhones": ["0000000000"],
  "contactEmails": ["careers@sample-employer.example"],
  "detailFetched": true
}
```

### Plan Requirement

Totaljobs.com accepts a residential connection routed through the UK most reliably, which is the prefilled default. Listing pages are frequently refused on other connection types, and full job details (`fetchDetails`) always need that UK residential route; the run automatically retries with a different connection when one is refused. Set `proxy.apifyProxyGroups` to `["RESIDENTIAL"]` with `apifyProxyCountry` `"GB"` if you change the connection settings and results become sparse.

### FAQ

#### How much does it cost?

Billing uses three events: `apify-actor-start` once per run, `apify-default-dataset-item` for each job pushed to the dataset, and `detail-enrichment` for each pushed job that was detail-enriched (`fetchDetails`, or a single job-detail link). Suppressed, unchanged, duplicate or failed rows are never charged. The **Pricing** tab shows the current rates. Use **Max jobs** to cap the cost of any run.

#### Is it legal to scrape Totaljobs.com?

This actor collects only publicly available job listing data. You are responsible for how you use it: follow Totaljobs.com's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Take particular care with any recruiter phone number or email address the detail pass surfaces from a listing; it is public on the page, but it is still someone's contact information.

#### Can I get only new or changed jobs on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated, and (once a prior complete scan has marked a job gone with Emit expired on) reappeared or expired jobs, and unchanged ones are not billed.

#### Why did my run return fewer jobs than I expected?

Check **Max jobs**: it caps the total scanned across all locations or URLs, and defaults to 20. Set it higher, or to `0`, to collect until the site's last page or the built-in ceiling of 1000 pages (25,000 jobs) per search. In incremental mode, most jobs on a quiet recurring run are unchanged and suppressed by design; that is not a failed run.

#### Why did my run fail instead of returning an empty dataset?

If every result page is refused, the run stops with a clear message so "no jobs match this search" is never confused with "nothing could be read". A genuine zero-match search (a real result page reporting zero jobs) still succeeds with an empty dataset.

#### 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 results into Notion, Linear, or Airtable.

### 🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/jobsite-co-uk-scraper"><b>Jobsite UK</b></a><br>Collect UK job listings from Jobsite.co.uk at scale. Search by keyword, location...</td><td>🏨 <a href="https://apify.com/abotapi/caterer-com-scraper"><b>Caterer.com Scraper</b></a><br>Scrape UK hospitality jobs from Caterer.com, including chef, hotel, restaurant, bar, and...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/ziprecruiter-com-scraper"><b>ZipRecruiter Scraper</b></a><br>Scrape ZipRecruiter jobs with titles, companies, salaries, locations, benefits, apply...</td><td>💼 <a href="https://apify.com/abotapi/stepstone-de"><b>StepStone.de</b></a><br>Scrape StepStone.de jobs by keyword, location, filters, or URL. Extract 35+ fields...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/dice-com-scraper"><b>Dice.com Scraper</b></a><br>Scrape tech job listings from Dice.com by keyword, filters, or URL. Extract job titles...</td><td>💼 <a href="https://apify.com/abotapi/s1jobs-com-scraper"><b>s1jobs Scraper</b></a><br>Scrape jobs from s1jobs.com across Scotland and the UK. Search by keyword, location, or...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/totaljobs-com-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

## `mode` (type: `string`):

How to find jobs: build a search from filters, or paginate URLs you paste.

## `locations` (type: `array`):

Towns, cities, counties, or regions (Search mode). Examples: 'London', 'Manchester', 'Edinburgh', 'Surrey'. One search runs per location. Leave empty for all of the UK.

## `keywords` (type: `string`):

Job title, skill, or company to search for. Example: 'nurse', 'java developer', 'project manager'. Leave empty to list all jobs in the location.

## `jobType` (type: `string`):

Contract / working arrangement filter.

## `companyType` (type: `string`):

Restrict to direct employers or recruitment agencies.

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

Lowest salary to include. Combined with the salary period below. Leave empty for no minimum.

## `salaryType` (type: `string`):

Unit for the minimum salary filter.

## `postedWithin` (type: `string`):

Only jobs posted within this many days. Leave as Any time for no date filter.

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

Result ordering.

## `remote` (type: `boolean`):

Same as job type Work from home.

## `contractType` (type: `string`):

CONTRACT, FREELANCE, TEMPORARY and PART_TIME map onto the matching Job type. Job type wins when both are set.

## `excludeSponsored` (type: `boolean`):

Skip rows the site marks as sponsored. Skipped rows are not pushed and not charged.

## `urls` (type: `array`):

Totaljobs.com links to scrape (URL mode): search-result links (location, keyword, job type, company, filtered, sorted or paginated) and single job pages (/job/...). One example of each shape is prefilled. Filter fields above are ignored. A page number in a search link is honoured as the starting point. Unsupported links are skipped with a clear log message.

## `fetchDetails` (type: `boolean`):

Fetch detail pages to add the full description, GPS coordinates, structured address, employment type, valid-through date, and company profile. Adds ~12 fields. Needs an Apify Residential GB proxy and costs more per job. Off = fast, cheap, 35 listing fields.

## `maxListings` (type: `integer`):

Total jobs to collect across all searches. This is the main limit. Set 0 for unlimited (bounded by Max pages).

## `maxPages` (type: `integer`):

Optional safety limit on pages walked per location/URL (25 jobs per page). Leave empty or set to 0 to walk until the run stops at Max jobs, the site's own last page, or a repeat-page guard, with a built-in ceiling of 1000 pages (25,000 jobs) per search.

## `query` (type: `string`):

Alias of Keywords. Keywords wins when both are set.

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

Alias of Locations for a single place, e.g. London.

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

Alias of URLs. Setting it without a mode switches the run to URL mode.

## `age` (type: `integer`):

Rounded up to the nearest supported window (1, 3, 7, 14 days). Values above 14 apply no recency filter.

## `sort` (type: `string`):

Used when Sort by is left at its default.

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

Alias of Max jobs. Max jobs wins when both are set.

## `includeDetails` (type: `boolean`):

Alias of Fetch details.

## `descriptionFormat` (type: `string`):

With Fetch details: html keeps every description field, text keeps descriptionText only, markdown keeps descriptionMarkdown only.

## `descriptionMaxLength` (type: `integer`):

Truncate description fields to this many characters. 0 keeps the full text.

## `outputFields` (type: `array`):

Keep only these top-level fields in each row.

## `compact` (type: `boolean`):

Keep only the core fields (ignored when Output fields is set).

## `excludeEmptyFields` (type: `boolean`):

Drop null, empty text, empty list and empty object values from each row.

## `proxy` (type: `object`):

Connection settings. totaljobs.com accepts Apify Residential with country GB most reliably (the default); listing pages occasionally work on Datacenter but it is frequently refused, and full job details (fetchDetails) always need Residential GB. Datacenter and non-GB residential are refused on job pages.

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

Alias of Proxy. Proxy wins when both are set.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Jobs already in that dataset are skipped, so this run returns only NEW jobs (a delta). Combine both runs' datasets for the full set. Max jobs then counts only the new jobs. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching jobs as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED jobs. Turn on "Emit unchanged" or "Emit expired" only when you also want those jobs returned (and billed). State is kept separately for each distinct search/filter setup (or by State key below).

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from your search/filter settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return jobs that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max jobs or Max pages capped it, or when Resume was used. This returns — and bills — extra synthetic rows.

## `ignoreFieldsForChanges` (type: `array`):

Optional. Field names that must not make a job count as UPDATED, for example `salary` or `location.postalCode`. A plain name is ignored wherever it appears, including inside nested objects; a dotted path ignores one nested location. Ignored fields stay in the output and are only left out of the comparison and `changedFields`. Rotating or ad-placement fields are already ignored by default.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "locations": [
    "London"
  ],
  "jobType": "any",
  "companyType": "any",
  "salaryType": "annual",
  "postedWithin": "0",
  "sortBy": "relevance",
  "remote": false,
  "excludeSponsored": false,
  "urls": [
    "https://www.totaljobs.com/jobs/in-london",
    "https://www.totaljobs.com/jobs/nurse/in-london",
    "https://www.totaljobs.com/jobs/permanent/in-manchester",
    "https://www.totaljobs.com/jobs/in-london?page=2",
    "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2",
    "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2",
    "https://www.totaljobs.com/jobs/in-south-east",
    "https://www.totaljobs.com/jobs/nurse",
    "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1",
    "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737",
    "https://www.totaljobs.com/job/108199737",
    "http://totaljobs.com/jobs/in-london"
  ],
  "fetchDetails": false,
  "maxListings": 20,
  "startUrls": [
    {
      "url": "https://www.totaljobs.com/jobs/in-london"
    },
    {
      "url": "https://www.totaljobs.com/jobs/nurse/in-london"
    },
    {
      "url": "https://www.totaljobs.com/jobs/permanent/in-manchester"
    },
    {
      "url": "https://www.totaljobs.com/jobs/in-london?page=2"
    },
    {
      "url": "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2"
    },
    {
      "url": "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2"
    },
    {
      "url": "https://www.totaljobs.com/jobs/in-south-east"
    },
    {
      "url": "https://www.totaljobs.com/jobs/nurse"
    },
    {
      "url": "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1"
    },
    {
      "url": "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737"
    },
    {
      "url": "https://www.totaljobs.com/job/108199737"
    },
    {
      "url": "http://totaljobs.com/jobs/in-london"
    }
  ],
  "descriptionFormat": "html",
  "descriptionMaxLength": 0,
  "compact": false,
  "excludeEmptyFields": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "locations": [
        "London"
    ],
    "urls": [
        "https://www.totaljobs.com/jobs/in-london",
        "https://www.totaljobs.com/jobs/nurse/in-london",
        "https://www.totaljobs.com/jobs/permanent/in-manchester",
        "https://www.totaljobs.com/jobs/in-london?page=2",
        "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2",
        "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2",
        "https://www.totaljobs.com/jobs/in-south-east",
        "https://www.totaljobs.com/jobs/nurse",
        "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1",
        "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737",
        "https://www.totaljobs.com/job/108199737",
        "http://totaljobs.com/jobs/in-london"
    ],
    "startUrls": [
        {
            "url": "https://www.totaljobs.com/jobs/in-london"
        },
        {
            "url": "https://www.totaljobs.com/jobs/nurse/in-london"
        },
        {
            "url": "https://www.totaljobs.com/jobs/permanent/in-manchester"
        },
        {
            "url": "https://www.totaljobs.com/jobs/in-london?page=2"
        },
        {
            "url": "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2"
        },
        {
            "url": "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2"
        },
        {
            "url": "https://www.totaljobs.com/jobs/in-south-east"
        },
        {
            "url": "https://www.totaljobs.com/jobs/nurse"
        },
        {
            "url": "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1"
        },
        {
            "url": "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737"
        },
        {
            "url": "https://www.totaljobs.com/job/108199737"
        },
        {
            "url": "http://totaljobs.com/jobs/in-london"
        }
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "GB"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/totaljobs-com-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 = {
    "mode": "search",
    "locations": ["London"],
    "urls": [
        "https://www.totaljobs.com/jobs/in-london",
        "https://www.totaljobs.com/jobs/nurse/in-london",
        "https://www.totaljobs.com/jobs/permanent/in-manchester",
        "https://www.totaljobs.com/jobs/in-london?page=2",
        "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2",
        "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2",
        "https://www.totaljobs.com/jobs/in-south-east",
        "https://www.totaljobs.com/jobs/nurse",
        "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1",
        "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737",
        "https://www.totaljobs.com/job/108199737",
        "http://totaljobs.com/jobs/in-london",
    ],
    "startUrls": [
        { "url": "https://www.totaljobs.com/jobs/in-london" },
        { "url": "https://www.totaljobs.com/jobs/nurse/in-london" },
        { "url": "https://www.totaljobs.com/jobs/permanent/in-manchester" },
        { "url": "https://www.totaljobs.com/jobs/in-london?page=2" },
        { "url": "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2" },
        { "url": "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2" },
        { "url": "https://www.totaljobs.com/jobs/in-south-east" },
        { "url": "https://www.totaljobs.com/jobs/nurse" },
        { "url": "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1" },
        { "url": "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737" },
        { "url": "https://www.totaljobs.com/job/108199737" },
        { "url": "http://totaljobs.com/jobs/in-london" },
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/totaljobs-com-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 '{
  "mode": "search",
  "locations": [
    "London"
  ],
  "urls": [
    "https://www.totaljobs.com/jobs/in-london",
    "https://www.totaljobs.com/jobs/nurse/in-london",
    "https://www.totaljobs.com/jobs/permanent/in-manchester",
    "https://www.totaljobs.com/jobs/in-london?page=2",
    "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2",
    "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2",
    "https://www.totaljobs.com/jobs/in-south-east",
    "https://www.totaljobs.com/jobs/nurse",
    "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1",
    "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737",
    "https://www.totaljobs.com/job/108199737",
    "http://totaljobs.com/jobs/in-london"
  ],
  "startUrls": [
    {
      "url": "https://www.totaljobs.com/jobs/in-london"
    },
    {
      "url": "https://www.totaljobs.com/jobs/nurse/in-london"
    },
    {
      "url": "https://www.totaljobs.com/jobs/permanent/in-manchester"
    },
    {
      "url": "https://www.totaljobs.com/jobs/in-london?page=2"
    },
    {
      "url": "https://www.totaljobs.com/jobs/permanent/in-london?salary=30000&salarytypeid=1&postedWithin=7&companytypes=2"
    },
    {
      "url": "https://www.totaljobs.com/jobs/nurse/in-london?sort=2&page=2"
    },
    {
      "url": "https://www.totaljobs.com/jobs/in-south-east"
    },
    {
      "url": "https://www.totaljobs.com/jobs/nurse"
    },
    {
      "url": "https://www.totaljobs.com/jobs/gopuff/in-london?cmpId=1245157&cmp=1"
    },
    {
      "url": "https://www.totaljobs.com/job/delivery-driver-part-time-merton/ocado-job108199737"
    },
    {
      "url": "https://www.totaljobs.com/job/108199737"
    },
    {
      "url": "http://totaljobs.com/jobs/in-london"
    }
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}' |
apify call abotapi/totaljobs-com-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/totaljobs-com-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/RO7aokuNWfmP0kV0G/builds/8XKQtTceoDN0W7Bek/openapi.json
