# Platsbanken Scraper — Sweden Job Ads & Employer Leads (`scrapersdelight/platsbanken-jobs-scraper`) Actor

Scrape Sweden's national job board Platsbanken (Arbetsformedlingen open data): 40,000+ live ads with employer, organisation number, municipality, occupation, deadline, the named contact person's e-mail and phone, and the ATS vendor behind the apply link. No login, no API key.

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

## Pricing

$0.90 / 1,000 per job ad returneds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 🇸🇪 Platsbanken Scraper — Sweden Job Ads & Employer Leads

Scrape **Platsbanken**, Sweden's national vacancy board run by **Arbetsförmedlingen** (the Swedish
Public Employment Service), through its official **JobTech Dev open API**. Every ad on the board is
published as open data for reuse: **no login, no API key, no cookie, no CAPTCHA.**

**40,000+ live ads**, running from **2025-07-09** to today and growing by roughly **2,100–2,500 new
ads every day.** The exact size decays daily as ads expire, so no fixed figure is pinned here: the
board measured **41,246 ads on 2026-09-05**, and the actor reads the true total from the API at the
start of every run and prints it in the log.

This is not a job-board dump. It is a **Swedish employer lead list with a hiring trigger attached**:
every row carries the employer's **organisation number** (the Bolagsverket / SCB registry key), the
**named contact person with a direct e-mail and phone number** where the ad has one, and the **ATS
vendor** resolved from the apply link's host.

***

### 🎯 What does Platsbanken Scraper do?

| | |
|---|---|
| 🏢 **Company identity** | `employer_name` and `employer_workplace` (**100%**), `employer_url` (**63.5–63.7%**) and the Swedish **`org_number`** (**99.2–99.4%**) |
| 📞 **A human to call** | `contact_name`, `contact_email`, `contact_phone` flattened out of `application_contacts[]` — **45.1–47.5%** of ads name a human, **39.8–41.1%** give a direct e-mail, **32.0–35.6%** a phone; **57.8–59.6%** are reachable |
| 🛠 **The incumbent ATS** | `ats_vendor` + `apply_host` resolved from the apply URL. **40.5–50.2%** resolve to one of 49 named vendors; **11.9–17.0%** have **no apply link at all** — the displacement list |
| 📍 **Precise location** | Municipality + 4-digit code, region + code, city, postcode, street address, latitude/longitude |
| 💼 **The role** | Occupation, occupation group, occupation field, employment type, full/part-time, duration, scope-of-work band, experience and driving-licence requirements |
| 🗓 **Timing** | `publication_date`, `application_deadline`, plus derived `days_live` and `days_to_deadline` |
| 📝 **Full ad text** | `description_text` — **100% filled**. Where the technographic signals live |
| 🔁 **Incremental monitoring** | A named state key so a daily run only ever delivers — and only ever **bills for** — genuinely new vacancies |

*(Every percentage on this page was measured from real runs of this actor, re-measured
**2026-09-07**. Each is a RANGE across two differently-shaped runs made the same day — a full
calendar-day sweep (run `iDb78sXSeDHqPH5gN`, 2,268 ads) and the 2,000 most recently published ads
(run `jP1jGT0bR51D6UdWf`). Fill moves several points between a weekday's ads and a slice that
reaches back over a weekend, so a single number would be a promise this board does not make. Run
ids and the full tables are in **Measured output fill** below, and the same two runs are quoted on
the Input tab.)*

***

### 🧭 Who buys this data

- **Recruitment and staffing agencies** — "every Swedish company that posted a vacancy yesterday,
  with a named contact and a phone number, minus the other agencies." Turn on
  **Exclude staffing and recruitment agencies** and you get end clients, not competitors.
- **HR-tech and ATS vendors** — **Only employers with NO apply link** is a live list of companies
  hiring with no applicant-tracking system in the loop. **Only employers on these ATS vendors** is
  the competitive-displacement list: Varbi, Visma Recruit, Recman, ReachMee, Teamtailor, Bullhorn,
  Workday and 42 more.
- **B2B sales teams selling to growing companies** — a vacancy is a growth signal, and
  `org_number` joins it straight to a Swedish company record for revenue and headcount enrichment.
- **Labour-market analysts and job aggregators** — the whole 40,000+ ad corpus, complete, with the
  taxonomy concept ids for joining to taxonomy.jobtechdev.se.

***

### ⚠️ The trap this actor exists to survive

**A single query against this API can return at most 2,100 rows** — `limit` caps at 100
(101 → HTTP 400) and `offset` caps at 2000 (2001 → HTTP 400). And **a one-day window is already too
big**: re-measured 2026-09-05, ads published on 09-01 numbered **2,478**, on 09-02 **2,131**, on
09-03 **2,115** and on 09-04 **2,299**. (A given day's count drifts by a few tens as late ads
register and expired ones leave the index, which is why every figure on this page carries its date.)

A crawler that partitions by day therefore drops ads on a run that still reports SUCCEEDED. We
measured exactly that, twice on the same window, minutes apart:

| Same one-day window, 2026-09-04, 2,297 ads in scope | Ads delivered | Run |
|---|---|---|
| **Sweep the whole corpus: ON** (adaptive bisection) | **2,297 of 2,297 — 100%** | `forqADgU3QsybN16A` |
| Sweep the whole corpus: OFF | 2,100 — **197 ads lost** | `90JMcjeegPEppVjdy` |

The ON run took **92 seconds**, split the day into **2 date windows**, delivered **2,297 rows with 0
duplicates and 0 dropped by filters**, and `chargedEventCounts` came back at exactly **2,297** — so
delivered == billed. Peak memory **97 MB** of the 512 MB it declares.

The OFF run does not lose the ads *silently*: it logs
`2,297 ads match but a single query can only reach 2100` before it starts, and
`197 more ad(s) match this filter but cannot be paged to` when it stops. But it still returns 2,100
rows and a SUCCEEDED status.

**Turn on "Sweep the whole corpus (auto-partition)" for any run that should return more than
2,100 ads.** It probes each publication-date window with a free `limit=0` call, reads the true
total, and bisects until every slice fits under the ceiling.

***

### 🚀 Quick start

**Every Swedish job ad published in the last 24 hours, complete:**

```json
{
  "publishedWithinDays": 1,
  "fullCorpusMode": true,
  "maxItems": 0
}
```

**Reachable nurse vacancies in Stockholm, agencies excluded:**

```json
{
  "searchQueries": ["sjuksköterska"],
  "region": ["01"],
  "onlyWithContact": true,
  "excludeStaffingAgencies": true,
  "maxItems": 500
}
```

**ATS displacement list — companies hiring with no applicant-tracking system:**

