# Caterer.com Scraper - UK Hospitality Jobs, Salaries & Details (`abotapi/caterer-com-scraper`) Actor

Scrape UK hospitality jobs from Caterer.com, including chef, hotel, restaurant, bar, and events roles. Search by keyword, location, filters, or URLs. Returns salary, employer, logo, location, skills, and 35+ fields, with optional full description, GPS, and company profile.

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

## Pricing

from $1.00 / 1,000 job 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

## Caterer.com Scraper: UK Hospitality Job Listings

Caterer.com Scraper turns Caterer.com into a structured UK hospitality jobs feed. Get chef, restaurant, hotel, bar, and events roles with 35+ fields straight from the listing, including a parsed salary (min, max, currency, period), employer, location, and skills. Search by keyword and location with real server-side filters, or paste any Caterer.com search URL and walk it forward. Switch on an optional detail pass for the full description, GPS coordinates, employment type, and company profile, then export to JSON, CSV, or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **35+ fields per job from the listing page alone.** No detail fetch required for title, parsed salary, employer, logo, location, posting source, skills, and snippet.
- **Two ways to run.** Build a search from keyword, location, and filters, or paste any Caterer.com search URL and walk it forward.
- **Real server-side filters.** Job type, advertiser type (direct employer vs agency), minimum salary by period (year, day, or hour), posted-within window, and sort order.
- **Parsed salary.** The free-text salary string is split into min, max, currency, and period, including the hourly rates common in hospitality.
- **Optional full details.** GPS coordinates, structured address, full description, employment type, valid-through date, and company profile, plus any contact number or email the posting itself publishes.
- **Cost control built in.** A single job cap, an optional page cap, and a residential request budget to bound what each run collects and spends.
- **Resume and incremental modes.** Continue an interrupted run without re-collecting jobs, or schedule the same search daily and get back only what changed.

### Use Cases

- **Recruitment and staffing agencies:** benchmark competitor postings, salary bands, and hiring volume across chef, front-of-house, and management roles.
- **Job board and career site operators:** mirror UK hospitality vacancies with structured salary, location, and employer data for your own listings.
- **Compensation and labour-market research:** track parsed hourly and annual pay ranges by role, region, and posting date to study wage trends in hospitality.
- **Employer brand and competitor monitoring:** watch specific companies' hiring activity, job counts, and posting frequency over time.
- **Sales lead generation:** find companies actively hiring chefs, managers, or bar staff as leads for recruitment or hospitality-supply sales.

### Data You Get

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

| Field | Example |
| --- | --- |
| `jobId` | `100000001` |
| `title` | `"Head Chef"` |
| `jobUrl` | Caterer.com link to the job (also used as `applyUrl`) |
| `sourceSite` | `"Caterer.com"` |
| `datePosted` | `"2026-01-01T00:00:00.000Z"` |
| `employer.name` | `"Sample Restaurant Group"` |
| `employer.logoUrl` | link to the employer's logo image |
| `employer.isAnonymous` | `false` |
| `location.text` | `"Soho, Central London (W1)"` |
| `location.postalCode` | `"W1"` |
| `location.latitude` / `location.longitude` (detail) | `51.5000` / `-0.1300` |
| `salary.rawText` | `"From £14 to £18 per hour"` |
| `salary.min` / `salary.max` | `14` / `18` |
| `salary.currency` / `salary.period` | `"GBP"` / `"hour"` |
| `skills` | `["Menu planning", "Food safety"]` |
| `textSnippet` | `"We are looking for an experienced head chef..."` (listing-level description excerpt; the full text is `description` with `fetchDetails`) |
| `searchKeyword` / `searchLocation` | `"chef"` / `"london"` (the search that surfaced the job) |
| `isSponsored` | `false` |
| `crossPostedCount` | `1` |
| `description` (detail) | full job description text, when `fetchDetails` is on |
| `employmentType` (detail) | `["FULL_TIME"]` |
| `validThrough` (detail) | `"2026-02-01T00:00:00.000Z"` |
| `contactPhones` / `contactEmails` (detail) | best-effort numbers/addresses the posting itself publishes, often empty |
| `company` (detail) | employer profile: size, founded, industries, benefits, when available |

