{
  "openapi": "3.0.1",
  "info": {
    "title": "Duunitori Jobs Scraper - Finland Job Ads, Apply URL & Y-tunnus",
    "description": "Scrape duunitori.fi, Finland's largest job board (~17,500 live ads), by keyword, city, region, industry, occupation, contract type, language, remote and salary-published filters. Optional detail fetch adds the external apply URL + ATS domain; optional employer fetch adds the Y-tunnus business ID.",
    "version": "0.1",
    "x-build-id": "o4T4DMfx7T7zzPVhc"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/scrapersdelight~duunitori-jobs-scraper/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-scrapersdelight-duunitori-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/scrapersdelight~duunitori-jobs-scraper/runs": {
      "post": {
        "operationId": "runs-sync-scrapersdelight-duunitori-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/scrapersdelight~duunitori-jobs-scraper/run-sync": {
      "post": {
        "operationId": "run-sync-scrapersdelight-duunitori-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "properties": {
          "startUrls": {
            "title": "Start URLs",
            "type": "array",
            "description": "Paste Duunitori search URLs straight from your browser — every filter in the URL is kept exactly as you set it, including ones this form does not expose. Use this to drive the Actor from a spreadsheet of saved searches. Any ?sivu= page number is stripped; the Actor paginates the whole result set itself. Only duunitori.fi /tyopaikat URLs are accepted.",
            "items": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "url": {
                  "type": "string",
                  "title": "URL of a web page",
                  "format": "uri"
                }
              }
            }
          },
          "searchQueries": {
            "title": "Search keywords",
            "type": "array",
            "description": "Free-text keywords, one per line — the site's own 'haku' box. Works in Finnish AND English (measured: 'developer' 122 hits, 'kehittäjä' 7, so both are worth listing). Each keyword is crawled as its own search and the results are merged and deduplicated by job id, so you are never billed twice for a job two keywords both matched. Leave empty to sweep every live ad.",
            "items": {
              "type": "string"
            }
          },
          "searchDescriptions": {
            "title": "Search inside ad text too",
            "type": "boolean",
            "description": "Match your keywords against the body of the ad, not just the job title (Duunitori's 'search_also_descr'). Measured: 'developer' returns 122 hits on titles alone and 190 with ad text included — good for finding a skill or tool named deep inside a posting.",
            "default": false
          },
          "municipalities": {
            "title": "Cities / municipalities",
            "type": "array",
            "description": "Finnish city names as Duunitori spells them, one per line — helsinki, vantaa, turku, oulu, jyväskylä, kuopio. Verified live: helsinki 3,429 ads, vantaa 1,157, turku 1,076, oulu 903, jyväskylä 567, kuopio 457. A name Duunitori does not know returns zero rows rather than an error, so the run will tell you in the log instead of silently handing back the unfiltered board.",
            "items": {
              "type": "string"
            }
          },
          "regions": {
            "title": "Regions (maakunnat)",
            "type": "array",
            "description": "Official Finnish regions. Measured sizes: uusimaa 6,143 ads, pirkanmaa 2,201, lappi 1,314. Note these are browse PATHS on Duunitori, so at most one region applies per request — pick several and each is crawled separately and merged.",
            "items": {
              "type": "string",
              "enum": [
                "ahvenanmaan-maakunta",
                "etela-karjala",
                "etela-pohjanmaa",
                "etela-savo",
                "kainuu",
                "kanta-hame",
                "keski-pohjanmaa",
                "keski-suomi",
                "kymenlaakso",
                "lappi",
                "paijat-hame",
                "pirkanmaa",
                "pohjanmaa",
                "pohjois-karjala",
                "pohjois-pohjanmaa",
                "pohjois-savo",
                "satakunta",
                "uusimaa",
                "varsinais-suomi"
              ],
              "enumTitles": [
                "Åland",
                "Etelä-Karjala",
                "Etelä-Pohjanmaa",
                "Etelä-Savo",
                "Kainuu",
                "Kanta-Häme",
                "Keski-Pohjanmaa",
                "Keski-Suomi",
                "Kymenlaakso",
                "Lappi",
                "Päijät-Häme",
                "Pirkanmaa",
                "Pohjanmaa",
                "Pohjois-Karjala",
                "Pohjois-Pohjanmaa",
                "Pohjois-Savo",
                "Satakunta",
                "Uusimaa",
                "Varsinais-Suomi"
              ]
            },
            "default": []
          },
          "industries": {
            "title": "Industries (alat)",
            "type": "array",
            "description": "Industry slugs from https://duunitori.fi/tyopaikat/selaa/alat, one per line. Examples with live counts: talonrakennus 1,444 · tehdas-ja-tuotantotyontekijat 1,334 · ohjelmointi-ja-ohjelmistokehitys 618 · hoitajat 684 · laakarit 478 · lakiala 84. You can also paste the full browse URL. Each industry is crawled as its own search and merged by job id.",
            "items": {
              "type": "string"
            }
          },
          "occupations": {
            "title": "Occupations (ammatit)",
            "type": "array",
            "description": "Duunitori's ten cross-industry occupation buckets. Measured: kuljettaja (driver) 744 ads.",
            "items": {
              "type": "string",
              "enum": [
                "asentaja",
                "asiantuntija",
                "esihenkilo",
                "insinoori",
                "kuljettaja",
                "myyja",
                "paallikko",
                "sijainen",
                "tulityokortti",
                "vuorotyo"
              ],
              "enumTitles": [
                "asentaja (installer/fitter)",
                "asiantuntija (specialist)",
                "esihenkilö (supervisor)",
                "insinööri (engineer)",
                "kuljettaja (driver)",
                "myyjä (sales assistant)",
                "päällikkö (manager)",
                "sijainen (temp cover)",
                "tulityökortti (hot-work card)",
                "vuorotyö (shift work)"
              ]
            },
            "default": []
          },
          "employmentType": {
            "title": "Employment type",
            "type": "array",
            "description": "Full-time (15,469 ads) or part-time (2,956). Pick both to cover everything that declares a type.",
            "items": {
              "type": "string",
              "enum": [
                "full_time",
                "part_time"
              ],
              "enumTitles": [
                "Full-time (kokoaikatyö)",
                "Part-time (osa-aikatyö)"
              ]
            },
            "default": []
          },
          "contractType": {
            "title": "Contract type",
            "type": "array",
            "description": "Permanent (13,569 ads), fixed-term (4,658) or summer job (89).",
            "items": {
              "type": "string",
              "enum": [
                "permanent",
                "fixed_term",
                "summer_job"
              ],
              "enumTitles": [
                "Permanent (vakituinen)",
                "Fixed term (määräaikainen)",
                "Summer job (kesätyö)"
              ]
            },
            "default": []
          },
          "adLanguage": {
            "title": "Ad language",
            "type": "array",
            "description": "Language the ad itself is written in: Finnish (13,560), English (1,778) or Swedish (326). English-language ads are the ones an international recruiter can act on without translation.",
            "items": {
              "type": "string",
              "enum": [
                "fi_lang",
                "eng_lang",
                "swe_lang"
              ],
              "enumTitles": [
                "Finnish",
                "English",
                "Swedish"
              ]
            },
            "default": []
          },
          "remoteOnly": {
            "title": "Remote work only",
            "type": "boolean",
            "description": "Only ads flagged as remote-friendly (926 live).",
            "default": false
          },
          "withSalaryOnly": {
            "title": "Only ads that publish a salary",
            "type": "boolean",
            "description": "Only ads that state pay (2,452 live). This is the slice a compensation-data or salary-benchmarking buyer wants — combine it with 'Fetch job details' to get the structured min/max/currency out of the ad's own structured data.",
            "default": false
          },
          "greatPlaceToWorkOnly": {
            "title": "Great Place to Work certified employers only",
            "type": "boolean",
            "description": "Only ads from employers carrying Duunitori's Great Place to Work badge (79 live). A small, high-signal list of employers who pay for employer-brand certification.",
            "default": false
          },
          "diversityPromiseOnly": {
            "title": "Diversity-promise employers only",
            "type": "boolean",
            "description": "Only ads from employers who signed Duunitori's diversity commitment (1,697 live).",
            "default": false
          },
          "goodSummerJobOnly": {
            "title": "\"Good summer job\" ads only",
            "type": "boolean",
            "description": "Duunitori's vetted summer-job programme (5 live off-season — this is a seasonal filter and will be near-empty outside the spring hiring window).",
            "default": false
          },
          "ageGroup": {
            "title": "Age-group friendly ads",
            "enum": [
              "any",
              "15_16_years",
              "below_18",
              "students"
            ],
            "type": "string",
            "description": "Ads explicitly open to younger applicants: 15–16-year-olds (6 live), under-18s (40) or students (1,074).",
            "default": "any"
          },
          "postedWithinDays": {
            "title": "Posted within (days)",
            "minimum": 0,
            "maximum": 365,
            "type": "integer",
            "description": "Keep only ads posted within this many days. Read from the card's 'Julkaistu 21.8.' text, and re-checked against the exact posted timestamp when 'Fetch job details' is on. 0 = no limit.",
            "default": 0
          },
          "expiringWithinDays": {
            "title": "Expiring within (days)",
            "minimum": 0,
            "maximum": 365,
            "type": "integer",
            "description": "Keep only ads whose application deadline falls within this many days — the re-post/renewal trigger a staffing firm wants to call on. Requires the ad's expiry date, so switching this on turns 'Fetch job details' on automatically and those rows are billed the detail event. 0 = off.",
            "default": 0
          },
          "includeExpiredOrClosed": {
            "title": "Include ads past their deadline",
            "type": "boolean",
            "description": "Duunitori sometimes still lists an ad after its application deadline has passed. Leave on to keep them (useful for market-history analysis); switch off to keep only ads still open. Switching it OFF needs the expiry date, so it turns 'Fetch job details' on automatically.",
            "default": true
          },
          "fetchJobDetails": {
            "title": "Fetch job details",
            "type": "boolean",
            "description": "One extra request per job for the full ad: description, structured salary (min/max/currency/period), employment type, exact posted date, application deadline, the full expanded location list — and the EXTERNAL APPLY URL plus the ATS domain behind it (teamtailor.com, ats.talentadore.com, haileyhr.app, …), which tells you which recruiting software each employer runs. Measured over 28 varied ads: description/date/deadline/type 100%, external apply URL 71.4% (the other 28.6% take applications on Duunitori itself), published salary 32.1%. Billed as 'job-detail-enriched'.",
            "default": true
          },
          "fetchEmployerProfile": {
            "title": "Fetch employer profile",
            "type": "boolean",
            "description": "One extra request per UNIQUE employer (cached, so twenty ads from one staffing firm cost one fetch) for the company page: the Y-TUNNUS — the Finnish Business ID that joins this row to PRH/YTJ and every Finnish B2B database — plus the officially registered company name, the official toimiala, the employer's own website and social profiles, and their company description. Measured on 11 employer pages: 6 carried the registry block (Y-tunnus + official name + toimiala), 4 carried a website; older-style profiles have none. Needs 'Fetch job details' (the employer link lives on the ad page), so it turns that on automatically. Billed as 'employer-profile-enriched'.",
            "default": false
          },
          "maxItems": {
            "title": "Max jobs",
            "minimum": 0,
            "type": "integer",
            "description": "Hard stop on the number of jobs delivered AND billed. Counted after deduplication and after every filter, so you pay for exactly this many usable rows at most. 0 = no limit (the whole matching result set).",
            "default": 100
          },
          "maxPagesPerQuery": {
            "title": "Max pages per search",
            "minimum": 0,
            "type": "integer",
            "description": "Pages of results to read per search before moving to the next one — 20 jobs a page. The Actor already stops at the true last page (ceil(total/20); the page after it returns 404, which is end-of-results, not an error), so this is only needed to sample the top N pages of a very large search. 0 = read every page.",
            "default": 0
          },
          "deduplicateByJobId": {
            "title": "Deduplicate by job id",
            "type": "boolean",
            "description": "Keep this on. Duunitori's index churns about 33% a week, so offset pagination re-shows some ads at depth — measured 0.8% duplicates over a contiguous 12-page head band and 9.5% over a contiguous 20-page band at depth 600. With this on, a job is delivered and billed exactly once per run no matter how many of your searches matched it.",
            "default": true
          },
          "onlyNewSince": {
            "title": "Only jobs not seen in a previous run",
            "type": "boolean",
            "description": "Incremental mode for scheduled runs: remember every job id delivered, and on the next run return only ids that are new. State lives in a NAMED key-value store, so it survives between runs. With ~5,800 new ads a week, a daily or weekly schedule with this on returns just the delta.",
            "default": false
          },
          "stateKey": {
            "title": "Incremental state key",
            "type": "string",
            "description": "Which saved id list to compare against, inside the Actor's named key-value store. Use a different key per saved search so two schedules do not blank each other out.",
            "default": "seen-job-ids"
          },
          "descriptionFormat": {
            "title": "Description format",
            "enum": [
              "text",
              "html",
              "both",
              "none"
            ],
            "type": "string",
            "description": "How to return the ad body when 'Fetch job details' is on. HTML keeps the employer's formatting; text is what you feed an LLM; 'none' keeps the dataset small when you only want the structured fields.",
            "default": "text"
          },
          "descriptionMaxLength": {
            "title": "Truncate description at (characters)",
            "minimum": 0,
            "type": "integer",
            "description": "Cap the description length to keep the dataset small. Measured ad bodies run 2,500–8,400 characters. 0 = no truncation. Plain text is cut at the character limit; HTML is cut back to the last complete tag and any still-open tags are closed, so descriptionHtml stays valid markup rather than ending mid-element.",
            "default": 0
          },
          "outputFormat": {
            "title": "Output shape",
            "enum": [
              "flat",
              "nested"
            ],
            "type": "string",
            "description": "Flat gives one row per job with salaryMin/salaryMax/salaryCurrency/salaryUnit as top-level columns — the shape a spreadsheet or CSV export wants. Nested groups them under a 'salary' object for API consumers.",
            "default": "flat"
          },
          "proxyConfiguration": {
            "title": "Proxy",
            "type": "object",
            "description": "duunitori.fi is behind Cloudflare and only serves Nordic residential exits. Measured 2026-09-04: no proxy → challenge; datacenter → challenge; residential pinned to FI → 200 with real data (98.7% over 158 calls). SE, NO, DK, DE and EE also clear; GB and US are challenged, so an UNPINNED residential pool fails intermittently. If you leave the country blank the Actor pins it to FI for you.",
            "default": {
              "useApifyProxy": true,
              "apifyProxyGroups": [
                "RESIDENTIAL"
              ],
              "apifyProxyCountry": "FI"
            }
          },
          "maxConcurrency": {
            "title": "Max concurrent requests",
            "minimum": 1,
            "maximum": 10,
            "type": "integer",
            "description": "Parallel detail/employer requests. The site answers in about 0.9–1.2 s and every request costs residential bandwidth, so there is nothing to gain from hammering it — 2 is a good default and keeps the block rate at the measured 0%.",
            "default": 2
          },
          "requestRetries": {
            "title": "Retries per request",
            "minimum": 1,
            "maximum": 10,
            "type": "integer",
            "description": "Attempts per URL, each on a FRESH proxy session. About one call in eighty comes back as a residential-pool hiccup ('590 UPSTREAM504, 0 bytes'); a new session clears it. End-of-results 404s are never retried.",
            "default": 4
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}