# CV-Library Scraper \[$1/1K💰] | UK Jobs | Salary Data (`ahmed_jasarevic/cv-library-co-uk-scraper`) Actor

Scrape UK job listings from cv-library.co.uk: titles, companies, locations + geo-coordinates, parsed salaries, contract types, dates, full descriptions and apply URLs (up to 10,000 jobs/run). Automatic Cloudflare warm-up. Pay-per-event.

- **URL**: https://apify.com/ahmed_jasarevic/cv-library-co-uk-scraper.md
- **Developed by:** [Ahmed Jasarevic](https://apify.com/ahmed_jasarevic) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

## Cv Library Co Uk Scraper

Scrape UK job listings from cv-library.co.uk (one of the UK's largest job boards) as clean structured JSON: job title, company + advertiser type, location with geo-coordinates, parsed salary (min/max/period), contract type, posted date, description and apply URL. Up to **10,000 jobs per run**. Built for UK recruiters, staffing agencies, salary-benchmarking tools and labour-market researchers who need CV Library job data without an API key.

A plain-HTTP `CheerioCrawler` (no browser for the crawl) that reads the search backend records the site embeds in its pages as Next.js flight data, with an automatic UK-residential warm-up that solves the Cloudflare challenge.

### Main Use Cases

- **UK job market data and salary benchmarking** - parsed `salaryMin`/`salaryMax`/`salaryPeriod` across keywords, locations and distances
- **Recruitment lead generation** - companies, advertiser type (Recruitment Agency vs Direct Employer), locations and geo centroids
- **Job board and job feed APIs** - up to 10,000 jobs per run as clean JSON rows
- **Hiring trend tracking** - scheduled runs diffing `postedDateISO` and `expiryDate` across the UK
- **Candidate sourcing and market analysis** - city/region/postcode/country plus industry and contract type per role

### How It Works - The Bypass

**CheerioCrawler (plain HTTP, no browser)** with a single-stage crawl by default:

1. **Search pages** (`https://www.cv-library.co.uk/<keywords>-jobs[-in-<location>]`, 100 results per page, paginated via `?page_number=N`) - the primary structured source is the site's **own search-backend records embedded in the page as Next.js flight data** (`self.__next_f.push(...)` scripts carrying `{"data": {"meta": {total, limit, offset}, "results": [...]}}`). Each record carries id, title, full `account` (id, name, type - exactly "Recruitment Agency"/"Direct Employer"), `location` (name, city, county, postcode-style display, `centroid` lat/lon), `salary` ({currency, period, min, max}), `contract`, ISO `posted` date, `category`, `flags` (featured), `logo`, apply URLs and the description (HTML). No DOM scraping needed for any of these - **100 jobs per request**.
2. **Job detail pages** (`/job/{id}/{slug}`, optional with `includeDescription: true`) - add the full description, expiry date, contract length and direct-apply flag from the **`application/ld+json` JobPosting block**, with stable `data-qa` hooks as fallback. This costs **one request per job**, is paced (`maxConcurrency: 1`, ~1.2 s between requests) and is off by default because the site's WAF rate-limits datacenter IPs. If a detail page fails, the backend record is still pushed, so no listing is ever lost.

#### Why The Egress IP Matters (verified)

cv-library.co.uk sits behind a Cloudflare WAF that returns **HTTP 403 with a large block page** for unfamiliar IPs and request bursts. Verified tests on the same day:

- Node.js + got-scraping (Chrome-shaped TLS via the UA's ciphers/curves, HTTP/2) -> **403**.
- `impit` with a genuine Chrome TLS/HTTP2/QUIC fingerprint (JA3/JA4, header order) -> **still 403**. A real Chrome handshake from a datacenter IP is enough to fail the WAF; a perfect fingerprint on a blacklisted IP does not help.
- **UK residential IPs** -> **0 x 403**, ~7 s for a 10-job run.

So the fingerprint is already as good as it gets for free; the only lever that removes the 403s is the egress IP **plus** a real browser warm-up. **This actor bakes that in automatically**: at run start a real Chromium is launched once through a UK residential sticky session (real Chrome UA + TLS), which solves the Cloudflare challenge and "warms" that exit IP; then the CheerioCrawler scrapes fast on the same IP + User-Agent (`cf_clearance` is bound to the IP + UA pair, so both are reused). Tested: **0 x 403, 60 jobs in ~7 s**, versus 403 on every direct/datacenter request. Residential traffic is billed through Apify's normal residential proxy accounting (~$8 per GB - a list-mode run is only a few MB). A 1,000-job detail-mode run needs ~20 minutes and is the most likely mode to hit 403s (fail-safe: falls back to the list-level record, never loses a listing).

There is **no user-facing proxy or anti-block input** - the hybrid residential mode is fixed and automatic in the current version.

#### Which Bypass Was Used, And Why

| Candidate surface | Verdict |
| --- | --- |
| Official `search-jobs-json` / partner Job View API | Not usable - requires a partner API key (`key=KEY`, 400 without it); documented at cv-library.co.uk/developers but not available without approval |
| Algolia / Meilisearch / other third-party search backend | None found - first-party search index queried as `/jobs?class=!organic&facet=...&keyword=...&limit=...`, Cloudflare-walled for direct calls |
| **Embedded Next.js flight data with full backend records** | **Used (default)** - same records the `/jobs` backend returns, with salary/geo/company/posted/description, parsed server-side, no browser, no DOM scraping |
| Embedded `application/ld+json` JobPosting | Optional (`includeDescription: true`) - adds full description HTML, `validThrough` expiry, employmentType, contract length |
| Static search HTML via CheerioCrawler | Used as transport + fallback (DOM cards only if flight data is ever absent) |
| Playwright / browser rendering | Not needed for the crawl - all target fields exist in served HTML/embedded JSON (the browser is used only for the one-shot Cloudflare warm-up) |

### Input Parameters

| Field | Type | Required | Default | Notes |
| --- | --- | --- | --- | --- |
| `searchQuery` | string | No | `software developer` | Job title, keywords or skills; empty = browse all jobs |
| `keywords` | array | No | `[]` | Optional list of queries; each runs its own search and shares the `maxResults` budget |
| `location` | string | No | empty | City, town, postcode or county, e.g. `London`, `M1 1AA`, `West Yorkshire` |
| `distance` | integer | No | 15 | Search radius in miles (0-750); only applies when a location is set |
| `contractType` | string | No | `any` | `any` / `permanent` / `contract` / `temporary` / `part-time` / `apprenticeship` |
| `daysOld` | integer | No | 14 | Only listings posted within this many days; CV-Library supports 1 / 3 / 7 / 14-day bands, other values round up |
| `salaryMin` | integer | No | 20000 | Minimum annual salary, GBP |
| `salaryMax` | integer | No | 100000 | Maximum annual salary, GBP |
| `startUrls` | array | No | `[]` | CV-Library search-result or job URLs to scrape directly instead of building a search |
| `maxResults` | integer | No | 100 | Max jobs to return (1-10000); free users get a 10-job preview |
| `maxPages` | integer | No | 50 | Max search pages per search (1-500, 100 jobs per page) |
| `includeDescription` | boolean | No | false | OFF (default): list-level data only - 1 request per 100 jobs, fastest and least likely to be blocked. ON: open each job page (one request per job, paced, slower, more 403-prone; adds full description, expiry, contract length, direct-apply flag) |

### Output Fields

One dataset row per job (full field list in the dataset schema; fields the listing does not publish are `null`, never invented):

- **Identity**: `id`, `url`, `applyUrl` (job URL + `/apply`), `title`, `source`, `isFeatured`, `directApply` (detail mode only)
- **Company**: `company`, `companyId`, `companyUrl`, `companyLogo`, `advertiserType` (Recruitment Agency / Direct Employer heuristic)
- **Location**: `location`, `city`, `region`, `postcode`, `country`, `latitude`, `longitude` (geo centroid)
- **Compensation**: `salaryRaw`, `salaryMin`, `salaryMax`, `salaryCurrency` (GBP), `salaryPeriod` (YEAR/MONTH/WEEK/DAY/HOUR)
- **Employment**: `contractType`, `contractLength` (detail mode), `employmentType` (detail mode), `industry`
- **Dates**: `postedText`, `postedDate`, `postedDateISO`, `expiryDate` (detail mode), `scrapedAt`
- **Description**: `descriptionHtml`, `descriptionText` (list mode: from the search payload; detail mode: full page)

### Example Input And Output

#### Example Input

```json
{
  "searchQuery": "care assistant",
  "location": "Leeds",
  "distance": 15,
  "contractType": "permanent",
  "maxResults": 200,
  "includeDescription": false
}
```

#### Example Output

```json
{
  "id": "3494812",
  "url": "https://www.cv-library.co.uk/job/3494812/care-assistant-leeds",
  "applyUrl": "https://www.cv-library.co.uk/job/3494812/care-assistant-leeds/apply",
  "title": "Care Assistant - Days",
  "company": "Example Care Homes",
  "companyId": "100234",
  "advertiserType": "Recruitment Agency",
  "location": "Leeds, West Yorkshire",
  "city": "Leeds",
  "region": "West Yorkshire",
  "postcode": "LS1 4AP",
  "country": "GB",
  "latitude": 53.801,
  "longitude": -1.548,
  "salaryRaw": "GBP 22,000 - 26,000 per annum",
  "salaryMin": 22000,
  "salaryMax": 26000,
  "salaryCurrency": "GBP",
  "salaryPeriod": "YEAR",
  "contractType": "Permanent",
  "industry": "Healthcare",
  "postedText": "2 days ago",
  "postedDate": "2026-10-08",
  "postedDateISO": "2026-10-08T08:00:00Z",
  "isFeatured": false,
  "descriptionText": "We are looking for a Care Assistant...",
  "source": "cv-library.co.uk",
  "scrapedAt": "2026-10-10T10:00:00.000Z"
}
```

### Integrations & Automation

- **Apify API** - run via `POST https://api.apify.com/v2/acts/{actorId}/runs`, export via `GET /v2/datasets/{datasetId}/items?clean=true&format=json`
- **Schedules** - Apify Schedules for **daily UK job post refreshes** (recommended for hiring-trend and market tracking; recurring usage improves Store recommendation signals)
- **Webhooks** - new-job events into downstream pipelines
- **Zapier / Make** - via the Apify integration (e.g. new jobs to Google Sheets/Airtable)
- **Pay-per-event** - $1.00 per 1,000 jobs list mode, $2.40 per 1,000 with full details (`result` + `detail` events); free 10-job preview

### Related Actors

- [linkedin-jobs-scraper](https://apify.com/curious_coder/linkedin-jobs-scraper): the largest job-board scraper on the Store; pairs CV-Library data with LinkedIn reach.
- [indeed-scraper](https://apify.com/borderline/indeed-scraper): one of the most-used Indeed scrapers for cross-board UK job coverage.
- [cv-library-co-uk-scraper](https://apify.com/solidcode/cv-library-co-uk-scraper): the most-used competing CV-Library actor, for comparison.
- [totaljobs-scraper](https://apify.com/ahmed_jasarevic/totaljobs-scraper): another major UK job board (Totaljobs) with salary and geo data in the same schema style.
- [jobs-workable-scraper](https://apify.com/ahmed_jasarevic/jobs-workable-scraper): ATS-hosted job postings via clean public JSON APIs.

### FAQ

#### Does CV-Library have an API?

An official partner Job View API (`search-jobs-json`) exists at cv-library.co.uk/developers but requires a partner API key (returns 400 without it) and is not publicly available. This actor instead reads the site's **own search-backend records embedded in the pages as Next.js flight data** - the same records the `/jobs` backend returns, parsed server-side.

#### How do I scrape CV-Library without getting blocked?

You cannot fully avoid the Cloudflare WAF from a datacenter IP - verified tests show even a genuine Chrome TLS fingerprint still gets 403. This actor solves it for you: at run start a real Chromium warms up the Cloudflare challenge once on a UK residential sticky session, then the CheerioCrawler reuses that same IP + UA. Tested: 0 x 403, 60 jobs in ~7 s. Keep `includeDescription` OFF for the least 403-prone mode (1 request per 100 jobs).

#### What are the alternatives for UK job data?

Store alternatives: solidcode/cv-library-co-uk-scraper (most-used, $4.5 per 1K), blackfalcondata/cv-library-scraper ($1.5 per 1K), parseforge ($20.33 per 1K) and unfenced-group ($2 per 1K). This actor is the verified cheapest full-field option at $1.00-2.40 per 1,000. It pairs with own actors totaljobs-scraper and jobs-workable-scraper for multi-board UK coverage.

#### Why do I get 403 responses occasionally?

The WAF intermittently blocks datacenter IPs even with the warm-up, and `includeDescription: true` bursts detail-page requests, which is the most likely mode to hit 403s. Fail-safes: a search page backs off 30/60/60 s between attempts then stops with a clear "rate-limited" log; detail pages give up after 2 attempts and fall back to the list-level record; cookie continuity (`__cf_bm`) is enforced so the clearance survives across requests.

#### What salary data does CV-Library publish?

`salaryRaw` (exact text), `salaryMin`/`salaryMax` (parsed numbers) and `salaryPeriod` (YEAR/MONTH/WEEK/DAY/HOUR) in GBP - but only when the employer publishes a salary; otherwise fields are `null`.

#### How many jobs can one run return?

Up to 10,000 jobs (`maxResults` cap). Free Apify users get a 10-job preview per run; paid runs are unlimited up to `maxResults`. Multiple `keywords` share the budget across searches.

#### How much does a run cost?

List mode: $1.00 per 1,000 jobs. With `includeDescription: true` (full details): $2.40 per 1,000 (`result` + `detail` events). Residential warm-up traffic is billed via Apify's residential accounting (~$8 per GB - a list-mode run is only a few MB). Detail mode also costs ~0.7 s pacing per job, so a 1,000-job detail run is ~20 minutes.

### SEO Keywords

cv library scraper, cv-library api, uk job scraper, uk jobs api, scrape cv-library, cv library jobs data, uk job listings data, job board scraper, recruitment lead generation, salary benchmarking, uk labour market data, hiring data uk, cv library salary data, job posts monitoring, uk jobs csv, recruitment agencies data, cv library alternative, job hire trends uk, uk job market analysis, candidate sourcing data

### For AI Agents & LLM Apps

- **Purpose**: returns one JSON row per UK job listing from cv-library.co.uk (title, company, advertiser type, location + geo, parsed salary, contract type, dates, description, apply URL).
- **Minimal working input**:

```json
{ "searchQuery": "software developer", "location": "London", "maxResults": 10, "includeDescription": false }
```

- **Variant input** (full details - only when `includeDescription: true`):

```json
{ "searchQuery": "care assistant", "maxResults": 50, "includeDescription": true }
```

- **Output field names**: `id, url, applyUrl, title, company, companyId, companyUrl, companyLogo, advertiserType, location, city, region, postcode, country, latitude, longitude, salaryRaw, salaryMin, salaryMax, salaryCurrency, salaryPeriod, contractType, contractLength, employmentType, industry, postedText, postedDate, postedDateISO, expiryDate, directApply, isFeatured, descriptionHtml, descriptionText, source, scrapedAt`.
- **Behaviors an agent should know**:
  - `includeDescription` (default false) is the cost lever: OFF = 1 request per 100 jobs, rows carry list-level description only, `null` for `contractLength`/`expiryDate`/`directApply`/`employmentType`. ON = one paced request per job and an extra `detail` pay-per-event charge; rows then carry the full page. Only turn it on when the full description matters.
  - `keywords` and `searchQuery` combine: each keyword runs its own search and they all share the `maxResults` budget. `startUrls` bypass search construction entirely.
  - `daysOld` rounds to CV-Library's 1/3/7/14-day bands; `distance` (0-750 miles) only applies when `location` is set.
  - `salaryMin`/`salaryMax` are annual GBP filters; salary fields are `null` when the employer does not publish them - never invent a salary.
  - `advertiserType` is a text heuristic, not an official field.
  - A 403 in flight is handled internally (fallback to the list-level record); do not re-request failed detail rows - the record is already in the dataset.
  - `maxPages` (default 50) is per search, not per run.
- **Billing**: pay-per-event - $0.001 per job row (list mode), +$0.0014 per job with `includeDescription` (detail event), $0.00005 per run start. Residential warm-up traffic billed additionally via Apify residential accounting.

### Legal & Compliance Disclaimer

This actor is an independent tool and is not affiliated with, endorsed by, or sponsored by CV-Library Ltd (cv-library.co.uk). It accesses only publicly available job listings and public job pages; it does not bypass login, authentication or paywalls, and it uses a standard browser warm-up only to pass the site's public-facing Cloudflare challenge on a residential IP - it does not circumvent access controls.

Users are responsible for complying with cv-library.co.uk's terms of service, robots.txt and applicable data-protection law (UK GDPR applies to candidate and recruiter data). Use the data for lawful purposes such as market research, salary benchmarking and compliant recruitment workflows, and do not use extracted personal data for unsolicited outreach.

# Actor input Schema

## `searchQuery` (type: `string`):

Job title, keywords or skills to search for, e.g. "software developer". Leave empty to browse all jobs.

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

Optional list of queries; each runs its own search and shares the maxResults budget.

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

City, town, postcode or county, e.g. "London", "M1 1AA", "West Yorkshire".

## `distance` (type: `integer`):

Search radius around the location, in miles. Only applies when a location is set.

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

Filter by contract type.

## `daysOld` (type: `integer`):

Only return listings posted within this many days. CV-Library supports 1, 3, 7 or 14 days; other values round up to the nearest band.

## `salaryMin` (type: `integer`):

Only return jobs with an annual salary at or above this amount, in GBP.

## `salaryMax` (type: `integer`):

Only return jobs with an annual salary at or below this amount, in GBP.

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

Optional CV-Library search-result or job URLs to scrape directly instead of building a search from the fields above.

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

Maximum number of jobs to return (up to 10,000 per run). Free users get a 10-job preview.

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

Maximum search-result pages to paginate through per search (100 jobs per page).

## `includeDescription` (type: `boolean`):

OFF (default): list-level data only â€” 1 request per 100 jobs, fastest and least likely to be blocked. ON: open each job page for the full description HTML, contract length, expiry date and direct-apply flag (one request per job, paced slowly, slower and more likely to hit 403s).

## Actor input object example

```json
{
  "searchQuery": "software developer",
  "keywords": [],
  "distance": 15,
  "contractType": "any",
  "daysOld": 14,
  "salaryMin": 20000,
  "salaryMax": 100000,
  "startUrls": [],
  "maxResults": 100,
  "maxPages": 50,
  "includeDescription": false
}
```

# Actor output Schema

## `results` (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 = {
    "searchQuery": "software developer",
    "keywords": [],
    "location": "",
    "distance": 15,
    "daysOld": 14,
    "salaryMin": 20000,
    "salaryMax": 100000,
    "startUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/cv-library-co-uk-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 = {
    "searchQuery": "software developer",
    "keywords": [],
    "location": "",
    "distance": 15,
    "daysOld": 14,
    "salaryMin": 20000,
    "salaryMax": 100000,
    "startUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/cv-library-co-uk-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 '{
  "searchQuery": "software developer",
  "keywords": [],
  "location": "",
  "distance": 15,
  "daysOld": 14,
  "salaryMin": 20000,
  "salaryMax": 100000,
  "startUrls": []
}' |
apify call ahmed_jasarevic/cv-library-co-uk-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ahmed_jasarevic/cv-library-co-uk-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/sHcHvgeVt1XfeXI5N/builds/vzUWPOAMg6h8U6jw1/openapi.json