Other detail-only fields also come back once `fetchDetails` is on: structured address (`location.locality`, `location.region`, `location.country`, `location.streetAddress`), `industry`, `directApply`, `applyType`, `jobLocationType`, `contractType`, and `workType`. `externalId` is filled from the listing and refined from the detail page when available.

#### Dataset views

The dataset has three views: **Jobs Overview** (title, employer, location, salary, link), **Full Details** (adds the description and company data collected with `fetchDetails`), and **Changes (incremental mode)** (`changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`). The dataset schema types every field, so exports keep stable column types.

### How to Use

1. Pick a **mode**: `search` (build from keyword, location, and filters) or `url` (paste Caterer.com search links).
2. For search mode, set **Locations** and any filters you need (keywords, job type, advertiser type, minimum salary, posted-within window, sort order). For URL mode, paste one or more search-result URLs; filter fields are ignored.
3. Turn on **Fetch full job details** if you need the full description, GPS, employment type, and company profile. Set **Max jobs** (and, optionally, **Max pages**) to control run size and cost.
4. Click **Start**. Download the dataset as JSON, CSV, or Excel, or read it through the API.

**Basic search in one city:**

```json
{
  "mode": "search",
  "locations": ["London"],
  "keywords": "chef",
  "maxListings": 20
}
```

**Search with filters (permanent chef roles, at least £30,000 a year, posted in the last 7 days, newest first):**

```json
{
  "mode": "search",
  "locations": ["Manchester", "Leeds"],
  "keywords": "chef",
  "jobType": "permanent",
  "minSalary": 30000,
  "salaryType": "annual",
  "postedWithin": "7",
  "sortBy": "date",
  "maxListings": 100
}
```

**Full details (adds description, GPS, employment type, company profile):**

```json
{
  "mode": "search",
  "locations": ["London"],
  "keywords": "restaurant manager",
  "fetchDetails": true,
  "maxListings": 50
}
```

**Paginate pasted URLs (filter fields are ignored in URL mode):**

```json
{
  "mode": "url",
  "urls": [
    "https://www.caterer.com/jobs/chef/in-london",
    "https://www.caterer.com/jobs/in-edinburgh"
  ],
  "maxPages": 5,
  "maxListings": 200
}
```

#### URL mode: supported URL shapes

URL mode accepts the shapes below. **Behaviour change:** if you switch to URL mode and leave the URLs field untouched, the run now scrapes one example of each shape (spread across your Max jobs limit) instead of a single London search. Replace the list with your own links to scrape only those.

| Shape | Example | What you get |
|---|---|---|
| Location | `https://www.caterer.com/jobs/in-london` | All jobs in a place |
| County or region | `https://www.caterer.com/jobs/in-surrey` | All jobs in the county |
| Keyword + location | `https://www.caterer.com/jobs/chef/in-london` | Keyword search in a place |
| Job type + location | `https://www.caterer.com/jobs/permanent/in-london` | Permanent, contract, temporary, part-time, work-from-home |
| Keyword + job type + filters | `https://www.caterer.com/jobs/chef/permanent/in-london?sort=2&salary=30000&salarytypeid=1` | Filters in the link are kept |
| Paginated | `https://www.caterer.com/jobs/in-london?page=3` | Starts at that page (a partial scope, so no EXPIRED rows) |
| Keyword only | `https://www.caterer.com/jobs/chef` | UK-wide keyword search |
| Root | `https://www.caterer.com/jobs` | The bare root has no results, so the UK-wide listing is scraped |
| Advertiser type | `https://www.caterer.com/jobs/agency/in-london` | Agency listings in a place (apex `caterer.com` and `m.caterer.com` links are also normalised to www) |
| Filter parameters | `https://www.caterer.com/jobs/chef/in-london?postedwithin=3` | Kept as typed |
| Single job page | `https://www.caterer.com/job/executive-chef-indian-fine-dining-restaurant/little-kitchen-help-ltd-job108217049` | One full row with description and company data |
| Single job page (numeric id only) | `https://www.caterer.com/job/108217049` | One full row with description and company data |

