TotalJobs UK [only $2/1k] Scraper · Jobs/Salary/Geo (/w EMAILS)
Pricing
from $2.00 / 1,000 results
TotalJobs UK [only $2/1k] Scraper · Jobs/Salary/Geo (/w EMAILS)
[only $2] Map the UK job market from TotalJobs.com — 44,490+ postings across 1,780+ listing pages. Each row carries title, employer + logo, location + lat/lng, parsed salary band (min/max/currency/period), dates, and employment type. One outputrecord per job.
Pricing
from $2.00 / 1,000 results
Rating
5.0
(1)
Developer
Muhamed Didovic
Maintained by CommunityActor stats
0
Bookmarked
13
Total users
12
Monthly active users
a day ago
Last modified
Categories
Share
TotalJobs UK Scraper
Scrape job postings from TotalJobs.com (UK) — title, employer + logo, location with lat/lng, parsed salary band (min / max / currency / period), datePosted, validThrough, employment type, industry, and directApply flag. One flat row per job from rich JobPosting JSON-LD.

Why this actor
TotalJobs has 119,000+ live UK job postings — one of the largest UK job boards by inventory. This actor delivers clean structured rows fast:
- Efficient JSON listing pagination at 50 items per page, with
totalItemsreported up-front so we know exactly when we've reached the end of a result set - Server-side filters —
postedWithin(1 / 3 / 7 / 14 days),companyTypes(direct employer / agency),salary(minimum), andjobType(permanent / contract / temporary / part-time / work-from-home) all honoured server-side, identical to the web page - 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 (min/max/currency/period) so you don't have to regex it client-side
applyUrlpopulated whendirectApply: true(≈ 60-70 % of jobs) — the candidate-facing apply URL, not just the search-page URL- Mixed input — listing URLs auto-paginate + emit one row per detail; direct detail URLs scrape one row each
Use cases
- Recruitment market intelligence — salary benchmarking by region/role, employer activity tracking
- Sales prospecting — find companies hiring in your target verticals + locations
- HR competitive analysis — compare your salary bands against TotalJobs market signal
- ATS / job-aggregator integration — clean structured input for downstream pipelines
- Geospatial analytics — every row carries
location.lat+location.lngfrom JSON-LDPostalAddress.geo
Input
| Field | Type | Required | Notes |
|---|---|---|---|
startUrls | string[] | yes | Mix of listing URLs (https://www.totaljobs.com/jobs/in-london, /jobs/{keyword}/in-{location}) and direct detail URLs (/job/{title-slug}/{org-slug}-job{id}). 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. 3 listings × maxItems: 100 → up to 300 total rows. Direct detail URLs always emit 1 row each. 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. Each retry rotates the proxy session with mild exponential backoff. Default 6. |
proxy | object | no | Apify Residential GB required for the detail-page fetch (/job-ad/{id}). Default is wired correctly — don't override unless you know what you're doing. |
Example input
{"startUrls": ["https://www.totaljobs.com/jobs/software-engineer/in-london","https://www.totaljobs.com/jobs/in-manchester"],"maxItems": 200,"maxConcurrency": 4,"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }}
Output schema
Every row has rowType: "job". 14 fields from JSON-LD + parsed salary band + structured location.
{"rowType": "job","sourceSearchUrl": "https://www.totaljobs.com/jobs/in-london", // the search/listing page this row came from (renamed from listingUrl in v0.1)"jobId": "107245246", // numeric — stable identifier"jobUrl": "https://www.totaljobs.com/job/regional-optimization-lead/bp-energy-job107245246", // canonical detail page"title": "Regional Optimization Lead",// ── JobPosting JSON-LD ──"description": "<p>Entity: Supply, Trading & Shipping…</p>", // HTML"datePosted": "2026-05-07T02:51:32.477Z", // ISO 8601"validThrough": "2026-06-18T02:51:32.477Z","employmentType": "FULL_TIME", // or null"industry": "Management, Management-Area Management","directApply": true,"jobLocationType": null, // "TELECOMMUTE" for remote"applicantLocationRequirements": [], // populated when remote-friendly// ── Employer (from JSON-LD hiringOrganization) ──"employer": {"name": "BP Energy","url": "https://www.totaljobs.com/jobs/bp-energy?cmpId=1428985&cmp=1","logoUrl": "https://www.totaljobs.com/CompanyLogos/2e66e5e85ad5408380e06078af3eb663.png"},// ── Location (from JSON-LD jobLocation.address + geo) ──"location": {"text": "St James, London, WC2N 5DU, GB","locality": "St James","region": "London","postalCode": "WC2N 5DU","country": "GB","lat": 51.50445,"lng": -0.13601},// ── Salary (parsed from body text via regex — not in JSON-LD) ──"salary": {"rawText": "£70,967 to £83,926 per annum","min": 70967,"max": 83926,"currency": "GBP", // ISO code (GBP/USD/EUR)"period": "annum" // "annum" / "hour" / "day" / "week" / "month"},// ── Apply flow ──"applyUrl": "https://www.totaljobs.com/job/regional-optimization-lead/bp-energy-job107245246", // = jobUrl when directApply=true; null when directApply=false (external recruiter)"applyType": "internal", // "internal" (apply on TotalJobs) / "external" (external recruiter) / "unknown""scrapedAt": "2026-05-15T06:25:31.012Z"}
How it works
- Classify input — listings (
/jobs/...) vs. details (/job/{slug}-job{id}). Listings auto-paginate; details scrape one row each. - Fetch via Apify Residential GB —
impitwith Firefox TLS fingerprint. Direct + Evomi residential get 403 Akamai blocks; Apify GB returns clean 200 OK. - For listings: collect detail-URL anchors per page, follow
rel="next"until empty or cap. Then concurrent detail fetches via sliding window. - For each detail: parse
JobPostingJSON-LD for 14 fields. Run salary regex on body text. Emit one flat row.
Apply flow — what applyUrl and applyType mean
Every row carries two apply-flow fields derived from the JSON-LD directApply flag:
directApply | applyType | applyUrl | Meaning |
|---|---|---|---|
true | "internal" | = jobUrl | TotalJobs hosts a one-click apply form on the job page itself. Send the candidate to applyUrl (which is the same as jobUrl) and they can apply without leaving TotalJobs. |
false | "external" | null | The actual apply destination is an external recruiter ATS (e.g. Workday, Greenhouse). TotalJobs only resolves that URL after a click via async XHR. 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" — open jobUrl to apply. |
Why isn't applyUrl always the external URL? TotalJobs renders the apply button server-side as a disabled placeholder (<button aria-label="apply-button-placeholder" disabled>). The real external URL loads via XHR after click — we'd need a headless browser to capture it, which would 5–10× the per-row cost and trip Akamai. We tested 15 standard apply-API REST endpoints (e.g. /api/applicationredirect/{id}, /api/v1/listings/{id}/apply) — all 404. The data in __PRELOADED_STATE__.applyNowSection contains only listingId/listingGlobalId, not the external URL.
Practical rule of thumb: ≈ 60–70 % of TotalJobs listings are directApply: true (apply on TotalJobs), so for that majority you get a usable applyUrl directly. For the remainder, applyType="external" is the signal — open jobUrl in a browser to follow the recruiter redirect.
Notes & limitations
- Apify Residential GB is mandatory. Direct connections from non-UK IPs get 403 Akamai blocks. Evomi residential (any country) also gets blocked. Apify Residential GB returns clean 200 OK on every probe we tested.
- Listing discovery uses an efficient JSON pagination path — 50 items per page, with
totalItemsreported up-front. The paginator walksoffsetuntil eithermaxItemsis reached or the response returns 0 items. employmentTypefill ≈ 70%. Not every JobPosting JSON-LD declares it. We don't synthesize when missing.jobLocationTypeonly set for remote/hybrid roles. Non-remote jobs leave itnull— matches JSON-LD semantics.- Salary fill ≈ 100% when surfaced. When a job has a published salary it parses cleanly; when it doesn't (small fraction of jobs) the
salaryfield isnull. /jobs/{company-slug}-jobsURLs work for company-specific listings (vs the/jobs/in-{location}keyword listings).
FAQ
Which TotalJobs URLs work?
Two types: listing URLs (/jobs/in-london, /jobs/software-engineer/in-manchester, /jobs/{company-slug}-jobs) which auto-paginate and emit one row per linked job, and direct detail URLs (/job/{title-slug}/{org-slug}-job{id}) which scrape one row each. You can mix both in the same startUrls array.
Why do I need Apify Residential GB? TotalJobs sits behind Akamai Bot Manager with strict country rules. Direct connections, datacenter proxies, and non-GB residential (we tested Evomi IN/US/EU) all return 403 "Access Denied" from Akamai's edge. Apify Residential GB is the only pool we found that returns clean 200 OK on every probe.
What's the difference between sourceSearchUrl, jobUrl, and applyUrl?
Three distinct things:
sourceSearchUrl— the search/listing page you pasted as input (e.g./jobs/in-edinburgh?postedWithin=1). It's the breadcrumb back to "where this row came from in your batch", not a URL anyone clicks to view the job. Renamed fromlistingUrlin v0.1 because customers kept reading it as "apply URL".jobUrl— the per-job detail page (e.g./job/regional-optimization-lead/bp-energy-job107245246). Use this to view the full listing in a browser.applyUrl— where the candidate clicks to apply. Equal tojobUrlwhendirectApply=true(TotalJobs hosts the form);nullwhendirectApply=false(external recruiter — TotalJobs resolves the URL only after JS-driven click). See the Apply flow table above.
When is applyUrl set vs null?
Set to the jobUrl whenever directApply=true in JSON-LD (≈ 60–70 % of TotalJobs jobs — those with a one-click apply form on TotalJobs itself). null otherwise, with applyType="external" as the signal that the apply destination is on a recruiter ATS the browser-side click resolves dynamically. See the "Apply flow" section above for the full breakdown.
What does each dataset-item charge cover?
One job row with all 14 JSON-LD fields plus the parsed salary band (min/max/currency/period) and structured location (lat/lng). maxItems is per listing URL, so a maxItems: 100 run with 2 listings = up to 200 charges. The Apify Store pricing event is apify-default-dataset-item — Apify auto-charges per row written to the default dataset.
Can the parsed salary handle annual / hourly / ranges?
Yes. The regex catches £70,967 to £83,926 per annum, £50k - 70k per year, £15 per hour, and single values. The period field normalizes to annum/hour/day/week/month. When a job has no published salary, salary is null — we don't synthesize.
Why does one listing page sometimes return 7 jobs and another 10?
Listing discovery uses a JSON-based pagination path — 50 items per offset step until you hit totalItems or your maxItems budget. Per-page variance in the old web-pagination sense is gone.
My run returned fewer rows than the TotalJobs page shows — why?
Three possibilities, ranked by likelihood: (1) Akamai blocked a mid-stream page. As of v0.1 the pagination loop skip-ahead on the first failed page and only gives up after two consecutive blocked pages — you'll see a listing page=N blocked, skipping ahead to page=N+1 line in the log. If you see two such lines back-to-back, that listing's tail was unrecoverable on this run; just rerun and the proxy pool will give you a fresh session. (2) TotalJobs paginates beyond what's shown. The "1000+ jobs" badge at the top of a search is often the total filterable corpus, not the count for your current filter combo. Page-7+ on narrow filters (e.g. postedWithin=1 + companytypes=2) genuinely runs out of results. (3) maxItems cap. It's per-listing-URL — if you set maxItems: 100 and pasted one URL, that's the ceiling.
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 for the rest of the catalog
⚠️ Disclaimer
This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by TotalJobs.com, StepStone Group, or any of their subsidiaries. All trademarks mentioned are the property of their respective owners.
The scraper extracts only publicly visible job postings rendered server-side by TotalJobs — no login, no CAPTCHA solving, no API-key forgery, no gateway.totaljobs.com (VPC-internal) probing. The actor honours robots.txt and rate-limits via concurrency cap (default 4) to avoid burdening TotalJobs's infrastructure.
Users are responsible for:
- Complying with TotalJobs.com'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 TotalJobs
SEO Keywords
totaljobs scraper, scrape totaljobs, totaljobs uk scraper, totaljobs.com scraper, totaljobs api, Apify totaljobs, uk jobs scraper, uk job board scraping, jobpostings api, jobposting json-ld scraper, uk recruitment api, recruitment scraper uk, uk salary data, salary band extraction, uk job market data, hiring intelligence uk, employer hiring data, b2b sales prospecting uk, london jobs scraping, manchester jobs scraper, edinburgh jobs scraper, akamai bot manager bypass, apify residential gb