# NoFluffJobs Scraper (`datawatcher/nofluffjobs-scraper`) Actor

Scrapes job listings from NoFluffJobs.com (Poland, Czechia, Slovakia, Hungary, the Netherlands, Ukraine) via its public JSON search API.

- **URL**: https://apify.com/datawatcher/nofluffjobs-scraper.md
- **Developed by:** [DataWatcher](https://apify.com/datawatcher) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 job scrapeds

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

## NoFluffJobs Scraper

Extract job listings from [NoFluffJobs.com](https://nofluffjobs.com) as clean, structured
data from any of its markets: Poland, Czechia, Slovakia, Hungary, the Netherlands and
Ukraine.

### Why this one

- **The lowest per-row price, no start fee.** $0.40 per 1,000 jobs. You pay only for rows you get.
- **See when pay is revealed.** Many listings no longer publish a number. Each row says whether the salary is published, and if not, at which interview stage it's revealed (`salaryDisclosureStage`), instead of a blank.
- **Every market, filtered by the site itself.** Poland, Czechia, Slovakia, Hungary, the Netherlands and Ukraine.
- **Filters buyers ask for:** city, category, seniority, technology, contract type, posting language, salary range, posted date, and exclusions. Paste any NoFluffJobs search URL, or job URLs.
- **Salaries you can compare:** a monthly figure for every range, or every salary converted to one currency by NoFluffJobs.
- **Rich details when you want them:** must-have and nice-to-have skills, all salary ranges, daily tasks, benefits, company size and website. The description comes as text, HTML and Markdown.
- **Honest runs:** a run summary, a health status, and a daily health-check mode. Failed searches show up as unbilled diagnostic rows instead of silently returning nothing.
- **Fast and cheap:** it uses NoFluffJobs' own JSON API, with no browser.
- **No personal data:** no recruiter names, emails or phone numbers.

### Use with AI agents (MCP)

AI assistants (Claude, ChatGPT, Cursor…) can run this actor through Apify's MCP server. Connect the server in your assistant, then ask, for example:

> Use the NoFluffJobs Scraper to find senior Python jobs in Kraków or fully remote,
> published in the last 3 days, with salaries in EUR per month. Show the 10 best-paid,
> with company, salary and link.

For agents, set `compact: true` to get short rows: title, company, link, salary, cities, remote, seniority, tech stack, date.

### Input

Example: senior Python jobs in Kraków, posted in the last 3 days, salaries in EUR per month, with details:

```json
{
  "searchQueries": ["python"],
  "cities": ["Kraków"],
  "seniority": ["senior"],
  "postedWithin": "3d",
  "salaryCurrency": "EUR",
  "maxResults": 100,
  "includeDetails": true
}
```

Or paste a search URL straight from the site:

```json
{
  "searchUrls": ["https://nofluffjobs.com/pl/praca-zdalna/python?criteria=seniority%3Dsenior"],
  "maxResults": 50
}
```

Main fields:

| Field | Default | Notes |
| --- | --- | --- |
| `searchQueries` | – | One search each; with no query and no URL, `python developer` is searched. `react OR angular` runs both and merges the results |
| `searchUrls` | – | Any NoFluffJobs search URL (path filters and `?criteria=` both applied), or a job URL (`…/job/<slug>`) to get that job directly. A URL's own market (`/pl/`, `/cz/`…) overrides `country`, with a warning if they differ |
| `country` | `all` | `all`, `PL`, `CZ`, `SK`, `HU`, `NL`, `UA`. Filtered by the site itself; `all` searches the Polish market, the widest |
| `maxResults` | `50` | Rows in total. `0` = no limit (the request budget and run timeout still apply) |
| `includeDetails` | `false` | One extra request per job for the detail fields below (extra event, see Pricing). The old name `includeFullDescription` still works |

*Filters:*

| Field | Notes |
| --- | --- |
| `remoteOnly` | Only fully remote jobs |
| `cities` | e.g. `Warszawa`, `Kraków`; `Warsaw`, `Cracow`, `Krakow` also work |
| `categories`, `seniority`, `contractTypes`, `postingLanguages` | Pick from the site's own lists |
| `technologies` | e.g. `Python`, `React` |
| `postedWithin` | `24h`, `3d`, `2w`… |
| `excludeTitles`, `excludeCompanies` | Skip jobs whose title / company contains any of these |

*Salary:*

| Field | Notes |
| --- | --- |
| `salaryCurrency` | Convert every salary to `PLN`, `EUR`, `USD`, `CHF`, `GBP`, `HUF`, `CZK`, `UAH` or `BYN` per month; the site converts. `original` keeps each listing's own |
| `minSalaryMonthlyPln`, `maxSalaryMonthlyPln` | The site's own salary filter, monthly PLN |
| `withSalaryOnly` | Skip jobs without a published salary |
| `onlyCurrencies` | Keep jobs quoted in these currencies |

*Output:*

| Field | Notes |
| --- | --- |
| `descriptionMaxLength` | Cut the text and Markdown descriptions. `0` = no limit |
| `onlyUniqueJobs` | `true` (default): one row per job. `false`: one row per office location, each billed |
| `compact` | Only title, company, URL, salary, cities, remote, seniority, tech stack, posted date |
| `includeRawRecord` | Attach the raw search rows and detail response as `raw` |

*Limits & advanced:*

| Field | Notes |
| --- | --- |
| `maxResultsPerQuery` | Row cap per search. `0` = none |
| `maxPages` | Result pages (20 jobs each) per search. `0` = no limit |
| `maxRequests` | Hard ceiling on requests. `0` = automatic: derived from the input (search pages + one per job detail + 20% for retries), at most 1,000 |
| `proxyConfiguration` | Optional Apify proxy. Not needed for normal use today |
| `healthCheck` | Monitoring mode: a 5-row search that fails the run if any row breaks a data rule. Schedule it daily |

The run also stops at the platform's run timeout, keeping 30 seconds to save its records.

### Output

A real row (a pasted job URL with `includeDetails: true`; long texts and lists shortened here):

```json
{
  "recordType": "job",
  "jobId": "EQDYWOYL",
  "title": "Guidewire Policy Center Developer",
  "company": "Link Group",
  "url": "https://nofluffjobs.com/pl/job/guidewire-policy-center-developer-link-group-warszawa-1",
  "category": "backend",
  "techStack": ["Object-oriented programming", "REST API", "SQL", "Relational database"],
  "seniority": ["Mid"],
  "companyLogoUrl": "https://static.nofluffjobs.com/companies/logos/original/link_group_240px_20240110_120451.png",
  "salaryDisclosed": true,
  "salaryMin": 150.0,
  "salaryMax": 250.0,
  "currency": "PLN",
  "salaryPeriod": "Hour",
  "salaryMonthlyMin": 25200.0,
  "salaryMonthlyMax": 42000.0,
  "salaryText": "150–250 PLN / hour (b2b)",
  "contractType": "b2b",
  "salaryDisclosureStage": null,
  "cities": ["Warszawa"],
  "locations": [
    {"city": "Warszawa", "street": null, "postalCode": null, "province": null,
     "countryCode": "POL", "latitude": 52.2296756, "longitude": 21.0122287}
  ],
  "countries": ["POL"],
  "locationCount": 17,
  "remote": true,
  "postedAt": "2026-09-23T13:48:14.868000+00:00",
  "daysSincePosted": 0,
  "fullDescription": "About the role\nWe are looking for an experienced Guidewire developer…",
  "descriptionMarkdown": "### About the role\n\nWe are looking for an experienced Guidewire…",
  "mustHaveSkills": ["Object-oriented programming", "REST API", "SQL", "Relational database"],
  "niceToHaveSkills": ["integration technologies"],
  "spokenLanguages": [{"code": "en", "level": "B2", "required": true}],
  "dailyTasks": ["Develop and configure new functionality within Guidewire PolicyCenter…"],
  "salaryRanges": [{"contractType": "b2b", "min": 150.0, "max": 250.0, "currency": "PLN", "period": "Hour", "disclosed": true}],
  "companySize": "40+",
  "companyWebsite": "http://linkgroup.pl/",
  "companyProfileUrl": "https://nofluffjobs.com/company/link-group-ccywok00",
  "status": "PUBLISHED",
  "expiresAt": "2026-10-23T23:59:59",
  "detailFetchStatus": "success",
  "dataQualityWarnings": []
}
```

With `compact: true` a row has only `jobId`, `title`, `company`, `url`, `salaryMin`, `salaryMax`, `currency`, `salaryPeriod`, `cities`, `remote`, `seniority`, `techStack`, `postedAt`.

All fields (each dataset row is a job, `recordType: "job"`):

| Field | Description |
| --- | --- |
| `jobId`, `postingId` | Stable job reference (`jobId`) and the raw per-listing id |
| `title`, `company`, `url` | `url` is the job page in the searched market, `https://nofluffjobs.com/<market>/job/<slug>`; the same on every run |
| `allOfferUrls` | Every per-office URL the site has for this job |
| `category`, `technology`, `techStack`, `seniority` | The site's category, main technology, required skills and levels |
| `companyLogoUrl` | Full logo image URL |
| `salaryDisclosed`, `salaryMin`, `salaryMax`, `currency`, `salaryPeriod` | Numbers only when the listing publishes a range; never guessed |
| `salaryMonthlyMin`, `salaryMonthlyMax` | The range as a monthly figure (Hour ×168, Day ×21, Year ÷12) |
| `salaryText` | e.g. `150–180 PLN / hour (b2b)` or `revealed at first interview (permanent)` |
| `contractType`, `salaryFlexibleUpperBound` | e.g. `b2b`; whether the top of the range is negotiable |
| `salaryCheckOnly`, `salaryDisclosureStage` | When no range is published: the interview stage where pay is revealed |
| `cities` | City names, one spelling per city (`Warsaw` → `Warszawa`) |
| `locations` | One object per office or province: `city`, `street`, `postalCode`, `province`, `countryCode`, `latitude`, `longitude` |
| `countries`, `locationCount`, `provinces` | Office country codes, number of locations, province-only entries |
| `remote`, `hybridDescription` | The site's "remote" badge; the hybrid note, if any |
| `onlineInterviewAvailable`, `highlighted`, `topInSearch`, `searchBoost`, `help4Ua` | The listing's flags |
| `regions` | The site's own `regions` list (currently always `["pl"]`, so not a market marker) |
| `postedAt`, `renewedAt`, `daysSincePosted`, `scrapedAt` | Dates; compare `renewedAt` with `postedAt` to spot a repost |
| `searchQuery` | The query or URL that found this row |

With `includeDetails`:

| Field | Description |
| --- | --- |
| `fullDescription`, `descriptionHtml`, `descriptionMarkdown`, `descriptionSnippet` | The description as text, cleaned HTML, Markdown, and the first 300 characters |
| `requirementsDescription`, `mustHaveSkills`, `niceToHaveSkills`, `spokenLanguages` | Requirements |
| `dailyTasks`, `benefits`, `officePerks`, `equipment`, `recruitmentSteps` | The job itself |
| `salaryRanges` | Every contract type's range: `contractType`, `min`, `max`, `currency`, `period`, `disclosed` |
| `companySize`, `companyWebsite`, `companyProfileUrl` | Company basics |
| `status`, `expiresAt` | Listing status and expiry |
| `detailFetchStatus`, `dataQualityWarnings` | `success` / `failed` / `skipped`; notes such as `salary not disclosed` |

- Every field is present on every row, as `null` / `[]` when the source has no value.
- No recruiter names or contact emails are ever included.
- A search that fails completely writes one `recordType: "diagnostic"` row with `searchQuery` and `errorMessage`. It is never billed.

**Run records.** Each run also writes two records to its default key-value store:

- **`RUN_SUMMARY`:** rows, requests, the request budget, retries, failed requests, why it stopped early, warnings, billing events, data-rule problems.
- **`RUN_HEALTH`:** `{status, reason}`, one of:
  - `ok`
  - `degraded`: no rows, a broken rule, a failed request, an early stop, or a warning
  - `failed`: health-check mode only

The data rules:

- no more rows than `maxResults`
- no duplicate URLs
- URLs in the searched market
- remote rows when `remoteOnly` is on
- salary fields set exactly when disclosed
- the requested `salaryCurrency`
- descriptions are plain text. A listing the employer left blank gets the warning `the listing has no description`.

### Pricing (Pay-Per-Event)

| Event | Price | Trigger |
| --- | --- | --- |
| Result row | $0.0004 (= $0.40 / 1,000) | Per job returned |
| Job-details row (`job-details-scraped`) | +$0.0015 | Only when `includeDetails` is on and the job's details were fetched |

No start fee: a run costs only the rows it returns. Diagnostic rows are never billed.

### Example use cases

- Track new openings for a role/stack across five markets in one run
- Feed a job board aggregator or a personal job-search dashboard
- Monitor salary transparency trends by market or company

# Changelog

This Actor's version history is a separate document: https://apify.com/datawatcher/nofluffjobs-scraper/changelog.md

# Actor input Schema

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

Keyword/title searches, each run separately. `react OR angular` runs both and merges the results. Leave empty when using only Search URLs; with nothing at all, `python developer` is searched.

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

Paste NoFluffJobs search URLs to reuse filters set on the site (path filters and ?criteria= are both applied), or job URLs (…/job/<slug>) to get those jobs directly. A URL's own market (/pl/, /cz/...) overrides `country` for that search.

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

Market to search. NoFluffJobs filters each market server-side from one shared catalogue; `All` searches the Polish market, the widest one.

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

Rows to write in total. 0 = no limit (the request budget and the run timeout still apply).

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

One extra request per job to NoFluffJobs' job-detail API: full description (text, HTML, Markdown), requirements, must-have / nice-to-have skills, languages, daily tasks, benefits, all salary ranges, company size, website and profile link, status and expiry date. Priced as a separate PPE event.

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

Only return fully remote postings.

## `cities` (type: `array`):

Only jobs in these cities, e.g. Warszawa, Kraków (Warsaw / Cracow / Krakow also work).

## `categories` (type: `array`):

Only jobs in these categories.

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

Only jobs at these levels.

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

Only jobs requiring any of these, e.g. Python, React.

## `contractTypes` (type: `array`):

b2b, permanent (UoP), zlecenie, uod (contract for specific work), intern.

## `postingLanguages` (type: `array`):

Only job ads written in these languages.

## `postedWithin` (type: `string`):

Only jobs posted in the last period: e.g. 24h, 3d, 2w.

## `excludeTitles` (type: `array`):

Skip jobs whose title contains any of these (case-insensitive).

## `excludeCompanies` (type: `array`):

Skip jobs from companies whose name contains any of these.

## `salaryCurrency` (type: `string`):

Convert every salary into this currency, per month (NoFluffJobs converts server-side). `Original` keeps each listing's own currency and period.

## `minSalaryMonthlyPln` (type: `integer`):

Only jobs paying at least this much per month, in PLN (the site's own salary filter).

## `maxSalaryMonthlyPln` (type: `integer`):

Only jobs paying at most this much per month, in PLN.

## `withSalaryOnly` (type: `boolean`):

Skip jobs that don't publish a salary range.

## `onlyCurrencies` (type: `array`):

Keep only jobs whose salary is quoted in these currencies, e.g. PLN, EUR.

## `descriptionMaxLength` (type: `integer`):

Cut descriptions (text and Markdown) to this many characters. 0 = no limit.

## `onlyUniqueJobs` (type: `boolean`):

Off: one row per office location instead (each billed as a row).

## `compact` (type: `boolean`):

Only the key fields: title, company, URL, salary, cities, remote, seniority, tech stack, posted date. Good for AI agents.

## `includeRawRecord` (type: `boolean`):

Attach the raw search rows and detail response as `raw`.

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

Row cap per search query or URL. 0 = no per-search cap.

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

Result pages (20 jobs each) to read per search. 0 = no limit.

## `maxRequests` (type: `integer`):

Hard ceiling on requests to NoFluffJobs in one run. 0 = automatic (derived from the input, max 1000).

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

Optional. Route requests through an Apify proxy. Not needed for normal use — NoFluffJobs' public search API has not shown any blocking so far.

## `healthCheck` (type: `boolean`):

Monitoring mode: runs a 5-row search and fails the run if any row breaks a data rule (see RUN\_HEALTH in the key-value store). Meant for a daily schedule.

## Actor input object example

```json
{
  "searchQueries": [
    "python developer"
  ],
  "searchUrls": [],
  "country": "all",
  "maxResults": 50,
  "includeDetails": false,
  "remoteOnly": false,
  "cities": [],
  "categories": [],
  "seniority": [],
  "technologies": [],
  "contractTypes": [],
  "postingLanguages": [],
  "excludeTitles": [],
  "excludeCompanies": [],
  "salaryCurrency": "original",
  "withSalaryOnly": false,
  "onlyCurrencies": [],
  "descriptionMaxLength": 0,
  "onlyUniqueJobs": true,
  "compact": false,
  "includeRawRecord": false,
  "maxResultsPerQuery": 0,
  "maxPages": 0,
  "maxRequests": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "healthCheck": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per job (recordType "job"); a search that failed completely adds one unbilled recordType "diagnostic" row.

## `runSummary` (type: `string`):

Rows written, requests used and the request budget, retries, failed requests, early stops, warnings, billing events, data-rule problems.

## `runHealth` (type: `string`):

{status, reason}: ok, degraded, or failed (health-check mode only).

# 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 developer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datawatcher/nofluffjobs-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 developer"] }

# Run the Actor and wait for it to finish
run = client.actor("datawatcher/nofluffjobs-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 developer"
  ]
}' |
apify call datawatcher/nofluffjobs-scraper --silent --output-dataset

```

## MCP server setup

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