A link to an unknown place (the site answers with a generic empty page) logs a clear "No jobs matched this URL" warning. Links that are not Caterer.com listing or job pages are skipped with a warning in the run log. In search mode, Locations take plain city or county names such as `Greater London` or `Surrey`.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

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

JavaScript:

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

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

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

#### How Max jobs and Max pages shape a run

**Max jobs** (`maxListings`, default `20`) is the sole cap on total jobs collected across every location or URL in a run; set it to `0` for unlimited. Values above `1000000` are capped at `1000000` jobs per run. **Max pages** (`maxPages`, default `0`) walks the whole catalogue by default: the run stops on its own once the site's reported page count is reached, a page comes back empty, or a page holds only jobs already collected earlier in this run, never an artificial page-number ceiling. Give **Max pages** a finite value only as an extra safety limit; **Max jobs** is the cost control you actually want.

If a run is interrupted by a platform migration and continues from its automatic checkpoint, the continued part starts a fresh **Max jobs** count, so that one run's dataset can end up holding more than **Max jobs** jobs (never duplicates).

All locations and URLs in one run share a single run-wide dedup set, which matters when two of them genuinely overlap (for example `"London"` and `"Greater London"`):

- **The second search can stop early.** A page of the second search made up entirely of jobs the first search already collected counts as a repeat page, so the second search stops there and its later pages, even ones full of jobs the first search never saw, are not fetched.
- **Jobs past the cap are also marked seen.** When **Max jobs** is split across several searches and cuts a page short, the leftover job ids on that page are still marked as seen for the rest of this run (including a checkpoint-continued part of it), so the second search skips them too.

Run genuinely overlapping locations as separate runs if you need every job from each of them.

#### Resume and recurring updates

There are two different things here; pick the one that matches what you're doing.

| Need | Use |
| --- | --- |
| A run stopped and should continue | `resumeFromRunId`, or the automatic checkpoint recovery |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous large run: paste a run id or dataset id and this run skips jobs already collected there, returning only the remaining new jobs. An automatic same-run checkpoint also protects against platform migrations without any input needed.