```json
{
  "onlyWithoutAts": true,
  "onlyWithOrgNumber": true,
  "dedupeByEmployer": true,
  "publishedWithinDays": 7,
  "fullCorpusMode": true,
  "maxItems": 0
}
```

**A daily monitor that never re-bills you for an ad you already have:**

```json
{
  "stateKey": "sweden-daily",
  "publishedWithinDays": 1,
  "fullCorpusMode": true,
  "occupationField": ["apaJ_2ja_LuF"],
  "maxItems": 0
}
```

***

### 🇸🇪 Three Swedish gotchas this actor handles for you

**1. Freetext is diacritic-sensitive.** Measured live on 2026-09-05: `q=sjukskoterska` returns **0**
ads; `q=sjuksköterska` returns **1,915**. Never transliterate å ä ö.

**2. Region and municipality codes are ZERO-PADDED STRINGS, and a bad one is a silent success.**
Measured 2026-09-05: `region=01` returns 10,681 ads. `region=1` returns **HTTP 200 with total 0** —
not an error, just an empty run that looks fine. This actor validates and pads every code, accepts
the plain Swedish place name (`Stockholm`, `Göteborg`, `Västra Götaland`) and **refuses to run** on a
code it cannot resolve rather than handing you a clean, empty, billed result.

**3. AND/OR only bites with smart matching off.** Arbetsförmedlingen enriches freetext with
machine-learned synonyms. Measured 2026-09-05 on `sjuksköterska stockholm`: with enrichment on, AND
and OR both return 236; with **Turn off smart freetext matching** on, OR returns 12,106 and AND
returns 313.

***

### 📊 Measured output fill

Every number below comes from **one real platform run I made**, not an estimate: a complete day sweep
of **2026-09-04**, re-run on **2026-09-07** as run **`iDb78sXSeDHqPH5gN`** at the actor's declared
512 MB, `outputShape: full` — **2,268 read, 2,268 delivered, 2,268 unique ids, zero duplicates,
172 seconds**, delivered == billed. **70 columns per row.**

The same window was swept on 2026-09-05 as run `forqADgU3QsybN16A` and returned **2,297** ads: ads
are removed from the index as their deadlines pass, so the same calendar day shrinks slightly every
time you re-measure it. **Every fill percentage in the table below reproduced within 0.3 points
across those two sweeps** (`ats_vendor` 50.0% → 50.2%, `apply_url` 88.1% → 88.1%, `has_ats:false`
11.9% → 11.9%, `contact_name` 47.5% → 47.5%, `is_reachable` 57.8% → 57.8%). What does move is the
SLICE — see the 2,000-newest run below the table.

| Field | Fill over the 2,268 delivered rows of `iDb78sXSeDHqPH5gN` |
|---|---|
| `id`, `platsbanken_ad_url`, `webpage_url`, `headline`, `employer_name`, `employer_workplace`, `occupation`, `occupation_group`, `occupation_field`, `employment_type`, `salary_type`, `publication_date`, `application_deadline`, `last_publication_date`, `description_text`, `source_type`, `source_type_label`, `number_of_vacancies`, `days_live`, `days_to_deadline`, `timestamp`, `scraped_at` | **100%** |
| **`org_number`** | **99.4%** |
| `municipality`, `municipality_code`, `region`, `region_code` | 98.7% |
| `experience_required` (**true** on) | 93.3% |
| `city`, `postcode`, `latitude`, `longitude` | 92.6% |
| `working_hours_type`, `scope_of_work_min/max` | 90.1% |
| `duration` | 89.9% |
| `apply_url` | 88.1% |
| `apply_host` | 88.0% |
| `logo_url` | 81.3% |
| `street_address` | 67.3% |
| `employer_url` | 63.7% |
| **`is_reachable`** (a direct e-mail or phone on the ad) | **57.8%** |
| **`ats_vendor`** (resolves to a named vendor) | **50.2%** |
| `contact_name`, `contacts`, `contacts_count` | 47.5% |
| **`contact_email`** | **41.1%** |
| `apply_reference` | 37.4% |
| **`contact_phone`** | **35.6%** |
| `driving_license_required` (**true** on) | 33.6% |
| `contact_title` | 28.3% |
| `nice_to_have_work_experiences` | 21.8% |
| `driving_license` (specific class) | 16.2% |
| `apply_email` | 13.8% |
| `is_staffing_agency` (our own flag, **true** on) | 13.4% |
| **`has_ats: false`** — no apply link at all | **11.9%** |
| `must_have_languages` | 11.3% |
| `must_have_work_experiences` | 7.5% |
| `nice_to_have_languages` / `nice_to_have_skills` | 5.1% / 4.3% |
| `must_have_education`, `apply_other`, `must_have_skills`, `labels` | 1.4%–2.3% |

Those 2,268 ads came from **951 distinct organisation numbers** (964 distinct employer names), so
**One row per employer** turns a vacancy list into a company list at roughly 42% of the rows — and
you are only billed for the rows you keep.

#### The other slice — and why the two disagree

A second, independent run over the **2,000 most recently published ads across all of Sweden**
(run **`jP1jGT0bR51D6UdWf`**, 2026-09-07, 2,000 delivered / 2,000 unique ids / 140 seconds):
`org_number` **99.2%**, a named contact on **45.1%**, reachable **59.6%**, `contact_email` **39.8%**,
`contact_phone` **32.0%**, a named ATS vendor on **40.5%**, no apply link on **17.0%**, and **918
distinct organisation numbers**.

**Read that ATS number carefully if the displacement list is why you are here.** A weekday's ads
resolve to a named vendor **50.2%** of the time; a rolling 2,000 that reaches back across a weekend
resolves **40.5%**. Same actor, same parser, same day — the ads are different. The honest range is
**40–50%**, and the corresponding no-apply-link bucket is **12–17%**. A single day and a rolling
2,000 are different slices of the board; neither is rounded to flatter the other, and this page
quotes both rather than the better one.

#### ⚠️ Five of those 70 columns are near-constant — read this before you count fields

Measured across the same 2,268 rows. These columns are shipped because they are what the API
returns, but on a Swedish-market run they carry little or no row-level signal:

| Column | What it actually did over 2,268 rows |
|---|---|
| `workplace_model` | **One value on every row** — "Arbete på plats". A dead API field, not a dead parser: see the no-op note below. **Zero signal.** |
| `removed` | **`false` on every row.** The search index only carries live ads, so this can only ever be false here. It exists so a row's provenance is explicit. **Zero signal.** |
| `access_to_own_car` | **`false` on every row.** Arbetsförmedlingen barely populates it. **Zero signal today.** |
| `country` | 99.6% "Sverige" — but **not** a constant: 9 of 2,268 rows were abroad (Spanien 3, Schweiz 2, Norge, Cypern, Finland, Polen). Low signal, occasionally the point. |
| `country_code` | The same 7 values as `country` (199 = Sweden on 99.6%). Low signal. |

