# CWjobs UK Tech Jobs Scraper \[Only $1.5] (/w EMAILS) (`memo23/cwjobs-scraper`) Actor

🇬🇧 \[Only $1.5] Scrape every UK tech job on cwjobs.co.uk — JavaScript, DevOps, data, software engineering. Get parsed salary bands (£min–£max), employer names + logos, geo-coords (lat/lng), industries, posting dates. 14 fields per row from JobPosting JSON-LD. Auto-paginates. JSON + CSV.

- **URL**: https://apify.com/memo23/cwjobs-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, Automation, Agents
- **Stats:** 31 total users, 2 monthly users, 98.9% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.50 / 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.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

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

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

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

# README

## CWjobs UK Tech Jobs Scraper

Scrape **UK tech / IT job postings** from **CWjobs.co.uk** — JavaScript developer, DevOps engineer, data engineer, software architect, cybersecurity analyst. Every row carries title, employer + logo, location with lat/lng, **structured salary band** (min / max / currency / period — including contractor day-rates), `datePosted`, `validThrough`, employment type, industry, and a populated `applyUrl` when the role is direct-apply. One flat row per job from rich `JobPosting` JSON-LD plus structured salary metadata.

![How CWjobs Scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-cwjobs.png)

### Why this actor

CWjobs.co.uk is the UK's dominant tech-vertical job board — IT, software engineering, DevOps, data, and cybersecurity roles. ~6,500 live London tech postings at any time, server-side scoped to the tech vertical so you get exactly what CWjobs.co.uk's web UI shows.

- **Tech-only result set** — the actor's request is scoped to IT/tech roles (software, DevOps, data, cybersecurity, IT support) so you don't pay for irrelevant general-UK rows
- **Efficient JSON listing pagination** at 50 items per page, with `totalItems` reported up-front so we know exactly when we've reached the end
- **Server-side filters** — `postedWithin` (1 / 3 / 7 / 14 days), `companyTypes` (direct employer / agency), `salary` (minimum), and `jobType` (permanent / contract / temporary / part-time / work-from-home) all honoured server-side
- **Apify Residential GB** for the per-job detail fetch — the only proxy pool that consistently returns 200 OK on UK job-board detail pages
- **JobPosting JSON-LD parsing** — every row carries the full 14-field JSON-LD detail PLUS structured salary metadata. Tech contractor day-rates (£500–£900/day typical) and permanent annual salaries (£40k–£120k typical) are both captured cleanly with the structured `period` field
- **`applyUrl` populated** when `directApply: true` (≈ 60–70 % of jobs) — the candidate-facing apply URL, not just the search-page URL
- **Defence in depth** — built-in upstream-credential rotation safeguards keep runs flowing even during brief upstream auth hiccups
- **Mixed input** — listing URLs auto-paginate + emit one row per detail; direct detail URLs scrape one row each

### Use cases

- **Tech recruitment market intelligence** — salary benchmarking for software, DevOps, data, and cybersecurity roles across UK regions
- **Sales prospecting for B2B SaaS** — find tech companies hiring in your target verticals + locations, with employer details + Apify-extracted geo-coordinates
- **HR competitive analysis** — compare your engineering / data salary bands against UK tech market signal
- **Contractor market analysis** — day-rate trends for software contractors (`salary.period === "day"` filtering surfaces this cleanly)
- **ATS / job-aggregator integration** — clean structured input for downstream pipelines
- **Geospatial analytics** — every row carries `location.lat` + `location.lng` from JSON-LD `PostalAddress.geo`

### Input

| Field | Type | Required | Notes |
|---|---|---|---|
| `startUrls` | `string[]` | yes | Mix of listing URLs (`https://www.cwjobs.co.uk/jobs/javascript`, `/jobs/devops/in-london`, `/jobs/in-remote`) and direct detail URLs. Filters supported in the query string: `?postedWithin=1\|3\|7\|14`, `?companytypes=1\|2`, `?salary={int}`, `?jobType=permanent\|contract\|temporary\|partTime\|workFromHome`. |
| `maxItems` | `integer` | no | Maximum job rows emitted **per listing URL**. Each row = one paid dataset item. Default `1000`. Free-tier users have a hidden global ceiling of `100` rows. |
| `maxConcurrency` | `integer` | no | Parallel HTTP requests for detail-page fetches. Sweet spot 3–5 via Apify Residential GB. Default `4`. |
| `maxRequestRetries` | `integer` | no | Per-URL retry budget on proxy CONNECT failures, HTTP/2 stream resets, and network errors. Default `6`. |
| `proxy` | object | no | **Apify Residential GB required** for the detail-page fetch. Default is wired correctly — don't override unless you know what you're doing. |