**Incremental mode** (`incrementalMode`) is for a schedule, for example daily: the actor remembers the previous run of the *same* search by itself, so you never paste a run id. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` jobs by default; unchanged jobs are suppressed and not billed. Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned, and billed for. State is isolated automatically per mode, locations, keywords, job type, advertiser type, minimum salary, salary period, posted-within window, sort order, URLs, and `fetchDetails`; `maxListings`, `maxPages`, and `maxResidentialRequests` are caps, not filters, and deliberately do not affect the state key.

**What counts as a change.** A job is `UPDATED` when any returned field differs from the stored copy (the differing names are listed in `changedFields`). Excluded from the comparison, but still returned: `scrapedAt`, `detailFetched`, `crossPostedCount`, `sourceSearchUrl`, `searchKeyword`, `searchLocation`, `isSponsored`, `isHighlighted`, `isTopJob`, and `isTrafficFromPartner`. Use `ignoreFieldsForChanges` to exclude more fields, including nested ones with dot notation such as `salary.max` or `employer.name`; changing this list never causes a one-off wave of `UPDATED` rows. `EXPIRED` rows are only produced after a fully scanned search, never for a capped run, a Resume run, or a pasted URL that starts on a page after the first.

When `incrementalMode` is on, every returned record also carries `changeType` (`NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED`, or `EXPIRED`), `changedFields` (populated only for `UPDATED`), `firstSeenAt`, and `lastSeenAt`.

**Max jobs is spent on jobs scanned, not jobs returned.** Outside incremental mode the two are the same. Under incremental mode, most jobs on a quiet recurring run are `UNCHANGED` and suppressed; if the cap instead counted returned rows, a quiet run would keep paging deeper trying to "backfill" the quota with fresh jobs every day, and cost would never actually drop. The tradeoff: on a small **Max jobs** value, if the first jobs scanned all happen to be unchanged (the default sort is relevance, which can keep the same jobs on page one day after day), a run can scan its whole cap and return nothing new even though newer jobs exist further into the catalogue. If a small, capped incremental run keeps returning nothing, raise **Max jobs**, or sort by date so newer postings surface first.

**Combining Resume with Incremental mode** to bootstrap a monitoring campaign from an earlier large run works, but the jobs it seeds are known only by id, not by content: the very next incremental run reclassifies each of them as `UPDATED` (and bills them as such) even if nothing about the job actually changed, because there is nothing stored yet to compare against. Expect and disregard that one-off `UPDATED` batch the first time a resume-seeded campaign runs again.

`EXPIRED` is only synthesized once a run fully scans every search/URL target: not capped by **Max jobs** or **Max pages**, not a resume run, and not a checkpoint-interrupted run. If any one search stops early (a refused page, or the overlapping-search repeat-page stop described above), `EXPIRED` is skipped for the whole run, not just for that search. A URL that starts on a later page (`?page=3`), or any single job-page URL in the list, makes the run a partial scope, so `EXPIRED` is skipped for the whole run. Once a run does fully scan every target, though, every tracked job absent this run is returned as `EXPIRED` and billed, with **no cap of its own**: **Max jobs** bounds this run's scan, not the size of the `EXPIRED` batch a large, long-running monitoring campaign can produce. If a `stateKey` is deliberately shared across two differently configured searches, a completed run of the narrower one can mark the other search's entire tracked set `EXPIRED`, since state comparison has no notion of which search actually ran. Share a `stateKey` only between searches that genuinely cover the same jobs.

### Send results into your apps (MCP connectors)

Optionally pipe 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.

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.
3. For Notion, also set `notionParentPageUrl` to the page where items should be created.

Each connector receives a condensed, human-readable summary per item (title plus key fields), not the full JSON; the complete record always stays in the Apify dataset. 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 URLs from filters; `url` paginates URLs you paste. |
| `locations` | array | `["London"]` | Towns, cities, counties, or regions (search mode). One search runs per location. Leave empty for all of the UK. |
| `keywords` | string | (empty) | Hospitality role, skill, or employer to search for. Leave empty to list all hospitality 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. Leave empty for no minimum. |
| `salaryType` | string | `annual` | `annual` / `daily` / `hourly`. 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`. |
| `urls` | array | sample | Caterer.com search-result URLs to scrape (URL mode). Filter fields above are ignored. A page number in the URL is honoured as the starting point. |
| `fetchDetails` | boolean | `false` | Fetch detail pages to add the full description, GPS coordinates, structured address, employment type, valid-through date, and company profile. |
| `maxListings` | integer | `20` | Total jobs to collect across all searches. The main limit. `0` = unlimited (bounded by Max pages). |
| `maxPages` | integer | `0` | Optional safety limit on pages walked per location/URL (25 jobs per page). `0` = walk the whole catalogue: stops at Max jobs, the site's own last page, or a repeat-page guard, never an artificial page cap. |
| `maxResidentialRequests` | integer | `0` | Safety cap on residential-tier requests this run may use (listing pages and detail pages). `0` = unlimited. When reached, detail enrichment stops and listing pages continue only on whatever other connection is still available. |
| `resumeFromRunId` | string | (none) | 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). For recurring daily monitoring of the same search, use Incremental mode instead. |
| `incrementalMode` | boolean | `false` | Turn on for daily or recurring monitoring. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` jobs by default. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Fields (nested allowed, e.g. `salary.max`) that never make a job `UPDATED`. They are still returned. |
| `stateKey` | string | (none) | Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently configured runs. Leave empty for an automatically derived key. |
| `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 (not capped, not resumed). |
| `proxy` | object | Residential GB | Connection settings. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

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