All three constants held on the 2026-09-07 2,000-newest run (`jP1jGT0bR51D6UdWf`) too:
`workplace_model` one distinct value ("Arbete på plats" on 2,268 of 2,268 in the day sweep),
`removed` true on 0 rows, `access_to_own_car` true on 0 rows.

So of the 70 columns, **65 carry genuine row-level variation** and three (`workplace_model`,
`removed`, `access_to_own_car`) are constants on this market. Nothing is hidden and nothing is
dropped — you just should not budget them as fields.

### 🔎 The ATS wedge, measured

Vendors resolved from the apply-URL host across the 2,268-ad day sweep (`iDb78sXSeDHqPH5gN`,
2026-09-07):

| Vendor | Ads | Vendor | Ads |
|---|---|---|---|
| Varbi | 317 | Recruto | 19 |
| Visma Recruit | 175 | Ena Portal | 11 |
| ReachMee | 113 | CareerHub | 10 |
| Recman | 112 | Ledigajobb.se | 8 |
| Teamtailor | 74 | Intelliplan | 8 |
| Bullhorn (Aplitrak) | 65 | Wopify | 8 |
| EasyApply | 46 | SmartRecruiters | 7 |
| Ponty | 40 | Webcruiter | 5 |
| Avaron | 29 | SAP SuccessFactors | 4 |
| Talentech | 29 | Jobbet.se | 4 |
| PowerInit | 27 | Jobtip / TalentAdore | 2 each |
| Workday | 23 | Greenhouse | 1 |

**49 vendor host patterns are recognised in total** (counted in `src/taxonomy.js`); **25 of them
appeared** in this one day's ads, resolving **1,139 of 2,268 rows = 50.2%**.

**37.8% of ads point at the employer's own careers page or an unrecognised host** — those come back
with `apply_host` filled and `ats_vendor: null`, which is a real answer, not a miss. There were
**538 distinct apply hosts** in the day, so the long tail is genuine and a host is never guessed at.
**11.9% have no apply link at all** (`has_ats: false`) — the displacement target.

**Do not budget 50.2% as a constant.** The same parser over the 2,000 most recently published ads
the same day (`jP1jGT0bR51D6UdWf`) resolved **40.5%**, because a rolling 2,000 reaches back across
a weekend and weekend ads carry apply links less often. Across the two runs the honest span is
**40–50% named vendor** and **12–17% no apply link at all**. Both runs are quoted, in full, above.

***

### 🚫 What this actor does NOT give you, stated plainly

- **No salary figures.** Re-measured 2026-09-05 over the 2,000 most recently published ads
  (run `BEjkBLIO8FBrVejcC`): the API's own `salary_description` is filled on **3 of 2,000** — so thin that it is not emitted as a
  column at all. The `salary_type` column is a pay *structure*
  label ("Fast månads- vecko- eller timlön"), not an amount, and it is named accordingly. Two rivals
  in this lane advertise salary data on their store page and cannot deliver it; we would rather say
  so than repeat it.
- **`workplace_model` does not filter, and does not vary.** Re-measured 2026-09-05: the on-site
  concept returns the **entire corpus unfiltered** — the on-site total equalled the unfiltered total
  exactly, 41,246 = 41,246 — and remote and hybrid each return **0**, because every live ad carries
  the on-site concept. The input is exposed and passed through honestly, with a runtime warning, and
  the output column is therefore a constant (see the near-constant table above). For remote work use
  **Remote-friendly ads** (phrase-matched, 675 live ads on 2026-09-05) instead.
- **`removed` and `access_to_own_car` are constants on this market** — `false` on every row measured.
  They are shipped as-is rather than quietly dropped; see the near-constant table above.
- **Almost no employer e-mail or phone from the employer record.** Re-measured over 2,000 ads on
  2026-09-05 (run `BEjkBLIO8FBrVejcC`): `employer.email` is filled on **0 of 2,000** and
  `employer.phone_number` on **2 of 2,000**. The contact details this actor returns come from `application_contacts[]` and the
  application e-mail — real fields, honestly rated above.
- **Taxonomy skill and education tags are sparse** — `must_have_skills` 1.5% and
  `must_have_education` 2.3% over the 2,268-row day (`iDb78sXSeDHqPH5gN`, 2026-09-07). They are returned, but they are not the product.
- **`qfields: "trait"` is not offered.** The API's own spec advertises it; re-checked 2026-09-05, the
  live API rejects it with **HTTP 400** (`trait is not a valid enum value`). Shipping it would hand
  you a run that dies on the first call.
- **No paid "detail" step.** Re-verified 2026-09-05 on 5 ads: the API's `/ad/{id}` endpoint returns
  **the same 38 keys with byte-identical values** as the search hit; the search hit carries exactly
  one extra key, the internal `relevance` score. A second "detail" billing event would charge you for
  nothing, so there is one event and one price.

***

### 💰 Pricing

**$0.0009 per job ad returned** — one event, no start fee, no per-result auto-event.

- Ads dropped by your filters are **never billed**.
- Duplicates are collapsed **before** billing, so a partitioned sweep or several overlapping search
  keywords never charge you twice for one ad.
- With a **state key**, a daily monitor only bills for genuinely new vacancies.
- The whole live corpus: about **40,000 ads × $0.0009 ≈ $36–38** (41,246 ads measured 2026-09-05 =
  $37.12; the board hovers just above 40,000, so budget in that band rather than on a fixed figure).
- A daily monitor over every new Swedish ad: ~2,100–2,500/day ≈ **$57–67 per month**. The 2,268-ad
  day sweep above billed exactly 2,268 events = **$2.04**.

***

### ❓ FAQ

**Is this legal?** Platsbanken's ad data is published by Arbetsförmedlingen, a Swedish government
agency, through JobTech Dev **explicitly for reuse as open data**. There is no authentication, no API
key, no rate limit encountered across ~200 probe calls, and no anti-abuse control anywhere on the
path. This actor reads the documented public endpoint and nothing else.

**Does it need residential proxies?** No — datacenter is the default and it is what every run on this
page used. The full escalation ladder was measured at build time (2026-09-04) and all four tiers
returned identical bytes: direct 1,449 ms, Apify datacenter 1,644 ms, residential SE 1,666 ms,
residential US 2,849 ms. Residential is slower for no benefit. On the datacenter default, the
2,297-row sweep above completed in 92 seconds with no retries and no blocked requests.

