# Nordic Jobs Scraper & Alerts: Norway, Sweden, Finland, Denmark (`tidy-data/nordic-jobs-scraper`) Actor

Scrape job listings from NAV (Norway), Arbetsförmedlingen Platsbanken (Sweden) and EURES (Finland, Denmark, Iceland) via official public APIs. Daily job alerts with only new jobs to Slack, webhook or email. One clean schema, export CSV/JSON.

- **URL**: https://apify.com/tidy-data/nordic-jobs-scraper.md
- **Developed by:** [Tidy Data](https://apify.com/tidy-data) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.90 / 1,000 jobs

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

## Nordic Jobs Scraper & Alerts: Norway, Sweden, Finland, Denmark

This **Nordic jobs scraper** collects job listings from official public job sources for **Norway, Sweden, Finland, Denmark and Iceland** and turns them into one clean dataset: Norway from **NAV Arbeidsplassen**, Sweden from **Arbetsförmedlingen Platsbanken** (JobTech open-data API), and Finland, Denmark and Iceland from **EURES**, the European Commission's job portal. Turn on **job alerts** to get only the jobs that are new since the last run, delivered to Slack, a webhook or e-mail. Search by keyword, location and posting date, and export to JSON, CSV or Excel.

- 🌍 **Five countries, one schema.** NAV, Platsbanken and EURES ads land in the same fields: job title, company, location, posted date, application deadline, employment type, full description and apply link.
- 🏛️ **Official public APIs only.** No login, no browser, no fragile HTML scraping. It uses NAV's public job feed and search, the JobTech open-data API behind Platsbanken, and the public EURES search.
- 🧹 **Clean, de-duplicated data.** Dates in ISO 8601 (UTC), normalized employment and contract types, plain-text descriptions, and the employer's organisation number where it is available. Reposted duplicates are removed.
- ⚡ **Fast and light.** In our tests, about 1,000 jobs from four countries (with full descriptions) took under 3 minutes on 256 MB of memory.
- 🔔 **Built-in job alerts.** Turn on `onlyNewJobs`, schedule the Actor, and get only new jobs by Slack, e-mail or webhook. Jobs you already got are never charged again.
- 💸 **Pay only for results.** You pay per job delivered. Duplicates, filtered-out ads and failed requests are never charged.
- 🔒 **Contact details masked by default.** The Actor does not extract contact persons into separate fields, and e-mail addresses and phone numbers in the ad texts are replaced with `[email removed]` / `[phone removed]` (`redactContacts`, on by default). Apply links and company websites are kept (see [Personal data](#does-the-output-contain-personal-data)).

### Scrape jobs in Norway

Norway uses **NAV Arbeidsplassen**, Norway's public job board, via NAV's search and its public job feed. Ads that NAV imports from FINN.no come without the full text (see [Limits](#limits-you-should-know)). Set `countries: ["NO"]` and search in Norwegian or English, e.g. `utvikler` or `developer`.

### Scrape jobs in Sweden

Sweden uses **Arbetsförmedlingen Platsbanken** through the JobTech open-data API (often searched for as the "Arbetsförmedlingen API"). Every ad has a full description. Platsbanken's search does not translate, so use Swedish terms such as `sjuksköterska` or `utvecklare`.

### Finland and Denmark job listings

Finland, Denmark (and Iceland) use **EURES**, the European Commission's job portal. Most Finnish ads come from Työmarkkinatori (Job Market Finland), and Danish ads are those that Jobnet and Danish employers share with EURES. EURES searches across languages.

### What can you use it for?

- **Job market research:** compare demand for a role (e.g. nurses, developers, electricians) across Norway, Sweden, Finland and Denmark.
- **Recruitment and staffing agencies:** find companies that are hiring right now, with the organisation number for CRM enrichment.
- **Job alerts and job boards:** get notified about new jobs in Norway, Sweden, Finland or Denmark by Slack, e-mail or webhook (see [Job alerts](#job-alerts-new-jobs-only)), or push them to Google Sheets, a database or your own site.
- **Salary and skills analysis, HR tech and AI:** feed structured job ads into dashboards, LLM pipelines or RAG search.
- **Relocation and talent mobility:** list English-language jobs in the Nordics for international candidates.

### Input fields

| Field | Description |
|---|---|
| `countries` | Any of `NO`, `SE`, `FI`, `DK`, `IS`. Default: Norway, Sweden, Finland and Denmark. |
| `searchTerms` | Keywords, e.g. `["developer"]` or local job titles `["sykepleier", "sjuksköterska", "sairaanhoitaja", "sygeplejerske"]`. Every term is searched in every selected country and duplicates are removed. Leave empty to get the newest jobs of any kind. |
| `keywordMatch` | How strictly a search term must match. `title` (default): the term must appear in the job title, the standardized job title or the occupation. `fullText`: anywhere in the ad, including the description. `source`: keep whatever the job site's own search returns (broadest, often loose). See [Keyword matching](#keyword-matching). |
| `location` | Optional city, municipality or region, e.g. `Oslo`, `Bergen`, `Göteborg`, `Tampere`, `Aarhus`. Select the matching country for best results. |
| `postedWithinDays` | Only jobs published in the last N days, e.g. `1` for a daily schedule. |
| `postedAfter` | Optional `YYYY-MM-DD`. Only jobs published on or after this date. |
| `maxItemsPerCountry` | Stop after this many jobs per country. Default 100. `0` = all available. |
| `maxItems` | Optional cap for the whole run. `0` = no cap. |
| `includeDescription` | Fetch the full ad (description, apply URL, deadline, employer details). Default on. Turn off for a faster run with search data only (Sweden and the EURES countries still include the description). |
| `includeDescriptionHtml` | Also return the original HTML description. Default off. |
| `dedupeByTitleAndCompany` | Remove reposted ads with the same title, company and city. Default on. |
| `skipFinnImportedAds` | Norway only: skip ads that NAV imports from FINN.no, which come without a full description (see Limits). Default off. |
| `redactContacts` | Replace e-mail addresses and phone numbers in the ad texts with `[email removed]` / `[phone removed]` (see [Personal data](#does-the-output-contain-personal-data)). Default on. |
| `onlyNewJobs` | Job alert mode: return only jobs that previous runs of the same alert have not returned yet. See [Job alerts](#job-alerts-new-jobs-only). Default off. |
| `alertName` | Name of the alert, e.g. `oslo-developers`. Each name keeps its own memory of seen jobs. Use one name per search. |
| `resetAlert` | Forget the jobs this alert has seen and start a new baseline. Default off. |
| `webhookUrl` | POST a JSON summary plus the job list to this URL when the run finds at least 1 job (Zapier, Make, n8n, your own API). |
| `slackWebhookUrl` | Slack incoming webhook URL. Posts a message with the newest jobs and links when the run finds at least 1 job. |
| `emailTo` | Send an e-mail with the newest jobs to your address when the run finds at least 1 job. Must be the e-mail address of your Apify account (an Apify rule). Adds a very small platform cost, see below. |
| `notifyTopN` | How many jobs to list in the Slack message and e-mail. Default 10, max 40. |
| `proxyConfiguration` | Apify Proxy (datacenter) by default, used only for the NAV search, which rate-limits shared cloud IP addresses. All other sources connect directly. |

Example:

```json
{
  "countries": ["NO", "SE", "FI", "DK"],
  "searchTerms": ["developer", "utvikler", "utvecklare"],
  "postedWithinDays": 7,
  "maxItemsPerCountry": 200
}
```

### Job data fields

One dataset item per job:

```json
{
  "jobId": "platsbanken-31686710",
  "source": "Arbetsförmedlingen Platsbanken",
  "country": "SE",
  "countryName": "Sweden",
  "title": "Lärare i SV/SVA till Klingsborgsskolan",
  "jobTitle": "Lärare i grundskolan, årskurs 7-9",
  "company": "Norrköpings kommun",
  "companyId": "2120000456",
  "location": "Norrköping, Östergötlands län",
  "city": "Norrköping",
  "region": "Östergötlands län",
  "postedAt": "2026-11-04T16:39:15Z",
  "deadline": "2026-11-22",
  "employmentType": "full-time",
  "contractType": "permanent",
  "numberOfPositions": 1,
  "occupation": "Grundskollärare",
  "salary": null,
  "salaryType": "Fast månads- vecko- eller timlön",
  "description": "Har du förståelse för att människor tar till sig kunskap på olika sätt ...",
  "applyUrl": "https://norrkoping.se/jobba-hos-oss/sok-jobb/lediga-jobb?rmpage=job&rmjob=20019",
  "url": "https://arbetsformedlingen.se/platsbanken/annonser/31686710",
  "scrapedAt": "2026-11-04T17:03:47Z"
}
```

| Field | Meaning |
|---|---|
| `jobId` / `sourceJobId` | Stable ID (source prefix + the source's own ID). Use it to de-duplicate across scheduled runs. |
| `source` / `originalSource` | `NAV Arbeidsplassen`, `Arbetsförmedlingen Platsbanken` or `EURES`, and where the ad was first published (e.g. `NAV`, `FINN.no`, `Platsbanken`, `Työmarkkinatori (Finland)`, `Jobnet (Denmark)`). |
| `country` / `countryName` | ISO country code and name. |
| `title` | Ad headline as the employer wrote it. |
| `jobTitle` | Standardized job title. Norway: the employer's job title field, or NAV's own classification (JANZZ/ESCO) when that field is empty. Sweden: the occupation from the JobTech taxonomy (e.g. `Systemutvecklare/Programmerare`). Finland/Denmark/Iceland: the ESCO occupation of the ad, in English (e.g. `software developer`). |
| `occupation` | Broader occupation group. Norway: NAV category (e.g. `Utvikling`). Sweden: SSYK occupation group. EURES: ISCO group from ESCO (e.g. `Software developers`). |
| `company` / `companyId` / `companyWebsite` | Employer name, organisation number (Norwegian orgnr, Swedish organisationsnummer, Finnish Y-tunnus, Danish CVR) and website, where available. |
| `location` / `city` / `municipality` / `region` / `postalCode` / `address` | Workplace. For EURES, `region` holds NUTS region codes. |
| `postedAt` / `updatedAt` / `expiresAt` | ISO 8601 timestamps in UTC. |
| `deadline` | Application deadline as `YYYY-MM-DD`, or the source's text (e.g. Norwegian `Snarest` = as soon as possible). |
| `employmentType` | `full-time` or `part-time`. |
| `contractType` / `contractTypeOriginal` | Normalized (`permanent`, `temporary`, `seasonal`, ...) and original wording. |
| `salary` / `salaryType` | None of the public Nordic job APIs publishes a structured salary amount. `salaryType` is Sweden's pay type (e.g. `Fast månads- vecko- eller timlön` = fixed monthly, weekly or hourly pay). `salary` is filled only when Platsbanken has a salary text, which is rare. NAV and EURES have no salary field; look in `description`. |
| `numberOfPositions`, `startDate`, `sector`, `occupationField` | Extra details where the source provides them. |
| `summary`, `keywords`, `workLanguage` | Norway: NAV's short summary, skill tags and working languages. |
| `description` / `descriptionHtml` | Full ad text (plain text, plus HTML if enabled). |
| `applyUrl` / `url` | Where to apply, and the public ad page. |
| `searchTerm`, `scrapedAt` | The search term that found the job, and when it was scraped. |

#### Field coverage by source

| Field | Norway (NAV) | Sweden (Platsbanken) | Finland / Denmark / Iceland (EURES) |
|---|---|---|---|
| `jobTitle` | always | always | most ads, about 85% (ESCO, English) |
| `occupation` | always | always | most ads, about 85% (ISCO group) |
| `salaryType` | - | always | - |
| `salary` (amount text) | - | rare (0 of 400 in our tests) | - |
| `summary`, `keywords`, `workLanguage` | yes | - | - |
| `description` | only ads not imported from FINN.no (about 30% in our tests) | yes | yes |

A summary per country (jobs, duplicates skipped, filtered-out ads, keyword mismatches, errors, timing, plus the alert state, notification results and `contactsRedacted` counts) is saved to the key-value store as `RUN_SUMMARY`. Download the dataset as JSON, CSV, Excel, XML or HTML, or read it through the Apify API.

### Job alerts (new jobs only)

Use the Actor as a **job alert for Norway, Sweden, Finland, Denmark and Iceland**. Save a search, run it on a schedule, and get **only the new jobs** since the last run, straight to Slack, your inbox or any webhook. It works for any role or city: "new developer jobs in Oslo", "new nursing jobs in Sweden", "new jobs in Helsinki", "Danish engineering jobs this week".

#### How it works

1. Turn on **`onlyNewJobs`** and give the alert a name in **`alertName`** (e.g. `oslo-developers`).
2. The Actor remembers the IDs of the jobs it returned in a named key-value store in **your own Apify account** (`nordic-jobs-alert-<alertName>`). It keeps the latest ~50,000 job IDs per alert.
3. On every next run, jobs that the alert has already seen are skipped. **They are not saved and not charged.** Reposts of the same ad (same title, company and city with a new ID) are skipped too.
4. When the run finds at least 1 new job, it sends a notification to the channels you set. When there is nothing new, the run succeeds with the status "No new jobs since the last run" and sends nothing.

**The first run is the baseline.** An alert with no history returns the current matching jobs (limited by `postedWithinDays`, `maxItemsPerCountry` and `maxItems`) and notifies about them, so you can check the alert works. It also remembers recent matching jobs beyond your limits, without returning or charging them, so the second run does not flood you with old jobs. From the second run on, you only get jobs that are new. Use `postedWithinDays: 1` or `2` for the baseline if you don't want a big first batch.

#### Set up a daily job alert with an Apify Schedule

1. Open this Actor in the Apify Console, fill in your search, turn on **Only new jobs**, set an **Alert name** and the notification channels, then click **Save** (or **Save as a new task** to keep several alerts).
2. Go to **Schedules → Create new**, pick a time (e.g. every day at 07:00, cron `0 7 * * *`, and choose your time zone, e.g. Europe/Oslo or Europe/Stockholm).
3. Under **Actors and tasks**, add this Actor or your saved task with the same input, and save the schedule.

Example input for a daily "new developer jobs in Norway and Sweden" alert:

```json
{
  "countries": ["NO", "SE"],
  "searchTerms": ["developer", "utvikler", "utvecklare"],
  "postedWithinDays": 2,
  "maxItemsPerCountry": 200,
  "onlyNewJobs": true,
  "alertName": "nordic-developers",
  "slackWebhookUrl": "https://hooks.slack.com/services/T000/B000/XXXX",
  "emailTo": "my-apify-account-email@example.com",
  "notifyTopN": 10
}
```

Use one `alertName` per search. If you change the search terms or countries a lot, use a new name (or `resetAlert: true` once) so the alert starts a fresh baseline. Don't run two runs of the same alert at the same time.

#### Notification channels

- **Slack job alerts:** create an [incoming webhook](https://api.slack.com/messaging/webhooks) for your channel and paste it into `slackWebhookUrl`. The message shows the number of new jobs, the newest `notifyTopN` jobs with title, company, location, posted date, deadline and a link, plus links to the full dataset and the run.
- **E-mail job alerts:** put the e-mail address of your Apify account into `emailTo`. The e-mail is sent through Apify's official `apify/send-mail` Actor, which only delivers to the account owner's own address (to alert a team, use the Slack or webhook channel, or forward the e-mail). It starts a tiny extra run in your account. That run is billed as normal platform usage, typically well under $0.01 per e-mail, and only when there are new jobs.
- **Webhook (Zapier, Make, n8n, your API):** set `webhookUrl` and receive a POST with JSON like this:

```json
{
  "event": "nordic-jobs.jobs-found",
  "alertName": "nordic-developers",
  "isBaselineRun": false,
  "summary": "[nordic-developers] 12 new jobs for \"developer, utvikler, utvecklare\" in Norway, Sweden",
  "jobCount": 12,
  "countries": { "NO": 7, "SE": 5 },
  "runId": "...", "runUrl": "https://console.apify.com/...",
  "datasetId": "...", "datasetUrl": "https://console.apify.com/storage/datasets/...",
  "jobsTruncated": false,
  "jobs": [
    { "jobId": "platsbanken-31686710", "country": "SE", "title": "...", "company": "...", "location": "...",
      "postedAt": "2026-11-04T16:39:15Z", "deadline": "2026-11-22", "employmentType": "full-time",
      "contractType": "permanent", "applyUrl": "...", "url": "...", "source": "Arbetsförmedlingen Platsbanken", "searchTerm": "utvecklare" }
  ],
  "createdAt": "2026-11-05T06:00:41Z"
}
```

The webhook lists up to 500 jobs without descriptions. Read the full data from the dataset `datasetId` with the Apify API (`https://api.apify.com/v2/datasets/<datasetId>/items`). Failed notifications (e.g. an invalid Slack URL) are retried, then logged in the run log and in `RUN_SUMMARY.notifications`. They never fail the run, and your jobs are always in the dataset.

You can also use the notifications without `onlyNewJobs`. Then every run with results notifies about all jobs it found. Apify's own integrations (Slack, e-mail, Google Sheets, webhooks on the Integrations tab) work too, but they fire on every run, even when nothing is new.

#### Job alert limits

- After the baseline, each run looks at ads published since the last successful run of that country, minus a 24-hour safety margin. If you pause the schedule for a week, the next run still picks up everything published in between, as long as `postedWithinDays` / `postedAfter` and your item limits allow it. Leave `postedWithinDays` empty for alerts if runs may be skipped.
- If the run's maximum cost is reached, the remaining new jobs are not marked as seen. They come in the next run.
- A country added to an existing alert starts with its own baseline.
- An ad that the source edits keeps its ID, so it is not sent again.

### Jobs API in Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("tidy-data/nordic-jobs-scraper").call(run_input={
    "countries": ["NO", "SE", "FI", "DK"],
    "searchTerms": ["developer", "utvikler", "utvecklare"],
    "postedWithinDays": 7,
    "maxItemsPerCountry": 200,
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["country"], job["title"], job["company"], job["url"])
```

### How much does it cost?

You pay **$0.90 per 1,000 jobs** saved to the dataset (pay per event, see the Pricing tab), plus a tiny per-run start fee. Platform usage is included in the price. Duplicates, ads removed by your filters, jobs a job alert has already seen and failed requests are never charged. A scheduled alert run with no new jobs costs only the start fee. You can cap spending with the run's maximum cost setting, and the Actor stops cleanly when the cap is reached.

### Data sources and coverage

| Country | Source | Notes |
|---|---|---|
| 🇳🇴 Norway | NAV Arbeidsplassen (search + NAV's public job feed) | Norway's public job board, tens of thousands of open ads. |
| 🇸🇪 Sweden | Arbetsförmedlingen Platsbanken via the JobTech open-data API | Sweden's public job board. Full description in every ad. |
| 🇫🇮 Finland | EURES | Most ads come from Työmarkkinatori (the public Job Market Finland). |
| 🇩🇰 Denmark | EURES | Ads that Jobnet and Danish employers share with EURES (about 1,000-1,500 open ads). |
| 🇮🇸 Iceland | EURES | Small number of ads. |

### Keyword matching

The job sites' own search is loose. EURES, for example, returns any ad whose text contains "develop" when you search for "developer", and NAV and Platsbanken match words anywhere in the ad. With the default `keywordMatch: "title"`, every job is re-checked before it is stored, and jobs that do not match are dropped. **You are not charged for dropped jobs.**

- Case-insensitive, with plurals and Nordic endings: `nurse` matches "Nurses" and "Sjuksköterskor", but not "Nursery" or "Nursing home". `developer` matches "Developers" and "Backend-utvikler", but not "Business Development".
- Compounds: `sykepleier` matches "Spesialsykepleier" and "Sykepleiervikar".
- Common job words are matched across Nordic languages: developer = utvikler / utvecklare / kehittäjä / udvikler, nurse = sykepleier / sjuksköterska / sairaanhoitaja / sygeplejerske, and the same for engineer, teacher, electrician, driver, accountant, cook/chef, carpenter, cleaner, welder, mechanic, doctor and salesperson. Nynorsk forms are included.
- `developer` does not match "business developer" or "property developer" (forretningsutvikler, affärsutvecklare, eiendomsutvikler), unless your term includes that word.
- On EURES (Finland, Denmark, Iceland) the ESCO occupation is chosen by the employer. Some employers tag QA, ERP or project roles as "ICT application developer", and those ads match `developer`.
- Multi-word terms need every word: `java developer` matches "Senior Java Developer" and "Java-utvecklare".
- Terms with symbols (`C#`, `.NET`, `C++`) are matched as plain text.
- `fullText` also checks the description. Ads without a description (FINN.no imports, or `includeDescription` off) are kept as the source returned them.
- The job title check uses the ad title, `jobTitle`, `occupation` and the source's occupation classification, so "Vil du jobbe med verdensledende droner?" is still found for "developer" when NAV classifies it as Programvareutvikler.
- Each search is cut off after checking 10 × `maxItemsPerCountry` search results (at least 500), so a rare term cannot become a full crawl. `RUN_SUMMARY` shows `keywordMismatch` per country.
- The search itself still runs on each source. Platsbanken's search does not translate (`sykepleier` finds nothing in Sweden), so add the Swedish term (`sjuksköterska`). EURES does search across languages.

### Limits you should know

- **Norway, FINN.no ads:** many ads on NAV Arbeidsplassen are imported from FINN.no. NAV's open feed does not redistribute their full text, so for these ads you get title, company, location, dates, deadline, NAV's short summary, skill tags and a link to the original ad, but `description` is empty. Turn on `skipFinnImportedAds` if you only want full ads.
- **Denmark:** coverage is the part of the Danish market that is published on EURES, not all of Jobindex.
- **Deep paging:** a single search returns up to about 10,000 jobs in Norway, 2,100 in Sweden and 10,000 per country on EURES. For bigger exports, split the run by search term or location.
- **Location in Finland:** Finnish EURES ads often have no structured city. The location is then matched in the ad text, including inflected forms such as "Tampereella".

### Tips

- **Daily job alert:** turn on `onlyNewJobs`, set an `alertName` and a Slack, e-mail or webhook channel, and schedule the Actor. See [Job alerts](#job-alerts-new-jobs-only).
- **Search in local languages:** add the Norwegian, Swedish, Finnish and Danish job titles. Duplicates are removed automatically.
- **Faster runs:** turn off `includeDescription` when you only need titles, companies and links.
- **Use it from AI agents:** this Actor works with the Apify MCP server, so Claude, ChatGPT or Cursor agents can call it directly.

### FAQ

#### Is there a NAV job API or an Arbetsförmedlingen API?

Yes, both are public, and this Actor uses them: NAV's job search and public job feed (pam-stilling-feed) for Norway, and the JobTech JobSearch API behind Platsbanken for Sweden. The Actor saves you from handling three different formats, paging limits and rate limits, and it adds keyword matching, de-duplication and job alerts.

#### Does it cover FINN.no, LinkedIn or Jobindex?

Not directly. Norwegian ads that FINN.no shares with NAV are included, without the full description. LinkedIn, Jobindex and other private job boards are not covered.

#### Does the output contain personal data?

The Actor never extracts contact persons into separate fields: there is no contact name, e-mail or phone field. The `description` field, however, is the full ad text written by the employer, and in a test sample of 415 ads about half of the ad texts contained a contact person's e-mail address or phone number.

By default (`redactContacts: true`) the Actor masks these before saving:

- **Masked:** e-mail addresses (also written as `name (at) firm.no`) and phone numbers in Nordic and international formats (`+47 412 34 567`, `0708-123456`, `040 123 4567`, `22 33 44 55`, `00 45 …`) in `description`, `descriptionHtml`, `summary`, `title`, `salary`, `startDate`, `workplace` and `keywords`. They become `[email removed]` and `[phone removed]`. In `descriptionHtml`, `mailto:` and `tel:` links are emptied.
- **Kept:** `applyUrl`, `url`, `originalAdUrl`, `companyWebsite` and every web address in the text, plus dates, times, salaries, years, postal codes, reference numbers and organisation numbers (`companyId`, CVR, org.nr., Y-tunnus).
- **Not masked:** names. The ad text may still say who the contact person is, so treat the data as possibly containing personal data under the GDPR.

`RUN_SUMMARY.contactsRedacted` shows how many e-mails and phone numbers were masked. Masking is pattern-based. In our tests on 415 real ads, no e-mail address was left and no date, salary or ID number was masked, but an unusual format can slip through either way. Turn `redactContacts` off only if you need the contact details and have a lawful basis to process them. You can also drop the description: for Norway turn off `includeDescription`. Sweden, Finland, Denmark and Iceland return the description in the search results, so those ads always include it.

#### Is it legal to scrape job listings?

This Actor only collects public job ads through official, openly available APIs (NAV, JobTech/Arbetsförmedlingen and EURES), without logging in. Make sure your use complies with the GDPR, the sources' terms of use and other applicable laws. If you are unsure, consult a lawyer.

### Support

Found a bug or need another country, source or field? Open an issue on the Issues tab. We monitor this Actor and fix breakages quickly.

### More scrapers from Tidy Data

- [Software Advice Reviews Scraper (Capterra Network)](https://apify.com/tidy-data/software-advice-capterra-reviews-scraper): B2B software reviews with pros, cons, company size and vendor replies.
- [App Store & Google Play Reviews Scraper](https://apify.com/tidy-data/app-store-google-play-reviews-scraper): iOS and Android app reviews, multi-country, with developer replies.

# Actor input Schema

## `countries` (type: `array`):

Norway uses NAV Arbeidsplassen, Sweden uses Arbetsförmedlingen's Platsbanken (JobTech open API), Finland, Denmark and Iceland use the EU's EURES job portal.

## `searchTerms` (type: `array`):

Keywords such as a job title, skill or company, e.g. "developer", "sykepleier", "sjuksköterska", "sairaanhoitaja". Each term is searched in every selected country and results are de-duplicated. Leave empty to get the newest jobs of any kind.

## `keywordMatch` (type: `string`):

How strictly each search term must match. The job sites' own search is loose: EURES, for example, returns any ad that contains "develop" somewhere in the text for "developer". "Job title or occupation" keeps only jobs whose title, standardized job title or occupation contains the term (case-insensitive, plurals and Nordic endings allowed, so "nurse" matches "Nurses" but not "Nursery"; common job words are also matched in Norwegian, Swedish, Finnish and Danish, e.g. developer = utvikler / utvecklare / kehittäjä / udvikler). "Anywhere in the ad" re-checks the term in the description too. Jobs that do not match are dropped before they are stored, so you are not charged for them.

## `location` (type: `string`):

Optional city, municipality or region, e.g. Oslo, Bergen, Stockholm, Göteborg, Helsinki, Aarhus. Matched against the job's workplace. Leave empty for the whole country.

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

Only jobs published in the last N days. Use 1 for a daily monitoring schedule. Leave empty for all open jobs.

## `postedAfter` (type: `string`):

Optional YYYY-MM-DD. Only jobs published on or after this date. If both date filters are set, the later date wins.

## `maxItemsPerCountry` (type: `integer`):

Stop after this many jobs in each country (across all search terms). 0 = all available.

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

Optional cap for the whole run, across all countries. 0 = no total cap.

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

Fetch the full job ad (description, apply URL, deadline, employer details). Turn off for a faster, lighter run with search data only (Sweden, Finland, Denmark and Iceland still include the description, because their search results contain it).

## `includeDescriptionHtml` (type: `boolean`):

Adds a descriptionHtml field with the original formatting next to the plain-text description.

## `dedupeByTitleAndCompany` (type: `boolean`):

Skip ads with the same title, company and city in the same country (common for reposted ads). Exact duplicates by job ID are always removed.

## `skipFinnImportedAds` (type: `boolean`):

Many Norwegian ads on NAV Arbeidsplassen are imported from FINN.no. NAV's open feed does not share their full text, so for these ads you get title, company, location, dates, a short summary and a link to the original ad, but no full description. Turn this on to get only ads with a full description.

## `redactContacts` (type: `boolean`):

On by default, for GDPR-friendlier datasets. Ad texts often name a contact person with a personal e-mail address or phone number. With this on, e-mail addresses and phone numbers (Nordic and international formats) in the free-text fields (description, summary, title, salary, start date, workplace, keywords and the HTML description) are replaced with \[email removed] / \[phone removed]. Apply links, ad URLs and company websites are kept, as are dates, salaries, postal codes and organisation numbers. Names of contact persons may still appear in the text. Turn off only if you have a lawful basis to process the contact details.

## `onlyNewJobs` (type: `boolean`):

Turn this on for scheduled runs. The Actor remembers every job it has delivered for this alert (in a named key-value store in your own Apify account) and returns, and charges for, only jobs it has not delivered before. The first run builds the baseline: it returns the current matching jobs. Every later run returns only new ones. Jobs already delivered are never charged again.

## `alertName` (type: `string`):

A short name for this alert, e.g. "oslo-developers". Each alert remembers its own delivered jobs, so use a different name for every search you schedule. Letters, numbers and dashes. If empty, a name is derived from your countries, search terms and location.

## `resetAlert` (type: `boolean`):

Forget the jobs this alert has delivered and start a new baseline. Turn it off again after one run.

## `webhookUrl` (type: `string`):

Optional. When a run finds jobs, the Actor POSTs a JSON summary with the job list (up to 500 jobs, without descriptions) to this URL. Works with Zapier, Make, n8n, Pipedream or your own server.

## `slackWebhookUrl` (type: `string`):

Optional. A Slack incoming webhook (https://hooks.slack.com/services/...). Posts a message with the newest jobs, each linked to its apply page.

## `emailTo` (type: `string`):

Optional. Your e-mail address. Apify's free apify/send-mail Actor only delivers to the e-mail address of your own Apify account (use that address; other addresses are rejected). It runs in your account and adds a tiny platform usage cost (usually well under $0.01 per e-mail).

## `notifyTopN` (type: `integer`):

How many of the newest jobs to show in the Slack message and the e-mail. The webhook always gets the full list (up to 500).

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

Used only for Norway's NAV search, which rate-limits shared cloud IP addresses. Keep the default Apify Proxy (datacenter) for reliable Norwegian results. All other sources connect directly.

## Actor input object example

```json
{
  "countries": [
    "NO",
    "SE",
    "FI",
    "DK"
  ],
  "searchTerms": [
    "developer"
  ],
  "keywordMatch": "title",
  "maxItemsPerCountry": 50,
  "maxItems": 0,
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "dedupeByTitleAndCompany": true,
  "skipFinnImportedAds": false,
  "redactContacts": true,
  "onlyNewJobs": false,
  "alertName": "oslo-developers",
  "resetAlert": false,
  "notifyTopN": 10,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All scraped jobs: title, company, location, country, posted date, deadline, employment type, description, apply URL, source.

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

Per-country counts, duplicates skipped, errors and timing, plus job alert state (new vs. already-seen jobs) and notification results (JSON).

# 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 = {
    "countries": [
        "NO",
        "SE",
        "FI",
        "DK"
    ],
    "searchTerms": [
        "developer"
    ],
    "maxItemsPerCountry": 50,
    "alertName": "oslo-developers",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidy-data/nordic-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 = {
    "countries": [
        "NO",
        "SE",
        "FI",
        "DK",
    ],
    "searchTerms": ["developer"],
    "maxItemsPerCountry": 50,
    "alertName": "oslo-developers",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("tidy-data/nordic-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 '{
  "countries": [
    "NO",
    "SE",
    "FI",
    "DK"
  ],
  "searchTerms": [
    "developer"
  ],
  "maxItemsPerCountry": 50,
  "alertName": "oslo-developers",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call tidy-data/nordic-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tidy-data/nordic-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/Rq7uOBeeB9g15yVJA/builds/yCTtkxjAOcTsQG6MF/openapi.json
