# Work.ua Job Scraper: Ukraine Vacancies & Salary (`reventadata/workua-scraper`) Actor

Scrape Work.ua, Ukraine's job board, by keyword, city or category: vacancy title, employer, city, salary range in hryvnia as numbers, experience, schedule and posting date. Optional full description and skills. Job ads only, no CVs or phone numbers.

- **URL**: https://apify.com/reventadata/workua-scraper.md
- **Developed by:** [reventadata](https://apify.com/reventadata) (community)
- **Categories:** Jobs, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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?

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

## Work.ua Job Scraper: Ukraine Vacancies & Salary

> **Unofficial.** This Actor is not affiliated with, authorised by, endorsed by, or in any way
> officially connected to Work.ua. "Work.ua" is a trademark of its respective owner and is used here
> only to describe what this tool reads. It reads the same public vacancy pages a browser does.

Search Work.ua, Ukraine's job board (work.ua), by keyword, city or category and get one clean row per
vacancy: title, employer, location, salary range in hryvnia, experience, schedule, posting date and, if you
ask for it, the full description, requirements and skills.

### What does it do?

For each search term it walks the result pages of the Work.ua vacancy search and saves one row per
vacancy. With **Fetch full vacancy pages** switched on it also opens each vacancy page and adds the
description, requirements, skill tags, work place and the company link. Only job ads are read: employer
names, titles, salaries and locations the site shows to anyone. No CVs, no phone numbers, no e-mail addresses.

### Why use it?

- **Salary as numbers.** When the vacancy shows a bold salary range (for example `35 000 – 50 000 грн`),
  `salaryMin`, `salaryMax` and `salaryCurrency` hold it as integers and `salaryAfterDeductions` says whether
  it is net of tax. Wording the employer typed instead (`Договірна`, `від 1200$`, an hourly rate) stays in
  `salaryText` and is never guessed into a number.
- **Any keyword, any city, any category.** Ukrainian or English terms, a city slug such as `kyiv` or `lviv`,
  one of 28 work.ua categories, or the remote-work listing.
- **Filters that run before you pay.** Salary band, posted within N days, maximum required experience,
  verified companies only, words the vacancy must or must not contain. Rows they drop are not charged.
- **Flags work.ua adds.** Remote and hybrid, hot (paid promotion), new, reservation for employees
  (`hasDeferment`), salary above the average for the role, who the employer is ready to hire
  (`suitableFor`), verified and VIP company.
- **Dates in Kyiv time.** `postedDate` on every row, `postedAt` as a timestamp with the Kyiv offset where the site gives one.

### How to use it

1. Put one or more terms in `searchQueries`, for example `python` or `водій`. Leave it empty and pick a
   city, a category or Remote only to list everything there.
2. Set `maxItems` (rows to save) and, if you want, filters.
3. Run the Actor and download the dataset as JSON, CSV or Excel.

#### Input

| Field | Type | Description |
|---|---|---|
| `searchQueries` | array | Job titles or keywords, one search per term. Empty: list every vacancy under the chosen city, category or remote listing. |
| `maxItems` | integer | Total rows to save across all queries, counted after the filters. Default 100. Queries run in the order given and the run stops once the cap is reached. |
| `maxPages` | integer | Result pages per query, 1 to 500. Default 10 (14 vacancies a page, up to 140 per query). The Actor stops earlier at the last page the site has. |
| `city` | string | work.ua city slug: `kyiv`, `lviv`, `odesa`, `dnipro`, `kharkiv`, `zaporizhzhya`, `ivano-frankivsk`. Full list at work.ua/jobs/by-region/. An unknown slug returns no vacancies. Cannot be combined with `remoteOnly`. |
| `remoteOnly` | boolean | Search work.ua's remote-work listing. Cannot be combined with `city`. |
| `category` | string | One of 28 work.ua category slugs, for example `it`, `sales`, `retail`, `accounting`. |
| `includeDetails` | boolean | Open every vacancy page for the full description, requirements, skills, work place, schedule and company link. One extra request per vacancy, so the run is slower. Default off. |
| `minSalary`, `maxSalary` | integer | Salary band in hryvnia against the bold numeric range. Vacancies without a numeric range are dropped when either is set. |
| `onlyWithSalary` | boolean | Drop vacancies without a numeric salary range. |
| `postedWithinDays` | integer | Keep vacancies posted in the last N days, Kyiv time (0 = today). |
| `maxExperienceYears` | integer | Keep vacancies asking for at most N years. Vacancies that state none pass. |
| `verifiedCompaniesOnly` | boolean | Keep only companies carrying work.ua's verified tick. |
| `includeKeywords`, `excludeKeywords` | array | Words the title, company or snippet must / must not contain, ignoring case and accents. |
| `proxyConfiguration` | object | Apify Unblocker (default, and the only transport measured to work). |

#### Sample input

```json
{
    "searchQueries": ["python"],
    "remoteOnly": true,
    "category": "it",
    "minSalary": 50000,
    "postedWithinDays": 14,
    "maxItems": 50
}
```

#### Output

One row per vacancy:

| Field | Type | Description |
|---|---|---|
| `id`, `url` | string | work.ua vacancy id and page URL. |
| `title` | string | Vacancy title. |
| `company` | string / null | Employer name. Null when work.ua shows `Компанія прихована` (company hidden); `companyHidden` is then true. |
| `companyVerified`, `companyIsVip` | boolean | work.ua's verified tick and VIP label. |
| `companyLogo` | string / null | Logo image URL. |
| `location` | string | City or place as shown (`Київ`, `Вся Україна`, `Дистанційно`). The distance suffix is split into `distanceFromCenterKm`. |
| `isRemote`, `isHybrid` | boolean | Location is `Дистанційно`; conditions list `Гібридний формат`. |
| `conditions` | array | The card's conditions line split on the dot: experience, employment type, schedule. |
| `experienceYearsMin` | integer / null | Years from `Досвід від N`, null when none is stated. |
| `suitableFor`, `studentsWelcome` | string / boolean | The card's `Кому підходить` line, and whether it names students. |
| `contactMethods` | array | Ways the employer accepts contact (`Телефон`, `Чат`). No numbers or addresses are collected. |
| `salaryText` | string / null | Salary as shown. |
| `salaryMin`, `salaryMax`, `salaryCurrency` | integer / string / null | The bold range in hryvnia (`UAH`); null when there is none. |
| `salaryAfterDeductions` | boolean / null | `Після всіх відрахувань` (net) shown next to the range. |
| `salaryNote` | string / null | The employer's remark next to the range. |
| `snippet` | string | Two-line excerpt from the listing. |
| `isHot`, `isNew`, `hasDeferment`, `salaryAboveAverage` | boolean | Labels `Гаряча`, `Нове`, `Бронювання`, `Вища за середню`. |
| `postedAt`, `postedDate` | string / null | Timestamp with Kyiv offset (null on hot cards, which show a date only) and the date `YYYY-MM-DD`. |
| `matchedKeyword`, `page` | string, integer | The query that returned the row (empty with no keyword) and its result page. |
| `scrapedAt` | string | ISO 8601 UTC timestamp. |

With **Fetch full vacancy pages** each row also has: `detailFetched` (false when the vacancy was closed or
unreachable, then the fields below are null or empty), `description`, `requirements`, `skills`, `workplace`,
`employmentSchedule`, `companyId`, `companyUrl`, `detailPostedAt`.

#### Sample output

Listing row, from a real run on 2026-10-21 (`scrapedAt` set to a fixed time):

```json
{
    "id": "8554021",
    "url": "https://www.work.ua/jobs/8554021/",
    "title": "Молоший оператор АЗК, заправник (р-н Корчуватого, подобово)",
    "company": "OKKO",
    "companyHidden": false,
    "companyVerified": false,
    "companyIsVip": true,
    "companyLogo": "https://assets.work.ua/employer_design/19423/company_logo_v8.png",
    "location": "Київ",
    "distanceFromCenterKm": 11,
    "isRemote": false,
    "isHybrid": false,
    "conditions": [
        "Позмінний графік: 1/3, 2/2"
    ],
    "experienceYearsMin": null,
    "suitableFor": "Готові взяти студентів, людей з інвалідністю, пенсіонерів",
    "studentsWelcome": true,
    "contactMethods": [
        "Телефон",
        "Чат"
    ],
    "salaryText": "16 000 грн",
    "salaryMin": 16000,
    "salaryMax": 16000,
    "salaryCurrency": "UAH",
    "salaryAfterDeductions": true,
    "salaryNote": "+чайові",
    "snippet": "ОККО — це місце, де робота кожного важлива та має значення. Тому без тебе ніяк! Долучайся до команди ОККО, формуймо надійний тил нашої країни разом! Шукаємо МОЛ…",
    "isHot": false,
    "isNew": true,
    "hasDeferment": false,
    "salaryAboveAverage": false,
    "postedAt": "2026-10-21T17:10:04+03:00",
    "postedDate": "2026-10-21",
    "matchedKeyword": "",
    "page": 27,
    "scrapedAt": "2026-10-21T12:00:00+00:00"
}
```

A row with **Fetch full vacancy pages** on (same day; `description` and `snippet` shortened here):

```json
{
    "id": "8337576",
    "url": "https://www.work.ua/jobs/8337576/",
    "title": "Фахівець з аналітики",
    "company": "Ekipazh, завод",
    "companyHidden": false,
    "companyVerified": true,
    "companyIsVip": false,
    "companyLogo": "https://assets.work.ua/employer_design/510829/company_logo_v2.png",
    "location": "Златопіль",
    "distanceFromCenterKm": null,
    "isRemote": false,
    "isHybrid": false,
    "conditions": [
        "Стандартний графік: 5/2"
    ],
    "experienceYearsMin": null,
    "suitableFor": null,
    "studentsWelcome": false,
    "contactMethods": [
        "Чат"
    ],
    "salaryText": "20 000 грн",
    "salaryMin": 20000,
    "salaryMax": 20000,
    "salaryCurrency": "UAH",
    "salaryAfterDeductions": true,
    "salaryNote": null,
    "snippet": "Вища освіта. Привіт! Ми — команда EKIPAZH, один із лідерів ринку світлопрозорих конструкцій в Україні, який вже понад 15…",
    "isHot": false,
    "isNew": true,
    "hasDeferment": true,
    "salaryAboveAverage": false,
    "postedAt": "2026-10-21T15:07:05+03:00",
    "postedDate": "2026-10-21",
    "matchedKeyword": "data analyst",
    "page": 1,
    "description": "Привіт! Ми — команда EKIPAZH, один із лідерів ринку світлопрозорих конструкцій в Україні, який вже понад 15 років створює якісний продукт, що прикрашає будівлі не лише в нашій країн. Запрошує до своєї…",
    "requirements": [
        "Вища освіта."
    ],
    "skills": [
        "MS Excel",
        "Користувач 1С",
        "Ведення управлінського обліку",
        "Користувач ERP-системи",
        "Формування зведених таблиць",
        "Уважність",
        "Планування бюджету",
        "Аналіз даних",
        "Економічний аналіз",
        "Аналіз вимог",
        "Робота з великим обсягом інформації",
        "Аудит"
    ],
    "workplace": "Златопіль",
    "employmentSchedule": "Повна зайнятість · Стандартний графік: 5/2",
    "companyId": "510829",
    "companyUrl": "https://www.work.ua/jobs/by-company/510829/",
    "detailPostedAt": "2026-10-21T15:07:05+03:00",
    "detailFetched": true,
    "scrapedAt": "2026-10-21T12:00:00+00:00"
}
```

### Limits

- **Work.ua's own filters are not used.** Its robots.txt disallows the filter parameters (`salaryfrom=`, `days=`,
  `experience=` and others), so the salary, date and experience filters here run on the rows after they are
  read. A narrow filter on a broad search therefore reads many pages for few rows; `maxPages` bounds it.