**How many requests does a full sweep take?** Roughly 40–80 for a wide sweep. The one-day complete
sweep of 2,297 ads took **2 date windows** and about **25 API calls** (a free `limit=0` probe per
window plus 100 rows per page) in **92 seconds**.

**Can I get every ad ever posted?** The live corpus goes back to **2025-07-09** (confirmed
2026-09-05: the oldest live ad is dated 2025-07-09T15:34:40, and everything published before
2026-01-01 is down to 7 ads). Set `fullCorpusMode: true` with no date filter and `maxItems: 0`; the
sweeper finds the oldest ad in scope and works forward.

**How do I watch specific companies?** Put company names or Swedish organisation numbers into
**Employers**. Dashes and spaces in an org number are stripped for you.

Read this before you budget the run. The API's own `employer=` parameter is a **full-text match over
the employer name, not an exact one**, and on its own it is useless for account-based prospecting:
measured 2026-09-06, `employer="Norrköpings kommun"` matched **4,061 ads** — every municipality in
Sweden, because *kommun* alone matches 4,234 — while **Norrköpings kommun itself had 43**. A company
that does not exist, `"Kommun som inte finns AB"`, matched **33,533** (81% of the board), and
`employer="Spotify AB"` matched **29,291** where `employer="Spotify"` matched 0.

**This actor sends that parameter to narrow the crawl and then re-checks every row against the name
or organisation number you actually typed, dropping the rest before billing.** Proven 2026-09-07:
`employer=["Norrköpings kommun"]` delivered and billed **22 rows, every one of them that employer**
(`tnSYbgB5S1WhKyEaA`) — 22 and not 43, because 2,100 of the 4,061 full-text matches is as far as one
query reaches, which the run says out loud; the non-existent company delivered and billed **0**
(`qIiduKkmWRb4D8RcT`).
Two consequences to expect in the log: **"dropped by filters" will be large**, and because a single
query reaches at most 2,100 rows, a common word in the company name can hit that ceiling before the
scan is finished — the run says so explicitly and tells you to turn on **Sweep the whole corpus**.
An organisation number is matched exactly and has none of that problem, so prefer it where you have
it.

**How do I search by geography instead of by municipality?** Set **Centre point** to
`latitude,longitude` and a **Radius**. Measured 2026-09-05 from central Stockholm
(`59.3293,18.0686`): 10 km → 7,016 ads, 100 km → 12,652.

**Are the labels in Swedish?** By default yes — that is what Arbetsförmedlingen publishes. Set
**Label language** to English to swap the closed vocabularies (occupation field, employment type,
working hours, duration, workplace model, pay structure) for an English gloss, or to Both to get
extra `_en` columns. Place names, specific occupation names and the ad text stay Swedish; machine-
translating 290 municipality names would be a fabrication, not data.

**What is the stable key for deduplication?** `id` — 100% present, unique per ad (2,268 distinct ids
across 2,268 rows in `iDb78sXSeDHqPH5gN`, and 2,000 across 2,000 in `jP1jGT0bR51D6UdWf`), and equal
to the trailing segment of `webpage_url`. Measured 2026-09-05 over 2,000 ads, the API's
`external_id` is filled on 4 of 2,000 (0.2%) and `original_id` on 0 of 2,000, so neither can be used
as a key.

**Can it return removed or expired ads?** No. The search index only carries live ads; every row this
actor returned measured `removed: false` (2,268 of 2,268 in `iDb78sXSeDHqPH5gN`, 2,000 of 2,000 in
`jP1jGT0bR51D6UdWf`). Fetching a specific expired ad id returns nothing, and that is **named in the
log** — "Not delivered and NOT billed: <id>" — and not billed.

**Does it work as a scheduled monitor?** Yes. Give it a **State key**, schedule it daily with
`publishedWithinDays: 1` and `fullCorpusMode: true`, and it delivers only ads it has never delivered
under that key before. State is kept in a **named** key-value store, so it survives across runs.

**What happens if my filter matches nothing?** The run exits cleanly with zero rows — a FAILED run
would be permanent on this actor's scorecard — and the log line names **which** of the possible
causes actually applied on that run, from the state the actor is holding at the time: the API itself
returned nothing; or your **Skip first N results** started past the end of the result set; or the
client-side filters dropped every row that was read (with the filters named, and a warning if the
2,100-row per-query ceiling meant only part of the match set was ever examined); or everything read
had already been delivered under your state key.

**Which filter values are checked before the run starts?** Region, municipality, country, occupation
field, employment type, working hours, duration, driving licence, workplace model, occupation
collection, freetext fields, sort and ATS vendor are resolved against a table, and an unresolvable
value **fails the run up front** naming the bad value — because this API answers a bad code with
HTTP 200 and zero rows rather than an error. **Six inputs have no table to resolve against and are
sent verbatim: Occupation group, Occupation name, Skill, Language, Label and Employers.** For those
the actor logs a notice at the start of the run saying so, and repeats it if the run comes back
empty. Nothing is billed either way.

**Which timezone are the date filters in?** Date filters are evaluated in **UTC**, while
`publication_date`, `application_deadline` and `last_publication_date` come back from the API in
**Stockholm local time** (+01:00 winter, +02:00 summer). A one-day UTC window therefore puts about
1% of its rows in the neighbouring local calendar day — measured 2026-09-06, a
`2026-09-03T00:00:00 .. 2026-09-04T00:00:00` request delivered 2,095 rows dated 09-03 and 20 dated
09-04 (run `Rcl2sclubWqOAgXcL`). The run logs this whenever a date filter is set.

**Do the two "max" caps count rows read or rows delivered?** **Delivered** — both of them, which is
also what you are billed for. **Max results** and **Max ads per search keyword** keep paging past
rows your client-side filters drop until the cap is filled or the source runs out, and if a run ends
short of the cap the log says which of the two it was: the source was exhausted (a complete answer)
or the 2,100-row per-query ceiling stopped it (a truncated one, fixed by turning on **Sweep the
whole corpus**).

***

### ⚖️ Legal and fair use