#### Example input

```json
{
  "startUrls": [
    "https://www.cwjobs.co.uk/jobs/javascript/in-london",
    "https://www.cwjobs.co.uk/jobs/devops/in-manchester",
    "https://www.cwjobs.co.uk/jobs/in-remote?postedWithin=7&salary=60000"
  ],
  "maxItems": 200,
  "maxConcurrency": 4,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

### Output schema

Every row has `rowType: "job"`. **14 fields from JSON-LD + structured salary band (annual or day-rate for contractors) + lat/lng + applyUrl.**

```jsonc
{
  "rowType":           "job",
  "sourceSearchUrl":   "https://www.cwjobs.co.uk/jobs/javascript/in-london",
  "jobId":             "107380088",
  "jobUrl":            "https://www.cwjobs.co.uk/job/product-consultant-ii-insurance/verisk-analytics-job107380088",
  "sourceSite":        "cwjobs.co.uk",
  "title":             "Product Consultant II (Insurance)",

  // ── JobPosting JSON-LD ──
  "description":       "<p>Verisk Analytics is looking for a Product Consultant…</p>",
  "datePosted":        "2026-05-20T09:14:32.000Z",
  "validThrough":      "2026-06-30T09:14:32.000Z",
  "employmentType":    "CONTRACTOR",
  "industry":          "Insurance, Insurance-Information Technology",
  "directApply":       true,
  "jobLocationType":   "TELECOMMUTE",
  "applicantLocationRequirements": ["United Kingdom"],

  // ── Employer ──
  "employer": {
    "name":            "Verisk Analytics",
    "url":             "https://www.cwjobs.co.uk/jobs/verisk-analytics?cmpId=%E2%80%A6",
    "logoUrl":         "https://www.cwjobs.co.uk/CompanyLogos/%E2%80%A6verisk.png"
  },

  // ── Location ──
  "location": {
    "text":            "London, GB",
    "locality":        "London",
    "region":          "London",
    "postalCode":      null,
    "country":         "GB",
    "lat":             51.50735,
    "lng":             -0.12776
  },

  // ── Salary (often DAY rates for contractors, ANNUAL for permanent) ──
  "salary": {
    "rawText":         "£650 - £700 per day",
    "min":             650,
    "max":             700,
    "currency":        "GBP",
    "period":          "day"
  },

  // ── Apply flow ──
  "applyUrl":          "https://www.cwjobs.co.uk/job/product-consultant-ii-insurance/verisk-analytics-job107380088",
  "applyType":         "internal",

  "scrapedAt":         "2026-05-21T20:25:31.012Z"
}
```

### Apply flow — what `applyUrl` and `applyType` mean

| `directApply` | `applyType` | `applyUrl` | Meaning |
|---|---|---|---|
| `true` | `"internal"` | **= `jobUrl`** | CWjobs hosts a one-click apply form on the job page itself. |
| `false` | `"external"` | `null` | External recruiter ATS (e.g. Greenhouse, Workday, the company's own careers portal). Open `jobUrl` in a browser and click *Apply* to be redirected. |
| missing | `"unknown"` | `null` | JSON-LD didn't surface the flag. Treat the same as `"external"`. |

**Practical rule of thumb:** ≈ 60–70 % of CWjobs listings are `directApply: true`.

### Notes & limitations

- **Day-rates are common for contractor roles.** UK tech contracting is rate-per-day (£500–£900/day typical for senior software engineering contracts). The structured `salary.period: "day"` field captures that — use it to separate contractor day-rates from permanent annual salaries in analysis.
- **`employmentType` fill is higher than average** (≈ 80%) because UK tech has clearer permanent-vs-contract distinctions.
- **`jobLocationType: "TELECOMMUTE"` is common** for tech roles (remote / hybrid working is mainstream in IT).
- **Apify Residential GB is mandatory** for the per-job detail fetch.
- **No radius-based filter.** Use a tighter `where=` location instead.
- **No maximum-salary filter.** Only minimum (`?salary=N`).
- **CWjobs editorial mix is broader than pure software.** Their definition of "tech" includes IT operations, industrial automation, analytics, insurance-tech, and adjacent engineering. Expect some "Instrument Control Automation Technician" or "Product Consultant" alongside the pure software roles.

### FAQ

**Which CWjobs URLs work?**
Two types: **listing URLs** (`/jobs/javascript`, `/jobs/devops/in-london`, `/jobs/in-remote`, `/jobs/{company-slug}-jobs`) which auto-paginate, and **direct detail URLs** (`/job/{title-slug}/{org-slug}-job{id}`) which scrape one row each.

**Why is my salary £650 instead of £65,000?**
Tech contractor jobs are paid by day. The `salary.period` field is the source of truth: `"day"` = daily rate (contractor), `"annum"` = annual salary (permanent), `"hour"` = hourly (uncommon in tech, more usual in support roles). The `min`/`max` numbers are in the unit indicated by `period`. For a clean salary analysis, group by `period` first.

**Are remote / hybrid jobs included?**
Yes. UK tech is heavily remote-friendly. Use `?jobType=workFromHome` to filter to remote-only roles, or check the `jobLocationType === "TELECOMMUTE"` field on each row.

**What does each dataset-item charge cover?**
One tech job row with all 14 JSON-LD fields plus the structured salary band (with `period` correctly identifying day-rate vs annual) and structured location (lat/lng). `maxItems` is per-listing-URL.

### Support

- **Bugs / feature requests** — open an issue on the GitHub repo
- **Custom exports / tailored fields** — drop a note via the Apify Store contact form
- **Other actors** — see my [Apify Store profile](https://apify.com/memo23)

***

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (actor: `memo23/cwjobs-scraper`).

**Purpose:** Extract UK tech / IT job postings from CWjobs.co.uk — one flat row per job from `JobPosting` JSON-LD plus a structured salary band (annual or contractor day-rate), lat/lng, and a populated `applyUrl` for direct-apply roles.

**Minimal input:**

```json
{
  "startUrls": ["https://www.cwjobs.co.uk/jobs/in-london"],
  "maxItems": 25,
  "maxConcurrency": 4,
  "maxRequestRetries": 6,
  "enrichEmails": false,
  "qualifyByPayment": false,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

**Output:** one row per job — rowType, sourceSearchUrl, jobId, jobUrl, sourceSite, title, description, datePosted, validThrough, employmentType, industry, directApply, jobLocationType, applicantLocationRequirements, employer {name, url, logoUrl}, location {text, locality, region, postalCode, country, lat, lng}, salary {rawText, min, max, currency, period}, applyUrl, applyType, scrapedAt.

**Behaviors an agent should know:**

- `startUrls` mixes listing URLs (auto-paginate) and direct detail URLs (one row each). Query filters honored server-side: `?postedWithin=1|3|7|14`, `?companytypes=1|2`, `?salary={int}`, `?jobType=permanent|contract|temporary|partTime|workFromHome`.
- `maxItems` is **per listing URL**; default `1000`. Free-tier users are capped at 100 total rows.
- `salary.period` distinguishes contractor day-rates (`"day"`) from permanent annual pay (`"annum"`) — group by it before analysis.
- Apify Residential GB is required for the per-job detail fetch; keep `maxConcurrency` at 3–5.
- `enrichEmails` (opt-in) adds employer contact-email fields, billed per email found (never for misses); `qualifyByPayment` requires it and adds payment-stack fields at no extra charge.
- Each job row is one paid dataset item (pay-per-result); see the Pricing tab on the actor page.

***

### ⚠️ Disclaimer

This Actor is an independent tool and is **not affiliated with, endorsed by, or sponsored by** CWjobs.co.uk or any of its parent / sister companies. All trademarks mentioned are the property of their respective owners.

The scraper extracts only **publicly visible** tech job postings from CWjobs.co.uk — no login, no CAPTCHA solving, no credential forgery. The actor uses a conservative concurrency cap (default 4 parallel requests) to avoid burdening CWjobs's infrastructure.

Users are responsible for:

- Complying with CWjobs.co.uk's Terms of Service
- Following UK GDPR + your jurisdiction's data-protection laws when storing or processing scraped postings
- Not contacting candidates listed by employers in scraped postings
- Not republishing scraped data in a way that competes commercially with CWjobs

***

### SEO Keywords

cwjobs scraper, cwjobs.co.uk scraper, scrape cwjobs, uk tech jobs scraper, it jobs scraper uk, software engineer jobs scraper, javascript jobs scraper, devops jobs scraper, data engineer jobs scraper, cybersecurity jobs scraper, uk tech salary data, contractor day rate data uk, uk it recruitment data, tech recruitment scraper, uk tech market intelligence, uk it salary benchmark, software contractor rates uk, remote tech jobs scraper, london tech jobs, manchester tech jobs, edinburgh tech jobs, uk it employer data, b2b sales prospecting tech

# Actor input Schema

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

Mix of listing URLs (e.g. `https://www.cwjobs.co.uk/jobs/in-london`, `https://www.cwjobs.co.uk/jobs/javascript-developer/in-london`) and direct job-detail URLs. Filters supported in the query string: `?postedWithin=1|3|7|14`, `?companytypes=1|2`, `?salary={int}`, `?jobType=permanent|contract|temporary|partTime|workFromHome`. Listings auto-paginate; details emit one row each. Result set is scoped server-side to the IT/tech vertical — exactly what cwjobs.co.uk's web search shows. Both plain URL strings and the request-list object form (`{"url": "https://.../"}`) are accepted.

## `enrichEmails` (type: `boolean`):

If enabled, tries to find a contact email for each employer by discovering the company's website (Clearbit) and reading its contact/about pages. Adds contactEmail + contactWebsite columns plus a detailed emailEnrichment object. Best-effort, billed per contact email found; only charged when an email is returned, never for misses.

## `qualifyByPayment` (type: `boolean`):

Requires "Enrich with contact emails". Scans each business's website — reusing the pages already fetched for email discovery, so no extra cost or time — for payment processors and e-commerce platforms (Stripe, Shopify, PayPal, Paddle, Lemon Squeezy, WooCommerce, Square, Chargebee and more). Adds takesPayments (is this a real paying business?), paymentProcessors (which stack), stripeLiveKey (the public key if exposed) and paymentConfidence. Turn raw contacts into monetization-qualified leads. No extra charge — included with each enriched company.

## `maxItems` (type: `integer`):

Maximum job rows emitted **per listing URL**. Pass 3 listings with `maxItems: 100` → up to 300 total rows (100 each). Direct detail URLs always emit 1 row each. Each row is one paid dataset item. Default 1000. Free-tier users are additionally capped at 100 total rows.

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

Parallel HTTP requests for detail-page fetches. Concurrency 3-5 is the sweet spot via Apify Residential GB.

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

Per-URL retry budget on network errors, proxy CONNECT failures, HTTP/2 stream resets, and transient 4xx responses. Each retry rotates the proxy session. Default 6.

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

Leave empty: paying users are routed through our own residential pool automatically. On a free plan the run uses your own Apify Proxy — set a Residential GB configuration here if your plan includes it, since CWJobs job pages are Akamai-protected.

## Actor input object example

```json
{
  "startUrls": [
    "https://www.cwjobs.co.uk/jobs/in-london"
  ],
  "enrichEmails": false,
  "qualifyByPayment": false,
  "maxItems": 1000,
  "maxConcurrency": 4,
  "maxRequestRetries": 6,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}
```

# 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 = {
    "startUrls": [
        "https://www.cwjobs.co.uk/jobs/in-london"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "GB"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/cwjobs-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 = {
    "startUrls": ["https://www.cwjobs.co.uk/jobs/in-london"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/cwjobs-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 '{
  "startUrls": [
    "https://www.cwjobs.co.uk/jobs/in-london"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "GB"
  }
}' |
apify call memo23/cwjobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/cwjobs-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/R2qiJWdAZ4CMaKqF5/builds/aQ9pQTLwMoicWDKRa/openapi.json
