Norway Jobs Search Api
Pricing
from $2.50 / 1,000 unique jobs
Norway Jobs Search Api
Search, normalize and deduplicate Norwegian jobs from LinkedIn, Jobbnorge and NAV/Arbeidsplassen. Strict filters, ranking, geo search and monitoring.
Pricing
from $2.50 / 1,000 unique jobs
Rating
0.0
(0)
Developer
Automation Tech
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Norway Jobs Search API — Multi-Source Job Aggregator
Search Norwegian vacancies across Jobbnorge, LinkedIn Jobs, and the supported NAV Arbeidsplassen feed in one Actor run. Results are converted to one canonical schema, filtered locally, conservatively deduplicated across sources, ranked, and emitted with complete source provenance.
This Actor is designed for job seekers, recruiters, job boards, analysts, automation workflows, and AI agents that need structured Norwegian job data rather than three incompatible source payloads.
What it does
One input search runs the selected source adapters in parallel and processes their records through a deterministic pipeline:
input and Norwegian query expansion-> source-specific HTTP collection-> canonical normalization-> cheap safe exclusions-> conservative cross-source clustering-> local geo enrichment and distance-> strict local filters-> explainable relevance score-> monitor/change classification-> final unique maxResults-> Dataset + unique-job event
maxResults means final unique canonical vacancies. It never means rows per source or raw pre-deduplication candidates.
Supported sources
| Source | Method | Authentication | Failure behavior |
|---|---|---|---|
| Jobbnorge | Public v3 search API with bounded v2 detail enrichment | None | Keeps search summaries if detail enrichment fails |
| LinkedIn Jobs | No-login guest search and job-detail HTTP endpoints | None | Preserves collected cards on 429/later-page failures; rejects non-authoritative HTTP-200 challenge or malformed pages |
| NAV Arbeidsplassen | Official pam-stilling-feed change feed and detail endpoint | NAV_JOB_FEED_TOKEN secret | Skips only NAV when the token is absent; cached state survives partial syncs |
FINN.no is intentionally not implemented. NAV feed records from FINN are not part of the supported feed, and no source adapter depends on FINN.
NAV consumer token
NAV’s old public search feed was discontinued on 1 May 2025. The supported replacement is a consumer change feed that requires a bearer token and local filtering. Register free of charge by accepting NAV’s API terms and contacting nav.team.arbeidsplassen@nav.no with the consumer information described in the NAV Job Vacancy Feed documentation.
Store the token as the secret environment variable NAV_JOB_FEED_TOKEN. Do not place it in Actor input, source code, logs, screenshots, or Git.
The first NAV run starts no earlier than six months back, upserts active feed events, removes inactive events, and stores a versioned cursor plus compact active summaries in the named key-value store norway-jobs-search-api-state. If the pagination limit is reached, a later run resumes from the saved cursor. Details are fetched only for bounded plausible matches.
Why use this Actor
- Search multiple Norwegian job sources with one input.
- Receive one stable output shape instead of source-specific HTML and JSON.
- Apply filters locally instead of trusting each source search engine’s semantics.
- Consolidate the same vacancy found on several portals into one result.
- Keep every contributing source ID and URL for auditability.
- Rank results with deterministic, explainable
matchScoreandmatchReasons. - Run incrementally with
NEW,UPDATED,REAPPEARED, conservativeREPOSTED, and optionalUNCHANGEDstates. - Use Apify Datasets, API, schedules, and webhooks without an external database.
Multi-source aggregation and canonical output
Each adapter retains its own raw type. Canonicalization happens only at the adapter boundary, preventing useful source-specific fields from being forced into a lossy fake common payload.
The canonical record includes:
- normalized and original title/company
- description as safe normalized plain text
- structured location and optional distance
- publication/update/deadline dates
- employment type and work arrangement
- cautiously parsed salary
- source and application URLs
- bounded source-specific metadata on every provenance entry
- relevance and data-quality scores
- canonical ID, merge confidence, and full provenance
- monitoring timestamps and change classification
Missing fields remain null. The Actor does not invent coordinates, salary amounts, deadlines, employer identifiers, or timestamp precision.
Cross-source deduplication
Deduplication is conservative and deterministic, not perfect. False merges are treated as more harmful than remaining duplicates.
Strong signals include shared application endpoints, stable external vacancy IDs, direct cross-links, and identical source records. Records without a strong identifier are admitted to comparison through bounded blocking keys and scored using corroborating title, company, organization, location, description, publication date, deadline, and application-domain signals.
The clusterer validates every new member against the existing cluster. A weak transitive bridge cannot merge candidates with conflicting organizations, seniority, distant locations, materially different rich descriptions, or clearly separate application endpoints. Records from one source with different IDs remain separate unless they share a strong vacancy identity.
For a single-source record, deduplicationConfidence is 0, meaning no cross-source merge was performed. For a merged cluster it is a deterministic merge-strength indicator from 0 to 1, not a statistical probability.
Canonical source selection is based on record completeness—description, organization number, structured geo, dates, employment metadata, application URL, and stable identifiers—not a hardcoded source priority. Complementary fields are merged safely, and all source provenance remains visible.
Strict filtering
Source engines are discovery mechanisms. The Actor enforces these rules after normalization:
- query or expanded synonym match
includeKeywordsandexcludeKeywordstitleMustIncludeAnyandtitleExcludedescriptionMustIncludeAnycompanyIncludeandcompanyExclude- normalized employment type
- remote/hybrid/on-site arrangement
- publication freshness when known
- location and radius rules
Matching is token-aware. A short query such as IT matches an IT-konsulent token, not arbitrary words such as kvalitetssikring. Norwegian æ, ø, and å, Unicode forms, dash variants, punctuation, and whitespace are normalized consistently.
Arrays named ...IncludeAny require at least one listed term. Any exclusion term rejects the record. If a requested structured field such as employment type is missing, that record cannot satisfy the explicit structured filter.
Norwegian query semantics
The Actor contains a deliberately small, extensible dictionary for common Norwegian/English occupation variants. For example, reservedeler can discover and rank reservedelsmedarbeider, delelager, parts advisor, and related variants; bilmekaniker can match kjøretøymekaniker and automotive technician.
Expansion helps source discovery and ranking. Strict local rules remain authoritative. No LLM, embedding service, paid taxonomy, or external search API is used.
Geo and radius search
Requested origins are resolved through a bounded local list of common Norwegian places. Source-provided coordinates are preferred. When a source supplies only a recognized city or municipality, the Actor uses the local city centroid and labels the location precision internally. Distance is the Haversine distance to the closest requested origin.
If a job has no reliable or locally resolvable coordinate, location.distanceKm remains null. Such jobs are retained by default because excluding weak source data can hide valid vacancies. Set requireCoordinatesForRadius: true to exclude them when radiusKm is active.
Relevance ranking
matchScore is an explainable 0–100 relevance score, not a probability. Centralized stable weights reward exact title phrases, title tokens, expanded synonyms, description relevance, requested keywords, location match, proximity, freshness, and requested structured attributes.
Example reasons:
["Strong title match","Matched requested keyword","Published 2 days ago","28 km from requested location"]
The same normalized input and job data produce the same score and ordering.
Search mode
mode: "search" returns current matching canonical jobs. changeType: "NEW" in search mode identifies a current result row and does not claim comparison with persisted history.
Monitor mode
mode: "monitor" compares jobs after cross-source deduplication. Provide stateKey, or let the Actor derive one from the normalized search configuration. Different queries, locations, filters, sources, result limits, and sorting configurations derive different keys.
Monitor state is versioned and stored in the named key-value store. Version-1 state is migrated to the compact version-2 representation; a deeply corrupt or incompatible value is reset with a warning rather than crashing mysteriously. Content values are stored as field digests, so descriptions do not inflate the state store.
- First observation:
NEW - Meaningful normalized content changed:
UPDATEDwithchangedFields - Previously observed, reliably absent, then returned:
REAPPEARED - New source identity plus later publication and identical normalized description: conservative
REPOSTED - No meaningful change:
UNCHANGED
Formatting-only description changes are ignored. Source-count changes alone do not make a canonical vacancy new. If any selected source is partial, failed, or skipped, missing jobs are not marked absent because source coverage is not reliable.
UNCHANGED jobs are not emitted by default. Set emitUnchanged: true if the full current monitor result set is required.
Monitor changes use acknowledgement-based delivery. Detection can queue more changed vacancies than maxResults, but a change is acknowledged only after its canonical row is successfully pushed to the Dataset. Unemitted changes remain pending and are prioritized on later runs, preventing the final result limit or a PPE budget limit from silently consuming monitor history. A crash after a Dataset push but before state persistence can cause at-least-once redelivery; downstream consumers should use canonicalJobId, changeType, and timestamps idempotently.
Do not overlap runs that use the same stateKey. The Apify key-value store does not provide a compare-and-swap operation for this state document, so concurrent writers could overwrite newer history. Separate schedules with different search configurations derive different keys automatically.
Input example
{"query": "reservedeler","locations": ["Jessheim","Oslo","Lillestrøm"],"radiusKm": 60,"publishedWithinDays": 7,"sources": ["arbeidsplassen","jobbnorge","linkedin"],"includeKeywords": ["reservedeler","parts","lager"],"titleExclude": ["manager"],"maxResults": 50,"mode": "search","sortBy": "match"}
The no-secret default selects Jobbnorge and LinkedIn. Add arbeidsplassen after configuring NAV_JOB_FEED_TOKEN.
Output example
Three listings that point to one vacancy become one Dataset item and one unique-job event:
{"canonicalJobId": "job_2bd71d...","title": "Reservedelsmedarbeider","normalizedTitle": "reservedelsmedarbeider","company": "Eksempel Bil AS","normalizedCompany": "eksempel bil","employerOrgNumber": "999888777","description": "Vi søker en reservedelsmedarbeider ...","location": {"displayName": "Jessheim","city": "Jessheim","municipality": "Ullensaker","county": "Akershus","postalCode": "2050","countryCode": "NO","latitude": 60.1415,"longitude": 11.1752,"distanceKm": 0},"publishedAt": "2026-08-29T08:00:00.000Z","deadline": "2026-09-15","employmentType": "FULL_TIME","workArrangement": "ON_SITE","salary": null,"applyUrl": "https://jobs.example.no/vacancy/123","matchScore": 92,"matchReasons": ["Strong title match","Matched requested keyword","Matched location: Jessheim"],"sourceCount": 3,"canonicalSource": "arbeidsplassen","sources": [{"source": "arbeidsplassen","sourceId": "nav-uuid","url": "https://arbeidsplassen.nav.no/stillinger/stilling/nav-uuid","applyUrl": "https://jobs.example.no/vacancy/123","publishedAt": "2026-08-29T08:00:00.000Z","sourceMetadata": {"occupationCategory": "Bilmekaniker"}},{"source": "jobbnorge","sourceId": "306169","url": "https://www.jobbnorge.no/ledige-stillinger/stilling/306169","applyUrl": "https://jobs.example.no/vacancy/123","publishedAt": "2026-08-29","sourceMetadata": {"teaser": "Arbeid med deleflyt og kundeservice"}},{"source": "linkedin","sourceId": "4448056298","url": "https://www.linkedin.com/jobs/view/4448056298","applyUrl": "https://jobs.example.no/vacancy/123","publishedAt": "2026-08-29","sourceMetadata": {"seniority": "Associate","industries": "Automotive"}}],"deduplicationConfidence": 0.99,"firstSeenAt": "2026-08-31T12:00:00.000Z","lastSeenAt": "2026-08-31T12:00:00.000Z","lastChangedAt": null,"changeType": "NEW","dataQualityScore": 96}
Source and pipeline statistics
The OUTPUT key in the default key-value store contains per-source status, requests, raw/normalized jobs, retries, pages, coverageComplete, warnings, elapsed time, and pipeline counts. coverageComplete is conservative: it is false whenever a source stopped at a result/page bound or NAV did not evaluate its full active cache. LinkedIn always reports incomplete absence coverage because the guest endpoint has no stable authoritative exhaustion contract. Monitor mode marks unseen jobs absent only when every selected source reports both SUCCESS and complete coverage; therefore searches that include LinkedIn detect new/updated rows but never overclaim disappearance or reappearance.
rawCandidates -> normalizedCandidates -> afterCheapFilters-> duplicateRowsMerged / canonicalUniqueJobs-> afterStrictFilters -> finalResults
Large upstream payloads and credentials are never logged.
Pricing architecture
The intended Store model is pay per unique final canonical job. Each final Dataset row is written through:
await Actor.pushData(job, 'unique-job');
The Apify SDK applies the event to the same pushed unit and respects the run’s charge limit. Raw source rows, failed requests, filtered records, and duplicate source listings are not custom-charged.
When configuring PAY_PER_EVENT in Apify Console, create the unique-job event and set its price. Apify can also synthesize an apify-default-dataset-item event for default Dataset writes. Disable or price that synthetic event appropriately if billing must be exclusively one unique-job charge per emitted vacancy.
The repository deliberately does not contain an event price or production monetization settings; those are account-level deployment decisions.
API, schedules, and webhooks
After deployment, start a run with the standard Apify API:
curl -X POST \"https://api.apify.com/v2/acts/USERNAME~norway-jobs-search-api/runs?token=APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"query":"sykepleier","locations":["Bergen"],"maxResults":25}'
Use Apify schedules for recurring monitor searches and webhooks to forward completed run or Dataset events to another workflow. Monitor mode uses the stable named state store, so scheduled runs share history when their stateKey or derived configuration is the same.
Do not place API or NAV tokens directly in scripts committed to Git. Use Apify secrets or your automation platform’s secret store.
Local development
Requires Node.js 22 or later.
npm cinpm run typechecknpm run lintnpm testnpm run build
Opt-in live sanity tests are run separately:
$npm run test:live
Build the same production image with Docker or Podman:
$podman build -t norway-jobs-search-api .
No Apify account, token, or production Actor is needed for deterministic tests.
Known limitations
- NAV is a supported change feed, not a server-side keyword API. The first token-authenticated run may need several feed pages and later runs resume from persisted state. Description-only discovery outside fetched candidates is inherently limited by the official interface.
- LinkedIn’s guest HTML and traffic controls can change. The connector uses bounded retries, adaptive backoff with jitter,
Retry-After, low concurrency, and partial-result preservation, but sustained 429 responses can reduce coverage. - LinkedIn coverage is deliberately conservative: non-empty HTTP-200 responses without wholly valid recognized job cards, mixed malformed cards, and redirects away from the exact guest-search endpoint are treated as incomplete failures. A whitespace-only response is the currently recognized empty terminal form; upstream changes to no-result markup will appear as an actionable source warning until a fixture-backed form is added.
- Jobbnorge’s API currently caps practical page size and exposes richer descriptions through separate detail requests; detail failures retain the summary record.
- City-centroid geo enrichment covers a bounded local list. Unknown or ambiguous places keep null coordinates; radius behavior is explicit and configurable.
- Different vacancies can legitimately look nearly identical, and the same vacancy can differ materially across portals. Deduplication is conservative, so some duplicates can remain.
- Salary parsing recognizes confident NOK ranges/hourly forms and retains nonnumeric text such as salary grades, tariff, or “by agreement.” It does not map salary grades to currency amounts.
REPOSTEDdetection is deliberately rare. Ambiguous returns are classified asREAPPEAREDorNEWrather than overclaimed.- Runs sharing one monitor
stateKeymust not overlap. Monitor delivery is at least once around a crash between Dataset push and state acknowledgement.
Security and data handling
- No login cookies, browser sessions, LLM APIs, paid data services, proxies, or external database are required.
- Network operations use bounded timeouts, retries, pagination, and a 32 MiB response-body ceiling.
- NAV bearer-token requests are restricted to the exact supported HTTPS API origin/path and reject redirects, URL credentials, unsafe upstream links, and unsafe persisted cursors before a token is sent.
- Source HTML is converted to plain text; scripts, styles, templates, and SVG content are removed.
- Normalized descriptions are capped at 200,000 characters, free-form input strings are bounded before normalization, and persisted source/monitor state is deeply validated.
- Contact-person data from NAV is intentionally not retained.
- Tokens, cookies, raw payloads, and full credential-bearing URLs are not logged.
.env, credentials, private keys, local storage, coverage, and build artifacts are excluded from Git.
Deployment handoff
- Import or connect this GitHub repository in Apify and select the
mainbranch. - Add
NAV_JOB_FEED_TOKENas a secret environment variable only if Arbeidsplassen is enabled. - Build the Actor with 256 MB memory initially; increase to 512 MB only if real NAV cache volume justifies it.
- Run a no-secret QA search against Jobbnorge and LinkedIn, then a NAV-only QA run after the initial feed sync.
- Enable PAY_PER_EVENT, define
unique-job, and review the synthetic default-Dataset event so one emitted vacancy has the intended single price. - Verify Dataset view,
OUTPUTsummary, monitor second-run/backlog behavior, source warnings, and non-overlapping schedules perstateKeybefore publishing.
This repository is deployment-ready but intentionally does not create, deploy, or publish an Actor in any Apify account.
Practical API examples
The scripts in examples/ start bounded runs of the published Actor through the Apify REST API and print the default Dataset. They use Node’s built-in fetch, so they add no production dependency.
examples/basic-search.mjs— query Jobbnorge + LinkedIn, then receive final normalized, deduplicated job records.examples/monitor-jobs.mjs— keep a stableWATCH_KEYand receiveNEWor changed matches on later runs.
Run an example with an environment variable; never hard-code an Apify token:
`APIFY_TOKEN=... node examples/basic-search.mjs "data analyst"``APIFY_TOKEN=... WATCH_KEY=my-oslo-watch node examples/monitor-jobs.mjs`
The examples intentionally omit NAV Arbeidsplassen because it requires the separately configured NAV_JOB_FEED_TOKEN. Set a small maxResults while you validate your own integration.
The examples call the published Apify Actor. A matching public Example Task link will be added here only after its URL has been verified from Apify Console.