# JustJoin.it Scraper (`santamaria-automations/justjoinit-scraper`) Actor

Scrape IT jobs from JustJoin.it, Poland's tech job board. Returns title, company, city, tech stack with skill levels, B2B/UoP salaries, remote/hybrid mode, languages, office address and expiry date. Sort by newest. Pay per result.

- **URL**: https://apify.com/santamaria-automations/justjoinit-scraper.md
- **Developed by:** [NanoScrape](https://apify.com/santamaria-automations) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 serp 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?

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

## JustJoin.it Scraper: Polish & EU Tech Jobs with Salary Data

Extract IT/tech job listings from [JustJoin.it](https://justjoin.it), Poland's leading IT job board. Filter by keyword, city, seniority, remote status, and contract type (B2B/UoP).

### What you get

- **Salary data**: Salary disclosure is mandatory for native JustJoin.it listings but may be null on partner-imported listings from LinkedIn, Pracuj.pl, etc. Listings with a disclosed range return salary_min, salary_max, currency (PLN/EUR), and period (hourly/daily/monthly/annual). In our test runs roughly a quarter of listings carried a numeric range, so expect nulls.
- **Tech stack with levels**: the required and nice-to-have skills the offer lists (for example Databricks, .Net, Python), with the skill level shown on the page (Nice To Have, Junior, Regular, Advanced, Master). The job category (python, data, java) is a separate field.
- **Salary per contract type**: separate B2B and UoP ranges with currency, period and gross/net.
- **Contract types**: B2B, UoP and mandate contract as listed on the offer.
- **Expiry date, languages, office address and every office location** when the offer has several.
- **Remote type**: fully-remote, hybrid, or office (from JustJoin.it's workplace type).
- **Company details**: company name, website, profile page link, size, and the "About the company" text.
- **Description quartet**: description_snippet (first 300 chars), description_full (plain text), description_html (raw HTML), description_md (Markdown). Requires includeJobDetails=true for the full variants.
- Real server-side sorting (newest, relevance, highest salary) and a posted-within-days filter.
- Around 10,000 active listings refreshed daily.

### Output fields

| Field | Type | Notes |
|-------|------|-------|
| `_type` | string | Always `"job"` |
| `id` | string | JustJoin.it job slug |
| `title` | string | Job title |
| `job_url` | string | Canonical listing URL |
| `source_url` | string | Same as job_url (pipeline compat) |
| `company_name` | string/null | Hiring company |
| `company_website` | string/null | Company site from schema.org |
| `location` | string/null | City, CountryCode |
| `country` | string/null | ISO 2-letter code (usually PL) |
| `city` | string/null | Primary city |
| `posted_at_text` | string/null | Human-readable date |
| `posted_at_datetime` | string/null | ISO 8601 timestamp |
| `employment_type` | string/null | `Full-time`, `Part-time`, or `Contract` (freelance and B2B-only offers) |
| `remote_type` | string/null | fully-remote / hybrid / office (kept for compatibility, same information as `remote_option`) |
| `remote_option` | string/null | Canonical work mode: `remote` / `hybrid` / `onsite` |
| `salary_min` | number/null | Lower salary bound |
| `salary_max` | number/null | Upper salary bound |
| `salary_currency` | string/null | ISO currency (PLN, EUR) |
| `salary_period` | string/null | monthly / hourly / annual |
| `salary_text` | string/null | Human-readable salary range |
| `seniority` | string/null | Junior / Mid / Senior / Lead |
| `tech_stack` | array | Skill names listed on the offer (required first, then nice-to-have), e.g. `["Databricks", ".Net", "Python"]` |
| `tech_stack_levels` | array | `[{name, level, level_value}]`, level is Nice To Have / Junior / Regular / Advanced / Master, level_value 1 to 5 |
| `category` | string/null | JustJoin.it job category (python, data, java, devops) |
| `contract_types` | array | B2B / UoP / Mandate contract as listed on the offer |
| `salaries` | array | Salary per contract type: `[{contract_type, min, max, currency, period, gross}]`, original currency only |
| `salary_gross` | boolean/null | Whether the primary salary is gross (true for UoP, usually false for B2B) |
| `working_pattern` | string/null | Full-time / Part-time / Freelance / B2B Contract |
| `languages` | array | `[{code, level}]` language requirements, e.g. `{"code":"en","level":"B2"}` |
| `english_level` | string/null | English requirement level (CEFR) when the offer lists one |
| `expires_at` | string/null | ISO 8601 date the offer expires (JSON-LD validThrough) |
| `description` | string/null | Plain-text description |
| `description_snippet` | string/null | First 300 chars of description |
| `description_full` | string/null | Full plain text (requires includeJobDetails) |
| `description_html` | string/null | Raw HTML (requires includeJobDetails) |
| `description_md` | string/null | Markdown (requires includeJobDetails) |
| `contact_emails` | array | Emails from description |
| `contact_email` | string/null | First email found |
| `contact_phone` | string/null | First phone found |
| `contact_phones` | array | Phone numbers from description |
| `contact_urls` | array | External URLs, taken from the real link targets in the description |
| `company_logo_url` | string/null | Company logo thumbnail |
| `company_about` | string/null | "About the company" text from the company profile, when the company has one |
| `company_profile_url` | string/null | JustJoin.it company profile page |
| `company_size` | string/null | Company size range when listed (e.g. `501+`) |
| `company_address` | string/null | Office street and city (city only when no street is listed) |
| `street_address` | string/null | Office street when listed |
| `locations` | array | Only when the offer has several locations: `[{city, street, latitude, longitude, url}]`. `location` is always the primary one |
| `latitude`, `longitude` | number/null | Primary office coordinates |
| `posted_at` | string/null | Same value as `posted_at_datetime` |
| `apply_method` | string/null | `external` (employer site) or `form` (apply on JustJoin.it) |
| `external_apply_url` | string/null | Employer apply link when the apply leaves JustJoin.it |
| `open_to_hire_ukrainians` | boolean/null | Offer is flagged open to Ukrainian candidates |
| `remote_interview` | boolean/null | Offer says the interview is remote |
| `hybrid_office_days`, `hybrid_remote_days` | integer/null | Weekly office and remote days for hybrid offers |
| `experience_level` | string/null | JustJoin.it level: junior, mid, senior, manager, c_level |
| `apply_url` | string/null | External apply link, otherwise the JustJoin.it job page |
| `search_query` | string/null | Keyword that matched this listing |
| `scraped_at` | string | ISO 8601 fetch timestamp |

### Input

| Field | Description | Default |
|-------|-------------|---------|
| `searchQueries` | Keywords searched on JustJoin.it, one server-side search per keyword (e.g. `["backend developer", "python"]`) | `developer` if empty |
| `maxResultsPerQuery` | Cap per keyword (the actor pages through results, 50 offers per page) | 5 |
| `maxResults` | Total result cap across all queries | 5 |
| `maxConcurrency` | Parallel detail-page workers (safe to increase for large runs) | 5 |
| `country` | Country filter: all, PL, DE, NL, CZ, SK | all |
| `city` | City filter (e.g. `Warszawa`, `Krakow`), applied server-side via the city page | empty |
| `seniority` | Junior / Mid / Senior / Lead | any |
| `remoteOnly` | Return only fully-remote positions (server-side) | false |
| `contractType` | Any / B2B / UoP, applied server-side. Offers that do not disclose a contract type are still returned by the site. | Any |
| `sortBy` | `newest`, `relevance`, `salary` (highest first). Legacy aliases: `published` = newest, `salaryDesc` = salary. | `newest` |
| `postedWithinDays` | Only jobs posted within N days (decimals allowed). Rounded up to the site buckets 1, 7, 14 or 30, then cut exactly on posted date. Above 30, 0 or empty = no filter. | empty |
| `includeJobDetails` | Fetch full description + contacts (+$0.005/row) | false |
| `titleFilter` | Comma-separated title terms (OR-match, case-insensitive). Only jobs whose title contains one of the terms are returned. Non-matching rows are dropped before any detail fetch, so you don't pay for them. Leave empty to disable. | empty |

#### Sorting, freshness and paging

Sorting, the date window, the keyword filter and pagination are all done by JustJoin.it's own search pages, so `sortBy` has a real effect:

- `newest` returns the site's "Latest" order (newest first). This is the default and the right choice for scheduled scrapes.
- `relevance` sends no sort parameter, so you get the site default, which is relevance-ranked when a keyword is set.
- `salary` returns the highest salaries first. The site ranks by salary normalized across hourly, daily, monthly and yearly units, so raw `salary_max` values with different `salary_period` can look out of order.

Notes on ordering:

- Promoted or pinned offers can appear at the top of a page with an older date. That is the site's promotion slot, not a sort failure.
- The site orders and filters by the last (re)publish time. `posted_at_datetime` is the original publish date, so an offer that was republished can look slightly out of order or be older than the `postedWithinDays` window you asked for. The exact cutoff uses `posted_at_datetime`, so such reposted offers are dropped.
- The actor stops paging for a keyword when the result cap is reached or a page returns no new offers. Results stay separated per keyword (see `search_query`).
- Start URLs that point to a JustJoin.it search page (`https://justjoin.it/job-offers/...`) are paged the same way.

#### Example input

```json
{
  "searchQueries": ["python", "java"],
  "sortBy": "newest",
  "postedWithinDays": 7,
  "contractType": "B2B",
  "maxResultsPerQuery": 50,
  "maxResults": 100
}
```

### Sample output

```json
{
  "_type": "job",
  "id": "fabrity-s-a--backend-developer-net-warszawa-net",
  "title": "Backend Developer .NET",
  "job_url": "https://justjoin.it/job-offer/fabrity-s-a--backend-developer-net-warszawa-net",
  "company_name": "Fabrity S.A.",
  "company_website": "https://kariera.fabrity.com/",
  "location": "Warszawa, PL",
  "country": "PL",
  "city": "Warszawa",
  "remote_type": "fully-remote",
  "employment_type": "Full-time",
  "salary_min": 17000,
  "salary_max": 23000,
  "salary_currency": "PLN",
  "salary_period": "monthly",
  "salary_text": "17000 - 23000 PLN/month",
  "seniority": "Senior",
  "tech_stack": ["Java", "Spring", "SQL"],
  "tech_stack_levels": [{"name": "Java", "level": "Advanced", "level_value": 4}],
  "category": "java",
  "contract_types": ["B2B", "UoP"],
  "expires_at": "2026-10-31T22:59:59.999Z",
  "company_profile_url": "https://justjoin.it/brands/story/fabrity",
  "company_address": "Prosta 67, Warszawa",
  "posted_at_text": "Aug 25, 2026",
  "posted_at_datetime": "2026-08-25T08:00:39Z",
  "description": "We are looking for a Backend Developer...",
  "description_snippet": "We are looking for a Backend Developer .NET to join our team...",
  "contact_emails": [],
  "search_query": "backend developer",
  "scraped_at": "2026-08-25T08:54:14Z"
}
```

### Pricing

Pay-per-result pricing. $5 free monthly credit on Apify.

| Event | Cost |
|-------|------|
| Actor start | $0.001 |
| Job listing (SERP) | $0.003 |
| Job listing with full details | $0.005 |

Typical cost: around $3 per 1,000 jobs (SERP only), or $5 per 1,000 with full details.

### Scale and speed

Runs on 128 MB with plain HTTP requests. A 30-job run finishes in about 10 seconds and a 120-job, two-keyword run in about 85 seconds. Each search page holds 50 offers, and you can raise `maxConcurrency` (up to 20) to speed up larger runs. JustJoin.it lists roughly 10,000 active offers at any time.

### How it works

1. Requests JustJoin.it's server-rendered search pages (`/job-offers/all-locations?keyword=...&sortBy=...&published-date=...&page=N`) for each keyword
2. Reads the listing data (title, company, city, publish date, salary, work mode) from the page's embedded Next.js payload, 50 offers per page
3. Fetches each job page and reads schema.org/JobPosting JSON-LD plus the page's embedded offer data for the full salary, skills with levels, company text, office address, expiry and description
4. Optionally extracts full description and contact details

#### Precision title filter

```json
{
  "searchQueries": ["developer"],
  "titleFilter": "developer, python",
  "maxResults": 50
}
```

Only jobs with "developer" or "python" in the title are returned. Non-matching rows are dropped before any detail fetch.

### API and integrations

See the [auto-generated API tab](https://console.apify.com/actors/COMzbfc2OypZMmzlc/info/api) for ready-to-run cURL, JavaScript and Python examples that stay in sync with the input schema. Found a problem? [Open an issue](https://console.apify.com/actors/COMzbfc2OypZMmzlc/info/issues).

### Use with AI agents (MCP)

This actor works with Apify's MCP server in Claude, Cursor, LangChain and other agent frameworks.

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=santamaria-automations/justjoinit-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}
```

For per-tool schemas and copy-paste snippets for MCP clients, see the [MCP tab](https://console.apify.com/actors/COMzbfc2OypZMmzlc/info/api/mcp?build=latest).

Example prompt: "Find the 20 newest Python jobs on JustJoin.it posted in the last 7 days, and return title, company, city and salary range."

### Related actors

Working across the Polish job market? These complement JustJoin.it:

- [Pracuj.pl Scraper](https://apify.com/santamaria-automations/pracuj-pl-scraper): Poland's largest private-sector generalist board (all industries, 80,000+ listings).
- [praca.gov.pl Scraper](https://apify.com/santamaria-automations/praca-gov-pl-scraper): Poland's official government portal (Central Job Offers Database / CBOP) for public-sector and PUP office listings.
- [NoFluffJobs EU Scraper](https://apify.com/santamaria-automations/nofluffjobs-eu-scraper): direct Polish tech peer with mandatory salary (CZ/SK/DE/AT/CH coverage too).

Broader coverage:

- [LinkedIn Jobs Scraper](https://apify.com/santamaria-automations/linkedin-scraper): global professional network jobs.
- [Indeed Scraper](https://apify.com/santamaria-automations/indeed-http-scraper): broad global coverage including PL market.
- [Glassdoor Scraper](https://apify.com/santamaria-automations/glassdoor-scraper): company reviews, ratings and salary data alongside listings.
- [Website Job Extractor](https://apify.com/santamaria-automations/website-job-extractor): pulls job postings from any company career page.

Turn `company_website` into outreach data:

- [Website Email Scraper](https://apify.com/santamaria-automations/website-email-scraper): finds public emails on the company websites returned here.
- [Website Contact Extractor](https://apify.com/santamaria-automations/website-contact-extractor): extracts phones, social links and decision-maker contacts from those sites.

### Support

- **Questions or issues?** Email contact@nanoscrape.com, we typically reply within 6 hours. You can also open a ticket in the [Issues tab](https://console.apify.com/actors/COMzbfc2OypZMmzlc/info/issues) on the actor page.
- **Feature request for this actor?** Open a ticket in the [Issues tab](https://console.apify.com/actors/COMzbfc2OypZMmzlc/info/issues) or email contact@nanoscrape.com with the platform field you need and (if possible) a URL that shows the data live.
- **Need a scraper for a job board we don't cover yet?** Email contact@nanoscrape.com with the target site and your rough usage volume. We ship one-off boards regularly.

# Actor input Schema

## `searchQueries` (type: `array`):

One or more job-title or tech keywords to search on JustJoin.it (e.g. 'backend developer', 'Python', 'React', 'DevOps'). Each keyword runs its own server-side search (with its own sort, date window and pagination) and results are kept separate per query. Leave empty only when using startUrls.

## `maxResultsPerQuery` (type: `integer`):

Maximum results per search keyword. The actor pages through search results (50 offers per page) until this cap is reached.

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

Total job cap across all queries. JustJoin.it has around 10,000 active IT listings at any time.

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

Number of detail pages fetched in parallel when includeJobDetails is enabled. Each worker uses an independent request session with rotating headers. Increase freely to speed up large runs.

## `searchUrls` (type: `array`):

Direct JustJoin.it job URLs to scrape. Paste individual job links (https://justjoin.it/job-offer/...) or category pages. Accepts both plain strings and Apify requestListSources objects.

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

Alternative URL input. Supports both plain strings and Apify requestListSources objects with url/method fields.

## `country` (type: `string`):

Filter results by country. JustJoin.it primarily covers Poland (PL), with some EU remote roles. Use 'all' for all countries.

## `city` (type: `string`):

Filter by city name (e.g. 'Warszawa', 'Krakow', 'Wroclaw', 'Gdansk'). Applied server-side through the city listing page when JustJoin.it has one, otherwise as a post-filter. Leave blank for all cities.

## `seniority` (type: `string`):

Filter by seniority level extracted from the job title. Leave blank for all levels.

## `techStack` (type: `array`):

Filter by tech category keywords in the job URL slug (e.g. \['python', 'javascript', 'devops', 'data']). Case-insensitive substring match.

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

Filter by contract type, applied server-side (employment-type filter). B2B = freelance/invoice, UoP = Polish employment contract (permanent). Offers that do not disclose a contract type are still included by the site. Any = no filter.

## `remoteOnly` (type: `boolean`):

When enabled, only returns fully remote positions (applied server-side with the workplace=remote filter). JustJoin.it has a large share of remote Polish IT jobs.

## `sortBy` (type: `string`):

How JustJoin.it orders results. Applied by the site itself (server-side), not re-sorted by the actor. 'newest' is recommended for scheduled scrapes. 'relevance' uses the site default (relevance-ranked when a keyword is set). 'salary' returns the highest salaries first. 'published' and 'salaryDesc' are legacy aliases kept for backward compatibility. Promoted or pinned offers may appear at the top with an older date.

## `postedWithinDays` (type: `number`):

Only return jobs posted within this many days. The site filters by 1, 7, 14 or 30 days: your value is rounded up to the nearest of those buckets, then an exact posted_at cutoff is applied. Values above 30, 0 or empty mean no date filter.

## `includeJobDetails` (type: `boolean`):

Fetch each job's full description, HTML, and markdown. Also extracts contact emails, phones, and URLs from the job description. Adds $0.005 per row (detail event).

## `titleFilter` (type: `string`):

Comma-separated list of terms to match against job titles (case-insensitive OR-match). Only jobs whose title contains one of the terms are emitted. Non-matching rows are dropped before the detail-fetch event, so you don't pay for filtered-out jobs. Leave empty to disable.

## Actor input object example

```json
{
  "searchQueries": [
    "backend developer"
  ],
  "maxResultsPerQuery": 5,
  "maxResults": 5,
  "maxConcurrency": 5,
  "country": "all",
  "city": "Warszawa",
  "seniority": "",
  "contractType": "Any",
  "remoteOnly": false,
  "sortBy": "newest",
  "includeJobDetails": false,
  "titleFilter": ""
}
```

# Actor output Schema

## `jobListings` (type: `string`):

Dataset containing scraped JustJoin.it tech job listings. Each row includes title, company_name, company_website, location (city + country), salary_min, salary_max, salary_currency, salary_period, seniority, tech_stack, tech_stack_levels, category, contract_types, salaries, expires_at, company_about, company_profile_url, company_address, languages, remote_option, remote_type, employment_type, description_snippet, and optional description_full + description_html + description_md + contacts. Order is applied server-side by JustJoin.it per sortBy (default 'newest').

# 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 = {
    "searchQueries": [
        "backend developer"
    ],
    "maxResultsPerQuery": 5,
    "maxResults": 5,
    "maxConcurrency": 5,
    "sortBy": "newest"
};

// Run the Actor and wait for it to finish
const run = await client.actor("santamaria-automations/justjoinit-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 = {
    "searchQueries": ["backend developer"],
    "maxResultsPerQuery": 5,
    "maxResults": 5,
    "maxConcurrency": 5,
    "sortBy": "newest",
}

# Run the Actor and wait for it to finish
run = client.actor("santamaria-automations/justjoinit-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 '{
  "searchQueries": [
    "backend developer"
  ],
  "maxResultsPerQuery": 5,
  "maxResults": 5,
  "maxConcurrency": 5,
  "sortBy": "newest"
}' |
apify call santamaria-automations/justjoinit-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,santamaria-automations/justjoinit-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/COMzbfc2OypZMmzlc/builds/foVBBuJSbx7S9ozvJ/openapi.json