```json
{
  "rowType": "job",
  "scrapedAt": "2026-01-01T00:00:00.000Z",
  "jobId": 100000001,
  "jobUrl": "https://www.caterer.com/job/head-chef/sample-agency-job100000001",
  "applyUrl": "https://www.caterer.com/job/head-chef/sample-agency-job100000001",
  "sourceSite": "Caterer.com",
  "title": "Head Chef",
  "datePosted": "2026-01-01T00:00:00.000Z",
  "employer": {
    "id": 1000000,
    "name": "Sample Restaurant Group",
    "url": "https://www.caterer.com/jobs/sample-restaurant-group?cmpId=1000000",
    "logoUrl": "https://www.caterer.com/CompanyLogos/00000000000000000000000000000000.png",
    "isAnonymous": false
  },
  "location": {
    "text": "Soho, Central London (W1)",
    "postalCode": "W1",
    "locality": "Soho",
    "region": "London",
    "country": "GB",
    "latitude": 51.5000,
    "longitude": -0.1300
  },
  "salary": { "rawText": "From £14 to £18 per hour", "min": 14, "max": 18, "currency": "GBP", "period": "hour" },
  "skills": ["Menu planning", "Food safety"],
  "textSnippet": "We are looking for an experienced head chef to lead our kitchen team.",
  "isSponsored": false,
  "crossPostedCount": 1,
  "description": "Full job description text appears here when fetchDetails is enabled.",
  "employmentType": ["FULL_TIME"],
  "industry": "Catering, Catering-Chef",
  "validThrough": "2026-02-01T00:00:00.000Z",
  "directApply": true,
  "applyType": "DirectApply",
  "contactPhones": [],
  "contactEmails": [],
  "company": { "name": "Sample Restaurant Group", "size": "51-200", "founded": 2005, "industries": ["Hospitality"] },
  "detailFetched": true
}
```

### Plan Requirement

Caterer.com is read through Apify Proxy. The prefilled connection is Apify Residential with country GB, which is the most reliable choice and is required for full job details (`fetchDetails`): job pages sit behind a stricter UK edge filter. With the prefilled connection, listing pages and job pages both use Apify Residential GB. Only if you switch the connection to Apify Datacenter are listing pages tried first on Datacenter and moved up to Residential GB when a request is refused; Datacenter was refused in our tests, so do not rely on it for a scheduled run. Apify Residential is available on the Starter plan and above.

If the selected connection is not available to your account, the run switches to a backup connection and logs: "Served over the backup connection (the default connection was unavailable)." Pages may still be refused on the backup, especially job pages. These are the messages you may see when nothing could be read:

- "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required." Upgrade, or check your own proxy URLs if you entered any.
- "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Caterer pages." Upgrade or supply your own proxy URLs, then try again.
- "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search." Try again later.

Use `maxResidentialRequests` to cap how much Residential traffic a run may use.

### FAQ

#### How much does it cost?

You pay per job row returned, plus a small per-run start fee, and an optional per-job surcharge for search-listing rows that were enriched from their job page (`fetchDetails`). Rows from a pasted single job URL, and rows pushed without details when no connection is left for job pages, carry no surcharge. Rows suppressed as unchanged in incremental mode are not billed. The **Pricing** tab shows the current rates. Use **Max jobs** to cap the cost of any run.

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

This actor collects only publicly available job listings. You are responsible for how you use it: follow Caterer.com's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

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

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated jobs, and unchanged ones are not billed. Turn on **Emit expired jobs** only if you also want to know when a tracked job disappears, and note that an `EXPIRED` batch on a large campaign is not capped by **Max jobs**.

#### Why did my run return few or no results?