- **Page one of a broad search is mostly promoted vacancies.** In a 1,000-row run on the whole Kyiv listing
  (2026-10-21), 358 rows carried the `Гаряча` label and had no `postedAt`; the other 642 did.
- **Salary numbers are the bold range only.** In that run 675 of 1,000 rows had one and 762 had some salary
  text. Employer wording such as `40 000 грн (до оподаткування)` stays text.
- **Promoted inserts are skipped.** work.ua puts an extra sponsored card (the Defence Forces slot) into some
  result pages that its own count leaves out; it is not saved.
- **Search matching is the site's.** work.ua matches loosely: a search for `data analyst` returned accounting
  vacancies that list data analysis among their skills. Use `includeKeywords` to tighten it.
- A hyphen inside a search term can be read by work.ua as a city or category prefix. Write multi-word terms with spaces.
- Pages past the last page of a query come back empty; the Actor stops there.
- A run that saves zero rows fails instead of finishing empty, so a mistyped slug or an over-strict filter is visible.

### How fast is it?

About 7 seconds per result page of up to 14 vacancies. Measured 2026-10-21: 1,000 rows over 77 pages of the
Kyiv listing in 8 minutes 38 seconds. With **Fetch full vacancy pages** the Actor opens four vacancy pages at a time:
28 vacancies plus 2 result pages took 27 seconds.

