# Djinni, DOU & JustJoin.it IT Jobs Scraper (`tiger19935/djinni-dou-justjoin-jobs-scraper`) Actor

All Ukrainian and Polish tech jobs in one run: Djinni, DOU and JustJoin.it in one schema, salaries converted to EUR, duplicates merged across boards, and a monitoring mode that returns only new and closed positions.

- **URL**: https://apify.com/tiger19935/djinni-dou-justjoin-jobs-scraper.md
- **Developed by:** [Andrew Boiko](https://apify.com/tiger19935) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 job records

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

## Djinni, DOU & JustJoin.it IT Jobs Scraper

Scrape **IT jobs from Ukraine's and Poland's biggest tech job boards in one run** - **Djinni.co, DOU.ua (jobs.dou.ua) and JustJoin.it** - into one clean, de-duplicated dataset with **salaries converted to EUR per month**, seniority, English level, remote type, skills and the full job description. Schedule it in **"New since last run"** mode to receive only fresh jobs - plus the ones that were taken down (usually: filled).

### Who is it for?

- **Recruiters and staffing / outstaffing agencies** - a daily feed of new PHP, Python, Java, DevOps... jobs in Ukraine and Poland, with salaries, ready for your CRM or ATS.
- **HR, comp & ben analysts** - salary benchmarks for Ukrainian and Polish IT in one currency (EUR / month, B2B vs. employment contract).
- **Lead generation for IT service companies** - companies that are hiring right now are companies with a budget and a need.
- **Job seekers and career services** - one alert instead of three boards, filtered by stack, level, remote and minimum salary.
- **Market research and media** - demand by technology and seniority, remote share, salary trends.

### What data do you get?

| Field | Example |
|---|---|
| `title`, `normalizedTitle` | PHP Engineer (F/M), `php developer` |
| `company`, `location`, `country` | PayPo, Warszawa, PL |
| `seniority`, `experienceYears`, `englishLevel` | middle, 3, B2 |
| `remote` | full / hybrid / flexible / office / unknown |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryType` | 19000, 22000, PLN, month, b2b |
| `salaryEurMonthlyMin`, `salaryEurMonthlyMax`, `fxRateDate` | 4341, 5026, 2026-10-05 |
| `skills[]`, `category` | PHP 8, Symfony 6, MySQL, Docker... |
| `postedAt`, `url`, `source`, `alsoPostedOn[]` | the same job on other boards is merged |
| `descriptionMarkdown` | full description as clean Markdown |
| `changeType` | `active`, or `new` / `updated` / `delisted` in monitoring mode |

Salaries are converted with the **official National Bank of Ukraine exchange rates** of the day; hourly B2B rates are converted with 168 hours per month.

### How to use it

1. Choose **job boards** (all three by default) and **technologies / roles** (Python, PHP, JavaScript, Java, .NET, Go, DevOps, QA, Data, AI/ML, Design, PM, Sales... 35 categories, mapped to each board's own categories).
2. Optionally add **keywords** (e.g. "Laravel", "fintech"), **exclude words**, **seniority**, **remote only**, **minimum salary in EUR**, **only jobs with a salary**, and **posted within N days**.
3. Click **Start** and download the results as JSON, CSV, Excel or HTML, or get them via API.

#### Example input - senior remote PHP/Python jobs paying at least €3,000/month

```json
{
  "sources": ["djinni", "dou", "justjoin"],
  "technologies": ["php", "python"],
  "seniority": ["senior", "lead"],
  "remoteOnly": true,
  "minSalaryEur": 3000,
  "postedWithinDays": 7,
  "maxItems": 200
}
```

#### Example output (one job, shortened)

```json
{
  "jobId": "justjoin:de7cb53f-ffdb-4233-8b52-3d6d5c330a9c",
  "changeType": "active",
  "source": "justjoin",
  "url": "https://justjoin.it/job-offer/paypo-php-engineer-f-m-inzynier-php-k-m--warszawa-php-149b69dd",
  "title": "PHP Engineer (F/M) / Inżynier PHP (K/M)",
  "normalizedTitle": "php developer",
  "company": "PayPo",
  "location": "Warszawa",
  "country": "PL",
  "remote": "full",
  "seniority": "middle",
  "englishLevel": "B2",
  "skills": ["PHP 8", "Symfony 6", "MySQL", "Docker", "REST API", "AWS"],
  "salaryMin": 19000,
  "salaryMax": 22000,
  "salaryCurrency": "PLN",
  "salaryPeriod": "month",
  "salaryType": "b2b",
  "salaryEurMonthlyMin": 4341,
  "salaryEurMonthlyMax": 5026,
  "fxRateDate": "2026-10-05",
  "postedAt": "2026-10-02T14:00:08.495Z",
  "alsoPostedOn": [],
  "descriptionMarkdown": "**O PayPo**\n\nW PayPo tworzymy przyszłość płatności cyfrowych..."
}
```

The run summary (`OUTPUT`) shows per-board status, how many jobs were collected, filtered and merged, and any warnings.

### Monitoring: only new jobs, every day

Set **Mode = New since last run** and create a **schedule** (e.g. every morning at 8:00). Each run returns only:

- `new` - jobs you have not received before,
- `updated` - jobs whose title or salary changed,
- `delisted` - jobs from your previous runs that disappeared from the board (a strong signal that the position was filled or closed).

Add a **digest webhook** (Slack, Discord, Microsoft Teams, Make, Zapier, n8n) and get a short message with the new jobs after every run. The history is kept per search (or per **Monitoring state key**), so several saved tasks can monitor different stacks independently.

### Pricing

Pay per event - you pay only for jobs you receive:

| Event | Price |
|---|---|
| Job saved to the dataset | **$0.001** per job ($1 per 1,000 jobs) |
| Digest delivered to your webhook | **$0.01** per digest (only when it was delivered) |

plus a small per-run start fee shown on the *Pricing* tab. Platform usage is included. **Max jobs** and your *maximum cost per run* are always respected.

*Example:* a daily monitor for PHP + Python jobs in Ukraine and Poland that finds 40 new jobs a day costs about $0.04 per day (~$1.25 per month).

### Integrations and API

Export to **Google Sheets, Excel, Airtable, HubSpot/your CRM** via Make, Zapier or n8n, or call it from code and from AI agents (Apify API, Apify MCP server).

```bash
curl -X POST "https://api.apify.com/v2/acts/<USERNAME>~djinni-dou-justjoin-jobs-scraper/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"technologies":["devops"],"remoteOnly":true,"maxItems":100}'
```

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_TOKEN>' });
const run = await client.actor('<USERNAME>/djinni-dou-justjoin-jobs-scraper').call({ technologies: ['java'], seniority: ['senior'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((j) => `${j.title} @ ${j.company}: €${j.salaryEurMonthlyMin}-${j.salaryEurMonthlyMax}`));
```

### FAQ

**Is scraping these job boards legal?** The Actor only reads publicly available job listings - the same pages anyone can open without logging in - at a moderate pace, and it does not collect personal data of candidates. Job ads are published by employers to be seen. You are responsible for using the data in line with the boards' terms and applicable law (e.g. do not republish full ads as your own job board without permission).

**Do I need a proxy?** Usually not. If a board blocks a request while running on Apify, the Actor automatically retries the blocked searches through Apify Proxy. You can also set a proxy explicitly in **Proxy configuration**.

**How is the salary in EUR calculated?** With the official National Bank of Ukraine rates for the day of the run (`fxRateDate`). Ranges are kept as published (`salaryMin`/`salaryMax` + currency); B2B and employment-contract salaries are labelled in `salaryType`.

**How are duplicates handled?** The same company + the same role + the same remote type across boards becomes one item; the other links are in `alsoPostedOn`. Turn **Merge the same job** off to get every listing.

**How much memory does a run need?** The default 1 GB handles about 10,000 jobs with full descriptions per run, which covers daily monitoring and most exports. For bigger exports (many technologies, tens of thousands of jobs), set *Memory* to 2 GB or more in the run options.

**Something is missing?** Open an issue in the *Issues* tab - requests for more boards (e.g. Work.ua, NoFluffJobs) and fields are welcome.

### More tools by the same author

- [Remote Tech Jobs Aggregator](https://apify.com/tiger19935/remote-tech-jobs-aggregator): Remote tech jobs from many boards in one schema
- [Career Page Jobs Scraper](https://apify.com/tiger19935/career-page-jobs-scraper): Jobs from Greenhouse, Lever, Ashby and more
- [Hiring Signals Monitor](https://apify.com/tiger19935/hiring-signals-monitor): Track new job postings at target companies

# Changelog

This Actor's version history is a separate document: https://apify.com/tiger19935/djinni-dou-justjoin-jobs-scraper/changelog.md

# Actor input Schema

## `sources` (type: `array`):

Djinni and DOU = Ukraine's biggest IT boards (incl. remote jobs for Ukrainians abroad), JustJoin.it = Poland's biggest IT board.

## `technologies` (type: `array`):

Mapped to each board's own categories. Leave empty for all jobs. Roles without a native category on a board are searched by keyword there.

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

Full-text search terms, e.g. "Laravel", "Kubernetes", "fintech". Each keyword is a separate search (OR), combined with the technologies (AND).

## `excludeKeywords` (type: `array`):

e.g. "Trainee", "Sales", "1C".

## `seniority` (type: `array`):

Detected from the title, required experience and the board's level. Leave empty for all.

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

Keep fully remote jobs and "office or remote" jobs.

## `minSalaryEur` (type: `integer`):

Compared with the top of the salary range converted to EUR per month (official NBU exchange rates; hourly rates x 168). Jobs without a published salary are kept unless "Only jobs with salary" is on.

## `requireSalary` (type: `boolean`):

Drop jobs that do not publish a salary range.

## `postedWithinDays` (type: `integer`):

Older jobs are skipped and paging stops early, which keeps runs fast and cheap.

## `mode` (type: `string`):

All = every matching job. New since last run = only jobs you have not received yet (plus jobs whose salary/title changed and jobs that were taken down). Use it with a Schedule for daily alerts.

## `detectDelisted` (type: `boolean`):

In monitoring mode, output jobs from your previous runs that disappeared from the board (changeType = delisted) - a signal that the position was filled.

## `stateKey` (type: `string`):

Optional name of the saved history (letters, digits, "-"). By default it is derived from your search settings, so every saved task keeps its own history.

## `digestWebhookUrl` (type: `string`):

Optional https:// webhook. Slack, Discord and Microsoft Teams URLs get a formatted message with the new jobs; any other URL (Make, Zapier, n8n, your API) gets JSON. Sent only when there is something new. Stored encrypted.

## `dedupe` (type: `boolean`):

Same company + same role + same remote type = one item; the other links are kept in alsoPostedOn.

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

Adds descriptionMarkdown. For DOU and JustJoin this needs one extra request per job (only for jobs that are output).

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

Maximum number of jobs saved in this run. You pay per saved job, so this also caps the cost.

## `maxPagesPerQuery` (type: `integer`):

Safety limit for paging on each board and search.

## `proxyConfiguration` (type: `object`):

The boards normally work without a proxy. If you see blocked requests, enable Apify Proxy (datacenter).

## Actor input object example

```json
{
  "sources": [
    "djinni",
    "dou",
    "justjoin"
  ],
  "technologies": [
    "python"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "seniority": [],
  "remoteOnly": false,
  "requireSalary": false,
  "postedWithinDays": 7,
  "mode": "all",
  "detectDelisted": true,
  "dedupe": true,
  "includeDescription": true,
  "maxItems": 100,
  "maxPagesPerQuery": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

Jobs found in this run.

## `summary` (type: `string`):

Per-board status, counts of new/updated/delisted jobs, exchange-rate source and warnings.

# 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 = {
    "sources": [
        "djinni",
        "dou",
        "justjoin"
    ],
    "technologies": [
        "python"
    ],
    "postedWithinDays": 7,
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tiger19935/djinni-dou-justjoin-jobs-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 = {
    "sources": [
        "djinni",
        "dou",
        "justjoin",
    ],
    "technologies": ["python"],
    "postedWithinDays": 7,
    "maxItems": 100,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("tiger19935/djinni-dou-justjoin-jobs-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 '{
  "sources": [
    "djinni",
    "dou",
    "justjoin"
  ],
  "technologies": [
    "python"
  ],
  "postedWithinDays": 7,
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call tiger19935/djinni-dou-justjoin-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tiger19935/djinni-dou-justjoin-jobs-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/iBHsOIaPDcie59e9b/builds/7ngk6kJTamrJTznx4/openapi.json