Platsbanken ad data is open data published by Arbetsförmedlingen through
[JobTech Dev](https://jobtechdev.se/) for reuse, under Sweden's open-data framework. This actor reads
only the documented public `jobsearch.api.jobtechdev.se` endpoints, sends no authentication, and
bypasses no access control of any kind.

**Personal data is your responsibility.** `application_contacts[]` carries the names, direct e-mail
addresses and telephone numbers of identifiable individuals at the hiring employer. Under the GDPR
you are the controller for whatever you do with those rows: have a lawful basis, honour objection and
erasure requests, and observe Swedish and EU rules on unsolicited commercial contact. Arbetsförmedlingen
publishes these details so candidates can apply — that is not the same as consent to be added to a
marketing list, and this actor makes no representation that it is.

# Actor input Schema

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

Free-text searches, one per line. Each is run as its own query and the results are merged and de-duplicated on the ad id. **Swedish freetext is DIACRITIC-SENSITIVE** — measured live 2026-09-05: `sjukskoterska` returns **0** ads and `sjuksköterska` returns **1,915**. Never transliterate å ä ö. Leave empty to sweep every ad matching your other filters.

## `qfields` (type: `array`):

Search these structured fields IN ADDITION to the default headline / description / employer match. **Honest note, measured 2026-09-05:** on the live corpus this made no difference — `volvo` returned 154 ads both with and without `qfields=employer`, because the default freetext already matches the employer name. It is passed straight through in case Arbetsförmedlingen starts honouring it. The API spec also advertises `trait`, but the live API rejects it with HTTP 400, so it is deliberately not offered here.

## `freetextBoolMethod` (type: `string`):

How to combine the words inside one search keyword. This ONLY changes the result set when "Turn off smart freetext matching" is also on — measured 2026-09-05 on `sjuksköterska stockholm`: smart matching ON gives 236 either way; smart matching OFF gives OR 12,106 and AND 313.

## `disableSmartFreetext` (type: `boolean`):

Arbetsförmedlingen enriches a freetext query with machine-learned synonyms and related occupations. Turn this OFF for literal, exact-word matching — measured 2026-09-05: `sjuksköterska` returns 1,915 with enrichment on and 2,447 with it off, because the two searches match different things, not more or fewer of the same thing.

## `enableFalseNegativeSearch` (type: `boolean`):

Runs an additional search for the literal term alongside the enriched one. Widens recall on rare or newly coined job titles, at some cost in precision.

## `relevanceThreshold` (type: `integer`):

Drop weakly matching ads. Only meaningful with a search keyword and the `Relevance` sort. Measured 2026-09-05 on `sjuksköterska`: no threshold 1,915 ads, threshold 0.9 → 1,038.

## `region` (type: `array`):

Swedish counties, OR-ed server-side (measured 2026-09-05: Stockholm 10,681 + Västra Götaland 6,523 → 17,204 combined). **The code is a ZERO-PADDED string** — the API answers `region=1` with HTTP 200 and ZERO ads rather than an error, so an unpadded code reads as "no jobs here". This actor validates and pads for you, and also accepts the plain Swedish name.

## `municipality` (type: `array`):

All 290 Swedish municipalities, OR-ed server-side. Live sizes measured 2026-09-05: Stockholm (0180) 6,531 · Göteborg (1480) 3,242. Codes are 4 characters and carry the same zero-pad trap as regions; this actor pads them and also accepts the Swedish name.

## `country` (type: `array`):

Numeric JobTech country codes or Swedish country names, for ads placed outside Sweden. Live sizes measured 2026-09-05: Sverige (199) 41,003 · Norge (155) 96 · Spanien (193) 42 · Danmark (44) 13.

## `unspecifiedSwedenWorkplace` (type: `boolean`):

Ads that say "Sweden" but name no municipality — 610 live, measured 2026-09-05. Remote-first and travelling roles cluster here. Setting this to false does nothing (the API ignores a false), so it is only ever sent when true.

## `abroad` (type: `boolean`):

Also return the ads for work abroad — 243 live, measured 2026-09-05 — even while a Swedish municipality or region filter is set. Setting it to false does nothing (the API ignores a false).

## `position` (type: `string`):

Latitude,longitude — e.g. `59.3293,18.0686` for central Stockholm. Pair it with a radius below. Territory-shaped lead lists (a franchise patch, a sales district) are what this is for.

## `positionRadiusKm` (type: `array`):

One or more radii in kilometres. Measured 2026-09-05 from central Stockholm (`59.3293,18.0686`): 10 km → 7,016 ads, 100 km → 12,652.

## `occupationField` (type: `array`):

The 21 top-level occupation areas, OR-ed server-side. Live sizes measured 2026-09-05: Hälso- och sjukvård 5,961 · Försäljning/inköp/marknadsföring 4,326 · Administration/ekonomi/juridik 3,517 · Data/IT 2,642.

## `occupationGroup` (type: `array`):

Narrower than a field. Accepts either the taxonomy concept id or the legacy SSYK code — both work (measured 2026-09-05: `Z6TY_xDf_Yup` and `9111` each return 962 ads for "Städare"). Codes come from taxonomy.jobtechdev.se. This value is sent to the API exactly as typed: unlike region, municipality or occupation field there is no table to resolve it against, so an unknown code comes back as HTTP 200 with ZERO rows rather than an error. The run logs a notice when this field is used, and names it again if the result is empty. Nothing is billed either way.

## `occupationName` (type: `array`):

The narrowest occupation level — one named job title. Measured 2026-09-05: `KF64_AvU_S4N` ("Städare") returns 952 ads. Concept ids come from taxonomy.jobtechdev.se. Sent to the API exactly as typed — there is no table to resolve an unknown concept id against, so a bad value returns a clean, empty, successful search rather than an error. The run logs a notice when this field is used, and names it again if the result is empty.

## `occupationCollection` (type: `array`):

Curated cross-cutting bundles maintained by Arbetsförmedlingen. Measured 2026-09-05: Bygg → 2,450 ads.

## `skill` (type: `array`):

Taxonomy competency codes. These tags are SPARSE — only 1.9% of ads carry `must_have.skills` (38 of the 2,000 newest ads, run BEjkBLIO8FBrVejcC, 2026-09-05) — so a skill filter returns a small, high-precision slice, never the whole occupation. Sent to the API exactly as typed, and it must be a taxonomy CONCEPT ID, not a word: `skill: "Python"` returned API total 0 on 2026-09-06 (run `Ldks48nqu3M7SXWN0`). There is no table to resolve a bad value against, so it comes back as a clean empty search — the run logs a notice when this field is used and names it again if the result is empty. Nothing is billed.

## `language` (type: `array`):

Taxonomy language codes. Measured 2026-09-05: `NVxJ_hLg_TYS` (Engelska) → 2,758 ads. 12.6% of ads carry a language requirement (251 of the 2,000 newest ads, run BEjkBLIO8FBrVejcC, 2026-09-05). Sent to the API exactly as typed — a bad concept id returns a clean, empty, successful search rather than an error, so the run logs a notice when this field is used and names it again if the result is empty.

## `employmentType` (type: `array`):

OR-ed server-side. Live sizes measured 2026-09-05: Vanlig anställning 33,987 · Behovsanställning · Tidsbegränsad · Vikariat · Sommarjobb 367. Note the taxonomy API lists only five of these seven; the two missing ones (including the largest) were recovered from the live corpus.

## `worktimeExtent` (type: `array`):

Live sizes measured 2026-09-05: Heltid 30,494 · Deltid 6,313.

## `parttimeMin` (type: `integer`):

Filters on the ad's stated employment-percentage band. Measured 2026-09-05: minimum 50 → 30,499 ads.

## `parttimeMax` (type: `integer`):

Pair with the minimum to isolate genuine part-time roles. Measured 2026-09-05: 0–50 → 2 ads, 50–100 → 30,499.

## `duration` (type: `array`):

How long the job lasts. Live sizes measured 2026-09-05: Tills vidare 28,076 · 6 månader eller längre 5,042. As with employment type, the taxonomy API is missing a value the corpus actually uses; both lists are merged here.

## `experience` (type: `string`):

Live sizes measured 2026-09-05: requires experience 38,282 · does NOT require experience 2,964. Leave on "Any" to ignore.

## `drivingLicenseRequired` (type: `string`):

Live sizes measured 2026-09-05: licence required 13,551 · not required 27,695.

## `drivingLicense` (type: `array`):

Live sizes measured 2026-09-05: B 6,384 (by far the most common) · C · CE · D. Only 16.2% of ads name a specific class (367 of the 2,268-ad 2026-09-04 day sweep, run `iDb78sXSeDHqPH5gN`, 2026-09-07).

## `remote` (type: `string`):

Phrase-matched by Arbetsförmedlingen over the ad text. Live sizes measured 2026-09-05: likely remote-friendly 675 · not 40,571. This is the filter that actually works for remote work — see the honest note on "Workplace model" below.

## `workplaceModel` (type: `array`):

**Honest warning, re-measured 2026-09-05:** this parameter does not currently filter anything. "Arbete på plats" (on-site) returns the ENTIRE corpus unfiltered — the on-site total equalled the unfiltered total exactly, 41,246 = 41,246, and "Distansarbete" and "Hybridarbete" each return 0 — every live ad carries the on-site concept. It is exposed because the API accepts it and Arbetsförmedlingen may start populating it, and it is passed through exactly as you set it, but for remote work use "Remote-friendly ads" above instead.

## `trainee` (type: `string`):

Phrase-matched. 13 live ads — genuinely rare, so expect a very small result set.

## `apprenticeship` (type: `string`):

Phrase-matched. 19 live ads.

## `openForAll` (type: `string`):

Ads that explicitly welcome every applicant regardless of background. 1,456 live, measured 2026-09-05.

## `franchise` (type: `string`):

Phrase-matched. 11 live ads.

## `hireWorkPlace` (type: `string`):

Ads renting out a chair or a workspace rather than employing. 6 live ads — salons and clinics.

## `label` (type: `array`):

Swedish employment-subsidy programme labels. Live sizes measured 2026-09-05: `nystartsjobb` 1,011 · `etableringsjobb` 343. A subsidised hire is a distinct buying signal. Sent to the API exactly as typed — a bad value returns a clean, empty, successful search rather than an error, so the run logs a notice when this field is used and names it again if the result is empty.

## `employer` (type: `array`):

Account-based prospecting: watch named companies. Accepts a company name OR a Swedish organisation number (digits only — spaces and dashes are stripped for you).

The API's own `employer=` parameter is a FULL-TEXT match over the employer name, not an exact one, so on its own it is useless here: measured 2026-09-06, `Norrköpings kommun` matched 4,061 ads — every municipality in Sweden, because *kommun* alone matches 4,234 — while that employer itself had 43; `Spotify AB` matched 29,291 of 41,232 while `Spotify` matched 0; and a company that does not exist, `Kommun som inte finns AB`, matched 33,533.

This actor therefore sends it to narrow the crawl and then RE-CHECKS every row against the name or organisation number you typed, dropping the rest BEFORE billing — you are never charged for another company's ad. Proven 2026-09-07: `Norrköpings kommun` delivered and billed 22 rows, every one of them that employer (run `tnSYbgB5S1WhKyEaA`); the non-existent company delivered and billed 0 (run `qIiduKkmWRb4D8RcT`). Expect a large "dropped by filters" count in the log — that is the filter working. An organisation number is matched exactly and is the more reliable input; a name that shares a common word with thousands of others can hit the API's 2,100-row per-query ceiling before the scan finishes, which the run reports rather than passing off as complete.

## `publishedWithinDays` (type: `integer`):

The "new ads since yesterday" lever, and the cheapest way to run this as a daily monitor. Sweden publishes roughly 2,100–2,500 new ads per WEEKDAY (measured 2026-09-05: 2,478 published on 09-01, 2,131 on 09-02, 2,115 on 09-03, 2,299 on 09-04), which is already over the API's per-query ceiling — so pair anything longer than a day with "Sweep the whole corpus". The weekend is far quieter: the trailing-24h window measured on Saturday 2026-09-05 held 165 ads. TIMEZONE: date filters are evaluated in UTC, while `publication_date` comes back in Stockholm local time (+01:00 winter, +02:00 summer), so about 1% of a one-day window's rows carry the neighbouring local date — measured 2026-09-06, a 2026-09-03..2026-09-04 request delivered 2,095 rows dated 09-03 and 20 dated 09-04 (run `Rcl2sclubWqOAgXcL`). The run logs this whenever a date filter is set.

## `publishedAfter` (type: `string`):

ISO date or datetime, e.g. `2026-09-01` or `2026-09-01T08:00:00`. Verified as a REAL server-side filter, not a label — measured 2026-09-05: `before 2026-01-01` returns 7 ads and a trailing six-hour window returned 46. TIMEZONE: date filters are evaluated in UTC, while `publication_date` comes back in Stockholm local time (+01:00 winter, +02:00 summer), so about 1% of a one-day window's rows carry the neighbouring local date — measured 2026-09-06, a 2026-09-03..2026-09-04 request delivered 2,095 rows dated 09-03 and 20 dated 09-04 (run `Rcl2sclubWqOAgXcL`). The run logs this whenever a date filter is set.

## `publishedBefore` (type: `string`):

Pairs with "Published after" for a backfill window. The live corpus runs from 2025-07-09 to today (oldest live ad confirmed 2026-09-05: 2025-07-09T15:34:40). TIMEZONE: date filters are evaluated in UTC, while `publication_date` comes back in Stockholm local time (+01:00 winter, +02:00 summer), so about 1% of a one-day window's rows carry the neighbouring local date — measured 2026-09-06, a 2026-09-03..2026-09-04 request delivered 2,095 rows dated 09-03 and 20 dated 09-04 (run `Rcl2sclubWqOAgXcL`). The run logs this whenever a date filter is set.

## `applicationDeadlineAfter` (type: `string`):

Applied over the rows (the API has no deadline filter). Use it to keep only ads that are still genuinely open. Evaluated in UTC against an `application_deadline` the API returns in Stockholm local time, so a deadline within an hour of midnight can fall on either side of the boundary.

## `applicationDeadlineBefore` (type: `string`):

Applied over the rows. Closing-soon ads are the urgent end of a recruitment-agency call list. Evaluated in UTC against an `application_deadline` the API returns in Stockholm local time, so a deadline within an hour of midnight can fall on either side of the boundary.

## `adUrls` (type: `array`):

Re-check named ads instead of searching — paste `https://arbetsformedlingen.se/platsbanken/annonser/31441923` or just `31441923`. Each ad is one billed row, exactly as in a search. A removed ad returns nothing and is NOT billed; the log names it — "Not delivered and NOT billed: <id>" — so a mixed batch tells you which ids expired.

## `onlyWithContact` (type: `boolean`):

Keep only ads carrying a contact e-mail, a contact phone number, OR an application e-mail such as jobb@foretaget.se. Measured 2026-09-07 on two differently-shaped runs: 57.8% qualify over run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04, 59.6% over run `jP1jGT0bR51D6UdWf`, the 2,000 most recently published ads — call it 58-60%. About a quarter of those qualify on the application inbox alone, which is a role address, not a named person. For a call list of actual humans use "Only ads with a named contact's e-mail or phone" below instead. Applied BEFORE billing — you never pay for a row this drops.

## `onlyWithNamedContact` (type: `boolean`):

Strictest reachability filter: keep only ads where the ad itself lists a contact person's e-mail or phone (the `contact_email` / `contact_phone` columns) — a generic application inbox never qualifies. Measured 2026-09-07: 47.5% qualify over run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04, 45.1% over run `jP1jGT0bR51D6UdWf`, the 2,000 most recently published ads; `contact_email` 41.1% / 39.8% and `contact_phone` 35.6% / 32.0%. Applied BEFORE billing — you never pay for a row this drops.

## `onlyWithOrgNumber` (type: `boolean`):

Keep only rows carrying the Swedish organisation number — the registry key that joins a lead to a real company record at Bolagsverket or SCB. Measured 2026-09-07: 99.4% fill over run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04 and 99.2% over run `jP1jGT0bR51D6UdWf`, the 2,000 most recently published ads, so this drops very little and guarantees a joinable list.

## `onlyWithoutAts` (type: `boolean`):

Keep only the ads with no application URL at all — employers who are hiring but have no applicant-tracking system in the loop. For an HR-tech or recruitment-software seller this is the highest-intent slice in the whole corpus. It is also the slice that moves most between samples: measured 2026-09-07, 11.9% over run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04 but 17.0% over run `jP1jGT0bR51D6UdWf`, the 2,000 most recently published ads, because a rolling 2,000 reaches back across the weekend. Budget 12-17%, not a fixed figure.

## `atsVendorFilter` (type: `array`):

Competitive displacement targeting: keep only employers whose apply link resolves to a named ATS. Measured 2026-09-07 over run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04 — Varbi 317, Visma Recruit 175, ReachMee 113, Recman 112, Teamtailor 74, Bullhorn 65, EasyApply 46, Ponty 40, Avaron 29, Talentech 29, PowerInit 27, Workday 23; 25 of the 49 recognised vendors appeared and 50.2% of ads resolved to a named vendor. The same parser over run `jP1jGT0bR51D6UdWf`, the 2,000 most recently published ads the same day resolved 40.5%, so budget 40-50%, not 50%. A rare vendor is a small slice of a big corpus: pair it with "Sweep the whole corpus" or the 2,100-row per-query ceiling will stop the scan long before it has examined every ad — the run warns you when that happens, and says so again if the result is zero rows.

## `excludeStaffingAgencies` (type: `boolean`):

Drops ads whose employer is itself a recruiter or staffing firm, so an agency buying this list gets END CLIENTS rather than its own competitors. Matched on the employer name against a 50-word Swedish agency vocabulary (bemanning, rekrytering, Manpower, Adecco, Randstad, StudentConsulting, …). Every row also carries `is_staffing_agency` so you can audit the call.

## `dedupeByEmployer` (type: `boolean`):

Collapse to a single row per organisation number for a COMPANY-level list rather than a vacancy-level one. Measured 2026-09-07: run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04 came from 951 distinct organisations (a 58% cut) and run `jP1jGT0bR51D6UdWf`, the 2,000 most recently published ads from 918 (a 54% cut) — and you are only billed for the rows you keep.

## `titleIncludeAny` (type: `array`):

Applied over the rows, diacritic-insensitively, so `sokerska` matches `söterska`. Use it to tighten a broad occupation filter without changing the query.

## `titleExcludeAny` (type: `array`):

Applied over the rows. Handy for stripping `praktik`, `sommarjobb` or `timvikarie` out of a permanent-hire list.

## `descriptionIncludeAny` (type: `array`):

Applied over the rows against the full ad text (filled on 2,268 of 2,268 rows — run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04, 2026-09-07). This is where technographic signals live — a named tool, a certification, a system the employer says they run. A rare phrase over a large corpus needs "Sweep the whole corpus" on, or the 2,100-row per-query ceiling stops the scan before it has read every ad.

## `outputShape` (type: `string`):

`Full` = every mapped field (~60 columns). `Lead` = the CRM spine only: employer, organisation number, contact, apply link, ATS vendor, location, occupation and dates. `Minimal` = the 21-field identity and reachability core.

## `outputFieldLabels` (type: `string`):

Arbetsförmedlingen publishes every label in Swedish. `English` swaps the CLOSED vocabularies (occupation field, employment type, working hours, duration, workplace model, pay structure) for an English gloss; `Both` adds them as extra `_en` columns. Place names, specific occupation names and the ad text always stay Swedish — machine-translating 290 municipality names would be a fabrication, not data.

## `includeDescriptionText` (type: `boolean`):

The plain-text ad body, filled on 2,268 of 2,268 rows (run `iDb78sXSeDHqPH5gN`, a 2,268-ad sweep of the full day 2026-09-04, 2026-09-07). On by default — it is where contact e-mail addresses, named systems and hiring context live. Turn it off for a slim CRM import.

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

The formatted `text_formatted` body. Off by default — it roughly doubles the row size and most pipelines want the plain text.

## `includeRawTaxonomyIds` (type: `boolean`):

Adds the `*_concept_id` columns beside every label, so you can join to taxonomy.jobtechdev.se or feed the ids straight back into this actor's own filters.

## `includeRawJson` (type: `boolean`):

Adds `raw`, the untouched JobTech hit, for pipelines that would rather map the fields themselves.

## `sort` (type: `string`):

`Newest first` is the right choice for lead work and for monitoring, and it is what makes contiguous paging safe here — measured 2026-09-07 (run `jP1jGT0bR51D6UdWf`): 2,000 rows across 20 pages, 2,000 unique ids, ZERO duplicates. `Relevance` only makes sense with a search keyword.

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

Hard cap on BILLED rows across the whole run — your budget guard. Counts rows DELIVERED and is enforced before delivery, so you are never charged past it, and paging continues past filtered rows until the cap is filled. If the run ends short of the cap the log says whether the source was exhausted or the API's 2,100-row per-query ceiling truncated the scan.

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

A per-keyword cap when you run several searches at once, so one broad keyword cannot eat the whole budget. Counts rows DELIVERED, not rows read: paging continues past ads your client-side filters drop until the cap is filled or the keyword runs out of ads, which is the same rule "Max ads to return" follows and the same thing you are billed for. If a keyword ends short of its cap the log says which — the source was exhausted (a complete answer), or the API's 2,100-row per-query ceiling stopped it (turn on "Sweep the whole corpus").

## `resultsPerPage` (type: `integer`):

The API accepts 1–100; 101 is HTTP 400. 100 is fastest and is the default — lower it only when you want a small run to stop precisely on a cap.

## `startOffset` (type: `integer`):

Skip the first N rows of a query. The API accepts 0-2000; 2001 is HTTP 400. If N lands past the end of the result set the run returns zero rows and says exactly that, naming N and the number of matching ads — it does not report an empty search.

## `fullCorpusMode` (type: `boolean`):

**Turn this on for any run that should return more than 2,100 ads.** A single API query can reach at most 2,100 rows (limit ≤ 100, offset ≤ 2000), and a one-DAY partition is already too big — measured 2026-09-05: 2,478 ads published on 09-01, 2,131 on 09-02, 2,115 on 09-03 and 2,299 on 09-04, so a day-window crawler silently drops ads on a green run. This mode probes each publication-date window with a free `limit=0` call, reads the true total, and bisects the window until every slice fits under the ceiling. A whole-corpus sweep takes roughly 40–80 requests.

## `deduplicate` (type: `boolean`):

On (default): each ad is delivered and BILLED at most once, even when several of your search keywords or date windows return it. Partitioned sweeps overlap by design, so leaving this on is what stops you paying twice for one ad.

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

Name this watch (e.g. `sthlm-nurses`) and the actor remembers every ad id it has delivered under that name in a NAMED key-value store. On the next run with the same key, ads already seen are dropped BEFORE delivery — so a daily monitor only ever bills you for genuinely new vacancies. Leave empty for a one-off scrape.

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

Apify DATACENTER proxies are enough — the full escalation ladder was measured and all four tiers returned identical bytes: direct 1,449 ms, datacenter 1,644 ms, residential SE 1,666 ms, residential US 2,849 ms. Residential is 1.7x slower for no benefit; do not pay for it. The API is open Swedish government data and is not geo-blocked.

## `proxyCountry` (type: `string`):

Optional two-letter code. Measured: SE and US return byte-identical responses, so leave this empty unless your own network policy requires a pin.

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

Only used when fetching specific ad IDs. Search paging is sequential on purpose: this is a public government API, and one pinned session paging politely measured 100% success over 20 pages (2,000 rows, run `jP1jGT0bR51D6UdWf`, 2026-09-07).

## `maxRequestRetries` (type: `integer`):

Keep this at 3 or more. The API itself never rate-limited across ~200 probe calls; the residual failure is the Apify proxy tunnel dropping a large response (ECONNRESET), and a retry clears it.

## `requestTimeoutSecs` (type: `integer`):

A 100-row page measured 2–9 s through a datacenter proxy depending on load. 60 s leaves room for a slow tunnel.

## `requestDelayMs` (type: `integer`):

Politeness pause between paged requests against a public government API.

## Actor input object example

```json
{
  "searchQueries": [],
  "qfields": [],
  "freetextBoolMethod": "",
  "disableSmartFreetext": false,
  "enableFalseNegativeSearch": false,
  "region": [],
  "municipality": [],
  "country": [],
  "unspecifiedSwedenWorkplace": false,
  "abroad": false,
  "position": "",
  "positionRadiusKm": [],
  "occupationField": [],
  "occupationGroup": [],
  "occupationName": [],
  "occupationCollection": [],
  "skill": [],
  "language": [],
  "employmentType": [],
  "worktimeExtent": [],
  "duration": [],
  "experience": "",
  "drivingLicenseRequired": "",
  "drivingLicense": [],
  "remote": "",
  "workplaceModel": [],
  "trainee": "",
  "apprenticeship": "",
  "openForAll": "",
  "franchise": "",
  "hireWorkPlace": "",
  "label": [],
  "employer": [],
  "publishedWithinDays": 1,
  "publishedAfter": "",
  "publishedBefore": "",
  "applicationDeadlineAfter": "",
  "applicationDeadlineBefore": "",
  "adUrls": [],
  "onlyWithContact": false,
  "onlyWithNamedContact": false,
  "onlyWithOrgNumber": false,
  "onlyWithoutAts": false,
  "atsVendorFilter": [],
  "excludeStaffingAgencies": false,
  "dedupeByEmployer": false,
  "titleIncludeAny": [],
  "titleExcludeAny": [],
  "descriptionIncludeAny": [],
  "outputShape": "full",
  "outputFieldLabels": "sv",
  "includeDescriptionText": true,
  "includeDescriptionHtml": false,
  "includeRawTaxonomyIds": false,
  "includeRawJson": false,
  "sort": "pubdate-desc",
  "maxItems": 50,
  "maxResultsPerQuery": 0,
  "resultsPerPage": 100,
  "startOffset": 0,
  "fullCorpusMode": false,
  "deduplicate": true,
  "stateKey": "",
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "proxyCountry": "",
  "maxConcurrency": 3,
  "maxRequestRetries": 3,
  "requestTimeoutSecs": 60,
  "requestDelayMs": 250
}
```

# Actor output Schema

## `jobAds` (type: `string`):

Headline, employer, organisation number, municipality, region, occupation, employment type, publication date and deadline — plus the named contact person with e-mail and phone, the apply link, and the ATS vendor resolved from its host.

# 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 = {
    "publishedWithinDays": 1,
    "outputShape": "full",
    "sort": "pubdate-desc",
    "maxItems": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/platsbanken-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 = {
    "publishedWithinDays": 1,
    "outputShape": "full",
    "sort": "pubdate-desc",
    "maxItems": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/platsbanken-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 '{
  "publishedWithinDays": 1,
  "outputShape": "full",
  "sort": "pubdate-desc",
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/platsbanken-jobs-scraper --silent --output-dataset

```

## MCP server setup

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