### How much does it cost to scrape Work.ua?

Pay-per-event: **$0.001 per result** saved to the dataset (**$1.00 per 1,000 results**), plus **$0.00005** charged once
when the run starts. No subscription. Filters run before a row is saved, so rows they drop are not charged.

### FAQ

**Is a proxy required?** Yes. Work.ua sits behind a Cloudflare challenge: plain, datacenter and residential requests
got an HTTP 403 challenge page on 73 of 75 test pages (one page got through, one request failed), and Apify Unblocker returned a real page on 25 of 25 (2026-10-21).
The Actor defaults to Unblocker.

**Does it collect CVs, phone numbers or e-mail addresses?** No. It reads vacancy ads and the public employer names
shown on them. The phone-reveal and resume pages are never requested.

**Why is `postedAt` empty on some rows?** Work.ua shows a time only on non-promoted cards. Promoted (`isHot`) cards
carry a date, in `postedDate`.

**Why is `company` null?** The employer chose to hide its name; work.ua shows `Компанія прихована`. `companyHidden` is true.

**Does it cover Robota.ua or other Ukrainian job boards?** No. It reads work.ua only.

### More scrapers

| Actor | What it scrapes |
|---|---|
| [Gupy Jobs Brazil Scraper](https://apify.com/reventadata/gupy-jobs-scraper) | Careers and openings on Gupy, Brazil |
| [Amazon Product & Price Scraper](https://apify.com/reventadata/amazon-scraper) | Amazon product listings and prices |
| [Idealo Price Comparison Scraper](https://apify.com/reventadata/idealo-scraper) | Idealo price comparison listings |

# Actor input Schema

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

Job titles or keywords, one work.ua search per term (for example python, водій, sales manager). Leave empty to list every vacancy in the chosen city, category or remote listing. A hyphen inside a term can be read by work.ua as a city or category prefix, so write multi-word terms with spaces.

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

Total number of vacancies to save across all queries. Each result page holds up to 14 vacancies.

## `maxPages` (type: `integer`):

Result pages to walk per query (14 vacancies each). The Actor stops earlier at the last page the site has. The default of 10 gives up to 140 vacancies per query; raise it for broad terms.

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

work.ua city slug, the part of the city's own page address after /jobs-: kyiv, lviv, odesa, dnipro, kharkiv, zaporizhzhya, ivano-frankivsk. The full list is at work.ua/jobs/by-region/. An unknown slug returns no vacancies. Cannot be combined with Remote only.

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

Search work.ua's remote-work listing (/jobs-remote-...). Cannot be combined with City.

## `category` (type: `string`):

Restrict the search to one work.ua job category. Leave unset for all categories.

## `includeDetails` (type: `boolean`):

Open every vacancy page to add the full description, requirements, skills, work place, schedule and the company link. This costs one extra Unblocker request per vacancy, so runs are slower. Off: each row carries the two-line snippet from the listing.

## `minSalary` (type: `integer`):

Keep only vacancies whose bold salary range, in hryvnia, reaches at least this amount. Vacancies with no numeric salary are dropped when this is set. Applied after fetching.

## `maxSalary` (type: `integer`):

Keep only vacancies whose salary range starts at or below this amount, in hryvnia. Vacancies with no numeric salary are dropped when this is set. Applied after fetching.

## `onlyWithSalary` (type: `boolean`):

Drop vacancies that show no salary range in hryvnia (text such as Договірна, a dollar amount or an hourly rate is not a numeric range).

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

Keep only vacancies posted in the last N days, counted in Kyiv time (0 is today). Applied after fetching.

## `maxExperienceYears` (type: `integer`):

Keep only vacancies that ask for at most this many years of experience. Vacancies that state no experience pass. Applied after fetching.

## `verifiedCompaniesOnly` (type: `boolean`):

Keep only vacancies from companies carrying work.ua's own verified tick.

## `includeKeywords` (type: `array`):

Keep only vacancies whose title, company or snippet contains every one of these words, ignoring case and accents.

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

Drop vacancies whose title, company or snippet contains any of these words, ignoring case and accents.

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

Apify Unblocker is required: work.ua sits behind a Cloudflare challenge that no proxy, datacenter or residential proxy gets past.

## Actor input object example

```json
{
  "searchQueries": [
    "python"
  ],
  "maxItems": 20,
  "maxPages": 1,
  "remoteOnly": false,
  "includeDetails": false,
  "onlyWithSalary": false,
  "verifiedCompaniesOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Vacancies scraped from work.ua.

# 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": [
        "python"
    ],
    "maxItems": 20,
    "maxPages": 1,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "UNBLOCKER"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("reventadata/workua-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": ["python"],
    "maxItems": 20,
    "maxPages": 1,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["UNBLOCKER"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("reventadata/workua-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": [
    "python"
  ],
  "maxItems": 20,
  "maxPages": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}' |
apify call reventadata/workua-scraper --silent --output-dataset

```

## MCP server setup

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