Check that your filters aren't too narrow, and that the connection settings match the **Plan Requirement** section above. A run that could not read any pages at all fails with a clear message instead of returning an empty dataset, so "no jobs match" is never confused with "nothing could be read".

#### Does this actor collect personal candidate information?

No. It collects job postings, which employers and agencies publish to attract applicants, not candidate or worker data. With **Fetch full job details** on, it also best-effort harvests any contact number or email the posting itself publishes (a `tel:`/`mailto:` link on the job page, or a number or address typed into the description), and it is frequently empty. This is usually a company or agency line, but it can be a named recruiter's direct number or email, which may count as personal data under UK GDPR, so handle these two fields accordingly.

#### 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 travel data?

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

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/totaljobs-com-scraper"><b>Totaljobs Scraper</b></a><br>Scrape UK job listings from Totaljobs.com by keyword, location, filters, or URL. Extract...</td><td>🏨 <a href="https://apify.com/abotapi/rakuten-travel-scraper"><b>Rakuten Travel Scraper</b></a><br>Scrape Rakuten Travel, Japan's largest domestic hotel-booking site, by prefecture...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/wego-com-scraper"><b>Wego Scraper</b></a><br>Scrape Wego hotels by city or search URL. Get rates, providers, booking links...</td><td>🏨 <a href="https://apify.com/abotapi/traveloka-com-scraper"><b>Traveloka Hotel Scraper</b></a><br>Scrape Traveloka hotels across Singapore, Malaysia, Indonesia, Thailand, Vietnam and the...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/hostelworld-scraper"><b>Hostelworld Scraper</b></a><br>Scrape Hostelworld hostels, hotels, guesthouses, B&amp;Bs, apartments and campsites by city...</td><td>🏨 <a href="https://apify.com/abotapi/hipcamp-camping-glamping-scraper"><b>Hipcamp Scraper</b></a><br>Scrape Hipcamp campgrounds, RV parks, glamping and unique stays by US or AU region 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/caterer-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`):

Hospitality role, skill, or employer to search for. Example: 'chef', 'sous chef', 'restaurant manager', 'bartender'. Leave empty to list all hospitality 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. Many hospitality roles quote an hourly rate, so set the period to match. 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.

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

Caterer.com URLs to scrape (URL mode). Accepts listing pages (location, county, keyword, job type, filters, ?page=N) and single job pages. Filter fields above are ignored. A page number in the URL is honoured as the starting point. Left untouched, one example of each URL shape is scraped.

## `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). Values above 1000000 are capped at 1000000.

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

Optional safety limit on pages walked per location/URL (25 jobs per page). Leave at 0 to walk the whole catalogue: the run stops at Max jobs, the site's own last page, or a repeat-page guard, not an artificial page cap.

## `maxResidentialRequests` (type: `integer`):

Safety cap on how many Residential proxy requests this run may use (listing pages and detail pages). 0 = unlimited. When the cap is reached, detail enrichment stops and listing pages continue only on whatever other connection is still available, so a run can never overspend on residential bandwidth.

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

Connection settings. The default is Apify Residential with country GB, which is required for full job details (fetchDetails). With this default, listing pages and job pages both use Residential GB. Only if you switch the connection to Datacenter are listing pages tried on Datacenter first and moved up to Residential GB when refused; Datacenter was refused in our tests, so do not rely on it. Datacenter and non-GB residential are refused on job pages.

## `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. A job is UPDATED when any of its returned fields changes (title, salary, location, employer, description, dates and so on; the changed field names are listed in changedFields). Excluded from the comparison: scrapedAt, detailFetched, crossPostedCount, sourceSearchUrl, searchKeyword, searchLocation and the ad-placement flags isSponsored, isHighlighted, isTopJob, isTrafficFromPartner, plus anything you list in "Ignore fields" below. 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.max or employer.name. Nested fields use dot notation. The fields are still returned in the output, they are just not compared. Already ignored by default: scrapedAt, detailFetched, crossPostedCount, sourceSearchUrl, searchKeyword, searchLocation, isSponsored, isHighlighted, isTopJob, isTrafficFromPartner.

