What the Just Join IT scraper collects
Scrapes justjoin.it job offers through the endpoint its own front end calls: paginated, filtered listings, full single offers with the description and per-skill levels, and the board's uncapped facet counts. Output is JSON or CSV with a sidecar recording the site's 10,000-result cap.
Job listings
Input: Filters: --category, --city, --experience, --with-salary and moreOne row per offer: company, category, seniority, workplace type, salary in the employer's unit plus a monthly figure, required skills, languages and locations.
Single offers
Input: A justjoin.it job-offer URL, or a listings run's JSON via --slugs-fileOne offer in full: the description body, per-skill levels, company website and size, and the country.
Board facets
Input: No input — the whole boardThe board's own taxonomy with its own offer counts, which are not capped, so you can plan category runs that cover everything.
From zero to Just Join IT data in 3 steps
Install
Clone the repository and install one browser engine. Python 3.9+.
$ git clone https://github.com/2scraper/justjoin-scraper.git && cd justjoin-scraper $ pip install -r requirements.txt -r requirements-playwright.txt $ python3 -m playwright install chromium $ python3 playwright_scraper.py --category python --pages 5 --format csv
Run a scraper
Pick what you need and run it. Every flag is documented in the README.
$ python3 playwright_scraper.py --city Warszawa --experience senior --with-salary --pages 5 --format csv --out warsaw_senior$ python3 playwright_scraper.py --mode offer --url "https://justjoin.it/job-offer/goodylabs-senior-devops-engineer-k-m-x--lodz-devops"$ python3 playwright_scraper.py --mode facetsGet structured data
Rows land in JSON or CSV (
--format), with a.meta.jsonsidecar recording status, pages fetched and why the run stopped.
Just Join IT data fields
50 columns per row in a predictable schema. The most useful ones:
| Field | What it holds |
|---|---|
sku | The offer's guid, a UUID — the join key across modes |
title | Job title |
company_name | Employer name |
category | Board category, e.g. python |
experience_level | Seniority, e.g. senior |
workplace_type | remote, hybrid or office |
salary_min | Lower bound in the employer's quoted unit; null when undisclosed |
salary_max | Upper bound in the employer's quoted unit |
salary_currency | The employer's original currency, not a site conversion |
salary_unit | Unit the salary was quoted in, e.g. hour or month |
salary_monthly_min | The site's monthly normalisation of the lower bound |
salary_contract_type | Contract the salary applies to, e.g. b2b |
required_skills | Required skills list |
required_skill_levels | Level for each required skill |
locations | Every city the offer lists |
apply_method | external or form (applied to on justjoin.it) |
[
{
"source": "justjoin.it",
"scraped_at": "2026-09-18T11:56:56Z",
"url": "https://justjoin.it/job-offer/port-blue-sky-sp-z-o-o-sp-k--senior-software-engineer---p…",
"sku": "3be7c75b-f182-4dd5-a956-16b505f93fa1",
"title": "Senior Software Engineer – Python / React / Data",
"slug": "port-blue-sky-sp-z-o-o-sp-k--senior-software-engineer---python-react-data-warszawa-python",
"company_name": "PORT BLUE SKY Sp. z o.o. sp.k.",
"company_logo_url": "https://imgproxy.justjoinit.tech/OeuSuuCp2rJICtPt10dWVrEYrgRPo_mYWIkJ7BElWFU/h:200/w:20…",
"company_url": null,
"company_size": null,
"category": "python",
"experience_level": "senior",
"workplace_type": "remote",
"working_time": "b2b_contract",
"salary_min": 200.0,
"salary_max": 240.0,
"salary_currency": "PLN",
"salary_unit": "hour",
"salary_is_gross": false,
"salary_contract_type": "b2b",
"salary_monthly_min": 33600.0,
"salary_monthly_max": 40320.0,
"salary_options": null,
"city": "Warszawa",
"street": "Rondo ONZ 1",
"country_code": null,
"latitude": 52.23,
"longitude": 21.0,
"locations": [
"Warszawa"
],
"location_count": 1,
"required_skills": [
"Python",
"React",
"Docker",
"… 6 more"
],
"required_skill_levels": [
5,
5,
5,
"… 6 more"
],
"nice_to_have_skills": null,
"languages": null,
"language_levels": null,
"office_days": null,
"remote_days": null,
"remote_interview": false,
"open_to_hire_ukrainians": false,
"is_super_offer": false
},
{
"source": "justjoin.it",
"scraped_at": "2026-09-18T11:56:56Z",
"url": "https://justjoin.it/job-offer/jit-team-mlops-data-engineer-warszawa-python",
"sku": "a8fdc280-9a73-4177-b03e-f070c1f54c06",
"title": "MLOps / Data Engineer",
"slug": "jit-team-mlops-data-engineer-warszawa-python",
"company_name": "Jit Team",
"company_logo_url": "https://imgproxy.justjoinit.tech/wWSYQ3PfIRATt5r6FpNQ1tx2EsfW1STfzPhAXi3RVQg/h:200/w:20…",
"company_url": null,
"company_size": null,
"category": "python",
"experience_level": "mid",
"workplace_type": "hybrid",
"working_time": "freelance",
"salary_min": 25200.0,
"salary_max": 28560.0,
"salary_currency": "PLN",
"salary_unit": "month",
"salary_is_gross": false,
"salary_contract_type": "b2b",
"salary_monthly_min": 25200.0,
"salary_monthly_max": 28560.0,
"salary_options": null,
"city": "Warszawa",
"street": "-",
"country_code": null,
"latitude": 52.23,
"longitude": 21.01,
"locations": [
"Warszawa"
],
"location_count": 1,
"required_skills": [
"Machine Learning",
"Azure",
"DevOps",
"… 4 more"
],
"required_skill_levels": [
3,
3,
3,
"… 4 more"
],
"nice_to_have_skills": null,
"languages": null,
"language_levels": null,
"office_days": null,
"remote_days": null,
"remote_interview": false,
"open_to_hire_ukrainians": false,
"is_super_offer": false
}
]First 2 of 9 rows from a real run — sample_output.json. Long values shortened here.
Handles what breaks naive Just Join IT scrapers
The employer's salary, not a conversion
employmentTypes mixes the employer's figure with currency conversions, and the original is not first on 40% of detail records. The scraper selects on currencySource, never on position.
Quoted rate and monthly figure kept apart
Reading the adjacent keys gives "18,480 PLN per hour" for a 110-an-hour job. salary_min/max carry the quoted rate beside salary_unit; the monthly figure has its own salary_monthly_* columns.
The 10,000 cap is in the sidecar
Every query stops at 10,000 results. The sidecar records total_results, site_total, reachable_max and capped_by_site, and the planner stops at the ceiling instead of hitting the site's HTTP 500.
Only filters that actually filter
The endpoint silently ignores several parameter names. Only parameters measured to change the result count are exposed as flags, so a filter typo cannot pass for a filtered run.
Scraping infrastructure included
The same tested core runs every 2scraper repository, so the hard parts work the same way on every site.
CAPTCHA handling
Detects the challenge a page really serves and, with a 2Captcha key, solves it only when it blocks the data — a capped number of solves per page.
Proxy rotation
Single proxy or a pool from a file, rotated on blocks with a fresh browser per exit. Credentials stay out of argv and logs.
Fingerprints
Optional complete device identity from the 2Captcha Fingerprint API — user agent, client hints, locale and timezone that agree with each other.
Scraping Browser API
Point --cdp-endpoint at a managed Chromium with its own exit country — no browser infrastructure to run yourself.
Honest run metadata
Every run writes a .meta.json sidecar and distinct exit codes: complete, blocked, empty and partial are never confused, and a failed run never overwrites good data.
Diff two runs
diff_runs.py compares two complete Just Join IT snapshots by id — new, removed and changed rows for monitoring.
What teams do with Just Join IT data
IT salary benchmarking
Compare quoted and monthly salaries by category, seniority, city and contract type across the Polish market.
Skills demand analysis
Count required skills and their levels across thousands of offers to see which technologies employers ask for.
Recruitment lead lists
Track which companies are hiring, in which cities and for which roles, and diff daily runs for new postings.
Job board market sizing
Use facets mode's uncapped counts to size categories and plan full-board coverage.
Free to run. Pay only for what the site requires.
Runs with no key, proxy or account
Every route this scraper reads was served in full to a bare datacentre address with no credentials, measured Sep 18 2026, and the daily canary runs from a GitHub runner with no secrets. A proxy buys volume and politeness for full-board runs; the Scraping Browser API buys a chosen exit country and no local Chromium, not access. No captcha appears on any route read, so a 2Captcha key buys nothing today.
Optional 2Captcha services — captcha solving, the Scraping Browser API, residential proxies and fingerprints — work behind one key.
Good to know
- Any single query reaches at most 10,000 offers; split by category to cover the board.
- --route ssr rows have a poorer schema: no category, languages or skill levels, and one page only.
- Salaries are reproduced as employers type them, typos included.
2scraper vs. hosted scraper APIs
| 2scraper | Typical hosted API | |
|---|---|---|
| Code | Open source, MIT — read and patch it | Closed |
| Cost | Free; optional services billed only when used | Subscription or per request / per result |
| Runs on | Your machine, server or CI | The vendor's infrastructure |
| Output | JSON/CSV files + run metadata | API responses |
| When the site changes | Fix it yourself or open an issue / PR | Wait for the vendor |
Just Join IT scraper FAQ
Do I need a proxy or API key?
No. Listings, offers and facets were served to a datacentre address with no headers at all. Only a Python-urllib User-Agent is refused, which the scraper never sends.
Why do I never get more than 10,000 offers?
The site caps every query at 10,000 results while the board held 19,381 on 2026-09-18. Run --mode facets and split by category; 25 category runs covered the board then.
Why is salary null on many rows?
461 of 1,000 measured offers disclose no salary. The scraper writes null, never zero, and --with-salary filters them out at the site.
Does it respect robots.txt?
By default it reads the /api/ endpoint the site's own front end calls, which robots.txt disallows — the README says so plainly. --route ssr reads the rendered listing page instead, within robots.txt, at one page of 100 rows.
Does the sort order matter?
Yes: because queries are capped, the sort decides which offers you get. diff_runs.py refuses to compare runs with different sorts, orderings or filters.
Is it legal?
It reads public job offers and never submits an application. Follow the site's terms and the law where you operate.