## `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",
  "urls": [
    "https://www.caterer.com/jobs/in-london",
    "https://www.caterer.com/jobs/in-surrey",
    "https://www.caterer.com/jobs/chef/in-london",
    "https://www.caterer.com/jobs/permanent/in-london",
    "https://www.caterer.com/jobs/chef/permanent/in-london?sort=2&salary=30000&salarytypeid=1",
    "https://www.caterer.com/jobs/in-london?page=3",
    "https://www.caterer.com/jobs/chef",
    "https://www.caterer.com/jobs",
    "https://www.caterer.com/jobs/agency/in-london",
    "https://www.caterer.com/jobs/chef/in-london?postedwithin=3",
    "https://www.caterer.com/job/executive-chef-indian-fine-dining-restaurant/little-kitchen-help-ltd-job108217049"
  ],
  "fetchDetails": false,
  "maxListings": 20,
  "maxPages": 0,
  "maxResidentialRequests": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `jobs` (type: `string`):

Individual job records with 35+ fields, plus ~12 detail fields when fetchDetails is enabled.

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

Compact table of jobs.

## `details` (type: `string`):

Detail fields: description text, employment type, contacts, company (fetchDetails).

## `changes` (type: `string`):

Change type, changed fields and first/last seen per job (incrementalMode).

## `output` (type: `string`):

Run summary: total jobs, detail pages fetched, pages walked, residential usage, duration.

# 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.caterer.com/jobs/in-london",
        "https://www.caterer.com/jobs/in-surrey",
        "https://www.caterer.com/jobs/chef/in-london",
        "https://www.caterer.com/jobs/permanent/in-london",
        "https://www.caterer.com/jobs/chef/permanent/in-london?sort=2&salary=30000&salarytypeid=1",
        "https://www.caterer.com/jobs/in-london?page=3",
        "https://www.caterer.com/jobs/chef",
        "https://www.caterer.com/jobs",
        "https://www.caterer.com/jobs/agency/in-london",
        "https://www.caterer.com/jobs/chef/in-london?postedwithin=3",
        "https://www.caterer.com/job/executive-chef-indian-fine-dining-restaurant/little-kitchen-help-ltd-job108217049"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "GB"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/caterer-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.caterer.com/jobs/in-london",
        "https://www.caterer.com/jobs/in-surrey",
        "https://www.caterer.com/jobs/chef/in-london",
        "https://www.caterer.com/jobs/permanent/in-london",
        "https://www.caterer.com/jobs/chef/permanent/in-london?sort=2&salary=30000&salarytypeid=1",
        "https://www.caterer.com/jobs/in-london?page=3",
        "https://www.caterer.com/jobs/chef",
        "https://www.caterer.com/jobs",
        "https://www.caterer.com/jobs/agency/in-london",
        "https://www.caterer.com/jobs/chef/in-london?postedwithin=3",
        "https://www.caterer.com/job/executive-chef-indian-fine-dining-restaurant/little-kitchen-help-ltd-job108217049",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/caterer-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.caterer.com/jobs/in-london",
    "https://www.caterer.com/jobs/in-surrey",
    "https://www.caterer.com/jobs/chef/in-london",
    "https://www.caterer.com/jobs/permanent/in-london",
    "https://www.caterer.com/jobs/chef/permanent/in-london?sort=2&salary=30000&salarytypeid=1",
    "https://www.caterer.com/jobs/in-london?page=3",
    "https://www.caterer.com/jobs/chef",
    "https://www.caterer.com/jobs",
    "https://www.caterer.com/jobs/agency/in-london",
    "https://www.caterer.com/jobs/chef/in-london?postedwithin=3",
    "https://www.caterer.com/job/executive-chef-indian-fine-dining-restaurant/little-kitchen-help-ltd-job108217049"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}' |
apify call abotapi/caterer-com-scraper --silent --output-dataset

```

## MCP server setup

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