# Finnish Company Scraper - Y-tunnus, Email & Phone + PRH (`scrapersdelight/fi-prh-website-ytunnus-contact-scraper`) Actor

Finnish B2B leads from company websites' statutory § 176 block — Y-tunnus (mod-11 checked), email, phone, address, ALV-numero, e-invoice address — verified in the PRH/YTJ register. Or discover companies in PRH by town, TOL code and form and read their sites. $9 per 1,000 rows.

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

## Pricing

$9.00 / 1,000 per finnish company row 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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Finnish Company Scraper — Y-tunnus, Email & Phone + PRH

**Turn Finnish company websites into verified B2B leads.** For each site, this Actor reads the contact and registration details Finnish companies publish on their websites, and returns them as one clean row: **company name, Y-tunnus (check digit verified), email, phone, address, ALV-numero and e-invoice address (verkkolaskuosoite)**. Each Y-tunnus is then **looked up in the PRH/YTJ register**, the official Finnish company register.

Or start from the register itself. **Discover companies in PRH by town, TOL business line, company form, post code or registration date**, and the Actor reads each company's registered website for the email and phone that PRH does not hold.

- **No login, no API key, no browser.** Plain HTTP, sent directly from Apify's network.
- **Pay only for rows you get: $9 per 1,000 rows.** Dead, blocked, parked or empty websites are never billed. Neither is a website that belongs to a different company.
- **Measured, not claimed.** Every number in this README comes from a real run on real Finnish company websites, including websites held out from development. The run IDs are in the SIGNOFF.

***

### 📚 Where the data comes from

- **Company websites:** the contact and registration block Finnish companies publish, usually in the footer, on *yhteystiedot*, on *toimitusehdot* or in the *tietosuojaseloste*.
- **PRH / YTJ register:** the Finnish company register's open data at `avoindata.prh.fi` (source: Patentti- ja rekisterihallitus, PRH). PRH holds no email and no phone number for any company — those come only from the website.

***

### 🔍 What it does

#### Mode 1: domain list (you already have websites)

For each website:

1. It fetches the homepage and reads the **footer**. On the probe, the footer was where the Y-tunnus was found most often.
2. It reads the homepage's **JSON-LD** structured data. This costs nothing extra.
3. It ranks the site's own links using **Finnish rules** and fetches the best 1–2 pages: *yhteystiedot*, *laskutustiedot*, then *toimitusehdot / tilausehdot*, then *tietosuojaseloste*, then *yritys / meistä*. Swedish and English pages are covered too.
4. It falls back to the XML sitemap and the WordPress page index, **only when the footer offered no candidate link**.
5. It merges up to 3 pages field by field. Every field records the URL of the page it came from.
6. It validates the Y-tunnus check digit and looks the number up in **PRH**.

#### Mode 2: PRH register discovery (you want new companies)

1. It queries PRH with your filters and pages through every result. It checks that the number of companies collected matches the register's own `totalResults`. A short sweep is a failure, not a smaller register.
2. It drops **housing / property companies** (on by default) and **inactive companies** (on by default).
3. It takes each remaining company's **registered website** and reads it exactly as in mode 1.
4. It **bills a row only if the company's own website gave a Y-tunnus, VAT number, email or phone.** A register row with no website, or a dead one, is never billed. With "Include unbilled miss rows" on, those rows are still delivered, **free**, with the full register block.
5. If the website's block names a **different company** (for example a property manager's website listed by a client company), the row is **not billed**. The website's Y-tunnus and the register's are both shown.

***

### 📦 What you get: every field

#### Contact (from the website)

| field | what it is |
|---|---|
| `email` | best email, preferring the company's own domain |
| `emails[]` | up to 10 emails. Platform, hosting-agency and regulator addresses (e.g. `@om.fi`, `@zoner.fi`) are removed, including their `www.`/subdomain forms |
| `emailConflict` | set when the best email is on a different domain that is not a free-mail provider (e.g. a group or agency address) |
| `phone`, `phones[]` | E.164 (`+358…`). Dates, clock times ("09.00–17.00"), Y-tunnus shapes, postcodes and regulator switchboards are rejected |
| `registeredOffice` (+ `…Postcode`, `…City`), `addressLine`, `postcode`, `city` | address. The postcode must exist in **PRH's own 4,348-code postcode table** and the town after it must be that postcode's post office |
| `socialLinks[]` | LinkedIn / Facebook / Instagram / X / YouTube / TikTok profiles |
| `officerName`, `officerRole` | a named toimitusjohtaja / yhteyshenkilö (rare, see limits) |
| `termsUrl`, `privacyPolicyUrl` | toimitusehdot / tietosuojaseloste URLs |

#### Identifiers (from the website)

| field | what it is |
|---|---|
| `businessId` | the **Y-tunnus** as printed, normalised to `NNNNNNN-C` (only separators change, no digit is ever added or altered) |
| `businessIdChecksumValid` | the mod-11 check digit (weights 7,9,10,5,8,4,2). A number printed with a wrong check digit is **shipped as printed and flagged `false`**, never "repaired" |
| `businessIdSource` | `label` / `legal-form` / `label-loose` / `bare` / `json-ld` |
| `businessIdsOnPage[]` | every checksum-valid Y-tunnus in the site's disclosure text (group sites print several) |
| `vatNumber`, `vatNumberValid` | **ALV-numero** (FI + 8 digits) as printed, with the same mod-11 check on its digits |
| `vatMatchesBusinessId` | compares the printed VAT number with the printed Y-tunnus. **Neither is ever derived from the other** |
| `eInvoiceAddress`, `eInvoiceOperator`, `eInvoiceOperatorId` | the **verkkolaskuosoite** (OVT `0037…` or IBAN), the e-invoice operator and its välittäjätunnus, exactly as printed |
| `eInvoiceAddressMatchesBusinessId` | whether the OVT address carries the company's own Y-tunnus digits |
| `kotipaikka` | the registered municipality, when printed |
| `inLiquidation`, `liquidationStatement` | the site says *selvitystila / konkurssi / yrityssaneeraus* |
| `companyName`, `registeredName`, `tradingName`, `legalForm` | name and legal form (Oy, Oyj, Ky, Ay, Osuuskunta, Tmi, ry…) |

#### Register (from PRH, joined on the Y-tunnus)

`registerBusinessId`, `registerName`, `registerAuxiliaryNames[]`, `registerCompanyForm` (+ Finnish, + type code), `registerBusinessLineCode` / `registerBusinessLine` (+ Finnish, TOL 2025), `registerRegistrationDate`, `registerBusinessIdDate`, `registerStatus`, `registerTradeRegisterStatus`, `registerSituations[]` (liquidation / bankruptcy / re-organisation), **`registerVatRegistered`**, **`registerEmployerRegistered`**, `registerPrepaymentRegistered`, `registerVisitingAddress`, `registerPostalAddress`, `registerPostCode`, `registerCity`, `registerWebsite`, `registerLastModified`, `registerRecordUrl`, `ytjUrl`, `registerSource`.

#### Cross-checks (the wedge: no rival ships these)

| field | meaning |
|---|---|
| `registerLookupStatus` | `found` / `not-in-register` / `lookup-error` / `register-row` (mode 2) |
| `businessIdMatchesRegister` | the website's Y-tunnus = the register row's |
| `registerIdPrintedOnSite` | the register's Y-tunnus appears anywhere in the site's disclosure |
| `registerNameAgrees` | the website's name and the PRH name (or an auxiliary name) refer to the same company |
| `registerWebsiteMatchesDomain` | the website PRH has on file is this domain |
| `redirectedToOtherDomain` | the site redirected to a different registrable domain |

#### Provenance

`status`, `missReason`, `mode`, `domain`, `inputUrl`, `resolvedUrl`, `disclosureUrl`, `disclosureSource`, `discoveryChannel`, `pagesParsed`, `pagesChecked`, `fieldCount`, **`fieldSources`** (the page URL for every field), `stableId`, `fetchedAt`.

***

### 📊 Measured results

#### Two sampling frames, 480 real Finnish websites (development probe)

Field-level accuracy doesn't depend on how the sites were sampled. Population rates do, so they were measured on two samples with opposite biases:

- **A: websites listed in PRH**, from random register pages across 12 municipalities. This leans toward registered companies.
- **B: the Tranco `.fi` tail** (rank 300k–1M). This leans toward busier sites, including associations and public bodies.

| | A: PRH websites | B: Tranco .fi |
|---|---|---|
| sampled | 240 | 240 |
| live | 195 | 205 |
| Y-tunnus found | **46.7% of live** | **41.0% of live** |
| email found | 80.0% | 75.1% |
| phone found | 71.3% | 62.9% |
| any contact or ID | 83.6% | 84.9% |

#### Held-out platform run: 399 domains never looked at during development

Run on the shipping build 0.1.3, 1,024 MB, default settings.

| | A: PRH websites (239) | B: Tranco .fi (159) |
|---|---|---|
| billed rows | 99 | 57 |
| **billed, % of live** | **51.6%** | **41.0%** |
| dead host / parked / blocked | 34 / 6 / 7 | 9 / 1 / 10 |

Expect **roughly 41–52% of live Finnish websites to become a billed row**, depending on your list. A list of registered companies' own sites sits at the top of that range.

**Field fill on the 156 billed rows** (delivered 156 = charged 156):

```
businessId 95.5% (checksum-valid in 148 of 149)  · email 92.3% · phone 84.6% · name 84.0%
addressLine 75.6% · registeredOffice 68.6% · e-invoice address 17.9% · ALV-numero 9.0%
kotipaikka 3.8% · officerName 0.6%
PRH register block 89.7% (140 found, 8 not in the trade register) · website name agrees with PRH 125/140
```

A found Y-tunnus that is `not-in-register` does not mean the number is fake. The PRH open-data API covers the **trade register**. Associations (ry), public bodies and some foundations have valid Y-tunnukset but are outside it. All 8 cases on the held-out run were of that kind (metsakeskus.fi, mela.fi, studyinfinland.fi, sey.fi …).

#### Register discovery, measured

| run | query | PRH rows read | with an active, usable website | billed | billed / websites read |
|---|---|---|---|---|---|
| 1 | Tampere / Oulu / Turku × TOL 43220, 62010, 69201 × OY | 702 (floor-asserted: 9/9 sweeps collected = totalResults) | 212 | **170** (+6 group-site duplicates, unbilled) | **80.2%** |
| 2 | Helsinki … × 43220, 69201, 62100, 71121 × OY, cap 300 rows | 1,572 | 525 | **300** (cap) | **71.1%** of 422 read |

On the billed register rows: **email 89.0–91.8%, phone 90.7–94.1%**, register block 100%. **60–62% of PRH companies list no website at all.** Those rows are never billed.

#### How you can tell the gaps are real, not parser misses

The offline suite runs the shipped parser over **400 real sites, captured live** from both frames: **56,495 assertions, 0 failures**.

- **Label → value:** every one of the **151 domains** whose text puts a Y-tunnus label right before a valid Y-tunnus emitted it (**151/151, 100%**). A Y-tunnus that is missing from a row is missing from the site.
- **The negative direction:** a Y-tunnus that belongs to a **third party** is never emitted as the company's. Seven sites printed only payment providers' or e-invoice operators' numbers ("Paytrail Oyj (2122839-7)", "Visma Pay, Paybyway Oy, y-tunnus 2486559-4"), and all seven were correctly refused.
- **No repair, ever:** all **12,456 single-digit mutations** of the 173 real Y-tunnukset in the corpus were fed back. **0** were "corrected" into a different number.
- **Newline weld:** two digit runs from different page elements are never joined into one Y-tunnus or phone number.

#### A real row, from the held-out run

```json
{
  "status": "ok", "mode": "domains", "domain": "sunhoiva.fi",
  "disclosureUrl": "https://www.sunhoiva.fi/yhteystiedot",
  "companyName": "SunHoiva Hoitopalvelut Oy", "legalForm": "Osakeyhtiö (Oy)",
  "businessId": "3276381-1", "businessIdChecksumValid": true, "businessIdSource": "label",
  "email": "info@sunhoiva.fi", "phone": "+358503250127",
  "registeredOffice": "33540 Tampere", "postcode": "33540", "city": "Tampere",
  "eInvoiceAddress": "003732763811", "eInvoiceAddressMatchesBusinessId": true,
  "privacyPolicyUrl": "https://www.sunhoiva.fi/tietosuojaseloste",
  "registerName": "SunHoiva Hoitopalvelut Oy", "registerCompanyForm": "Limited company",
  "registerBusinessLineCode": "78200",
  "registerBusinessLine": "Temporary employment agency activities and other human resource provisions",
  "registerStatus": "Valid", "registerTradeRegisterStatus": "Registered",
  "registerVatRegistered": true, "registerEmployerRegistered": true,
  "registerVisitingAddress": "Takojankatu 1c B 13, 33540 TAMPERE",
  "registerLookupStatus": "found", "businessIdMatchesRegister": true, "registerNameAgrees": true,
  "ytjUrl": "https://tietopalvelu.ytj.fi/yritys/3276381-1"
}
```

(Excerpt: the real row also carries `emails[]`, `phones[]`, `socialLinks[]`, `fieldSources` and the rest of the register block.)

***

### 🧰 Inputs

**Domain-list mode:** `domains[]` (bare domain, homepage URL or direct page URL; any TLD) · `startUrls` · `sourceDatasetId` + `domainFieldName` (reads `domain`/`website`/`verkkosivut`/`kotisivu`/`www` automatically) · `domainsFileUrl` (CSV/TXT/JSON/JSONL) · `verifyAgainstRegister` (default on) · `requireRegisterMatch`.

**Register mode:** `registerLocations[]` · `registerBusinessLines[]` · `registerCompanyForms[]` (OY, OYJ, KY, AY, OK, …) · `registerPostCodes[]` · `registerName` · `registerRegisteredFrom` / `…To` · `maxRegisterCompanies` (default 2,000 PRH rows read) · `excludeHousingCompanies` (default on) · `onlyActiveCompanies` (default on). Every combination of the lists is one PRH query.

> **Business-line codes are TOL 2025.** PRH moved to the new Statistics Finland classification on 2026-01-01. Measured in Tampere: `62100` (computer programming, TOL 2025) matches **813** companies, while the old TOL 2008 `62010` matches **10** that were never reclassified. A code shorter than five digits is applied as a true **prefix**, because PRH matches short codes as a substring (measured: `62` returned `86230` and `46220`).

**Both modes:** `maxItems` (billed-row cap, default 1,000) · `maxDomains` · `skipDomains[]` / `previousDatasetId` (never re-buy a company) · `requireBusinessId` / `requireContact` / `requireEmail` / `excludeLiquidation` / `minFieldsRequired` · `emailPolicy` (all / role-only / exclude-role) · `dedupeBy` (Y-tunnus, else domain) · `discoveryChannels` · `maxPagesParsed` (3) · `perDomainTimeoutSecs` (60) · `requestConcurrency` (10) · `includeMissRows` · `includeFieldSources` (on) · `flattenOutput` · `renderJsPages` (off) · proxy settings.

**Transport:** by default, requests go out **directly** with no proxy. On the held-out run, only 40 of 1,347 requests were refused. A block triggers one retry through **Apify RESIDENTIAL (Finland)**, which recovered 5 of 35. Supply your own proxy to route everything through it.

***

### 💰 Pricing

**$0.009 per row delivered ($9 per 1,000).** One pay-per-event charge, `company-contact-scraped`. There is no start fee. The row is delivered and billed in the same step, so you're never billed for a row you didn't get.

**Never billed:** dead hosts · blocked sites · parked domains · sites that publish nothing · websites that belong to another company · register rows without a usable website · duplicates · rows your own filters removed · coverage rows.

An empty `{}` input runs the built-in 10-site demo and bills 10 rows ($0.09).

***

### ❓ FAQ

**Why not just export PRH?** PRH has no email or phone number for any company. Several store actors export the register; this one adds the website contact and checks it against the register.

**Is the Y-tunnus the same as the VAT number?** The FI VAT number is "FI" plus the Y-tunnus digits without the hyphen, but a company having a Y-tunnus does not make it VAT-registered. The Actor never derives one from the other. It reports each as printed and compares them only when **both** are printed. Whether the company is VAT-registered is a separate field, `registerVatRegistered`, taken from the register.

**What is `other_company`?** Register mode found the company's website, but the website names a different company (typically a property manager, or a client of an accounting firm) and not the register company. It's unbilled and shows both Y-tunnukset. A **group** website that prints the parent's Y-tunnus but agrees on the name is billed **once per website**. The other subsidiaries that list the same site appear as unbilled duplicates.

**Sole traders (toiminimi)?** A toiminimi's Y-tunnus is a business ID, not a personal identity number, so it's delivered like any other. Its name is often the owner's name.

***

### ⚠️ Honest limits

- **Roughly half of live Finnish sites print no Y-tunnus** (41–47% of live sites do, across two frames). The 151/151 label check shows this is the sites' own compliance, not the parser.
- **`kotipaikka` fills at 1.7–3.9%** and **`officerName` at 0.6–1%.** Finnish sites rarely print either.
- **`registeredOffice` is sometimes postcode + town only** ("33540 Tampere"), when the site prints no street. The register block's `registerVisitingAddress` usually has the full address.
- **In register mode, 60–62% of PRH companies list no website**, and newly registered companies almost never do (8% of 500 Helsinki companies registered since 2026-06-01). Location-only queries are dominated by housing companies. Add a business line or company form.
- **Client-rendered sites** are not rendered by default. The Chromium rung is wired in (`renderJsPages`, needs ≥1,536 MB). On the Swedish sibling it added a few per cent more rows at 3–4× the compute per website.
- **About 14% of the websites PRH has on file no longer resolve** (34 of 239 held-out frame-A hosts). They're reported as `unreachable` and never billed.
- **`ytjUrl`** follows YTJ's web-UI route. That UI is rendered by JavaScript (every route returns the same page shell to a plain request), so the machine-verifiable link is `registerRecordUrl`, which is PRH's API record.

# Actor input Schema

## `mode` (type: `string`):

"Domain list" reads the websites you supply. "PRH register discovery" finds companies in the Finnish PRH/YTJ register by the filters below, takes each company's registered website, and reads that. "Auto" picks register discovery when you give register filters and no domains, and the domain list otherwise.

## `domains` (type: `array`):

One entry per company: a bare domain ("nyron.fi"), a homepage URL ("https://www.marlea.fi") or a direct page URL ("https://x.fi/yhteystiedot"). Any TLD is accepted (.fi, .com, .net, .eu …). Leave empty — together with the register filters — to run the built-in 10-site Finnish demo batch.

## `registerLocations` (type: `array`):

PRH register discovery. Towns as PRH stores them, e.g. "Tampere", "Helsinki", "Oulu" (case does not matter). Each value is one register query; combined with the other register filters as every combination.

## `registerBusinessLines` (type: `array`):

Main business line as a Statistics Finland TOL code or a Finnish word ("ohjelmisto"). PRH moved to TOL 2025 on 2026-01-01 (typeCodeSet TOIMI4): MEASURED in Tampere, the TOL 2025 code "62100" (computer programming) matches 813 companies while the old TOL 2008 "62010" matches 10 unreclassified ones. "43220" (plumbing, heating and air-conditioning installation) and "69201" (accounting) exist in both. A code shorter than five digits is re-applied as a true PREFIX, because PRH matches it as a substring (measured: "62" returned 86230 and 46220).

## `registerCompanyForms` (type: `array`):

PRH company-form codes. OY = limited company, OYJ = public limited company, KY = limited partnership, AY = general partnership, OK = co-operative. Leave empty for all forms.

## `registerPostCodes` (type: `array`):

Five-digit Finnish post codes (visiting or postal address), e.g. "00100".

## `registerName` (type: `string`):

PRH name search (current, previous, parallel and auxiliary names).

## `registerRegisteredFrom` (type: `string`):

Company registration date from (YYYY-MM-DD). Measured: newly registered companies rarely list a website yet (8% of 500 Helsinki companies registered since 2026-06-01).

## `registerRegisteredTo` (type: `string`):

Company registration date to (YYYY-MM-DD).

## `maxRegisterCompanies` (type: `integer`):

Cap on PRH register rows read (100 per register request). Only active, non-housing companies WITH a registered website go on to a website read. Register rows are never billed.

## `excludeHousingCompanies` (type: `boolean`):

Skip asunto-osakeyhtiöt, kiinteistö osakeyhtiöt and similar. MEASURED: they register their PROPERTY MANAGER's website as their own, so the site's statutory block names another company; and the first 500 Rovaniemi register rows held 367 of them.

## `onlyActiveCompanies` (type: `boolean`):

Skip register companies whose Business ID is not Valid, that are removed from or ceased in the trade register, or that are in liquidation, bankruptcy or re-organisation.

## `verifyAgainstRegister` (type: `boolean`):

For every checksum-valid Y-tunnus read off a website, fetch its PRH record and add the register block: registered name, company form, TOL business line, status, VAT / employer registration, addresses — plus whether the website's name agrees with the register. Free and on by default; the register's own values never overwrite what the website printed.

## `requireRegisterMatch` (type: `boolean`):

Domain-list mode: deliver (and bill) only rows whose Y-tunnus PRH returns and whose website name does not disagree with the register name.

## `startUrls` (type: `array`):

The same domain list handed over as URLs, so Make, Zapier, Clay or a Google Sheet can pass it natively. Merged with the domain list.

## `sourceDatasetId` (type: `string`):

Dataset ID of a previous Actor run. Each item's domain / website / verkkosivut column is read and enriched.

## `domainFieldName` (type: `string`):

Which field of the source dataset (or CSV column) holds the website. Leave empty to auto-detect domain / website / url / verkkosivut / kotisivu / www.

## `domainsFileUrl` (type: `string`):

URL of a CSV, TXT, JSON or JSONL file holding the websites.

## `skipDomains` (type: `array`):

Domains (or Y-tunnukset) to skip outright — existing customers, competitors, do-not-contact entries.

## `previousDatasetId` (type: `string`):

Dataset ID of an earlier run of THIS Actor. Its stableId / businessId / domain values become a suppression list, so a re-run never re-delivers — and never re-charges you for — a company you already bought.

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

Hard cap on rows DELIVERED AND BILLED this run. 0 = unlimited.

## `maxDomains` (type: `integer`):

Cap on websites ATTEMPTED (either mode), applied before any website request. 0 = no cap.

## `maxDiscoveryRequestsPerDomain` (type: `integer`):

Hard cap on requests per website after the homepage.

## `maxPagesParsed` (type: `integer`):

How many pages may be parsed and merged for one website (homepage included). The Y-tunnus sits in the footer, on yhteystiedot, on toimitusehdot or in the tietosuojaseloste; 3 covers the measured spread.

## `perDomainTimeoutSecs` (type: `integer`):

Wall-clock deadline for one website.

## `requestConcurrency` (type: `integer`):

Websites worked in parallel. Lowered automatically, with a warning, if the run's memory cannot support it (~70 MB per parallel website).

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

Timeout for a single HTTP request.

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

Retries per request on a transient failure.

## `discoveryChannels` (type: `array`):

How each website's statutory block is located. Ranked links use FINNISH scoring — yhteystiedot, toimitusehdot / tilausehdot, tietosuojaseloste, yritys / meistä — plus Swedish and English. The sitemap and WordPress index only run when the footer offered no candidate link.

## `renderJsPages` (type: `boolean`):

A real Chromium for websites where plain HTTP found no statutory block and the homepage looks client-rendered. OFF by default (measured on the Swedish sibling at 3-4x the compute per website for a few per cent more rows). Needs at least 1536 MB of memory; below that it is disabled rather than risk an out-of-memory failure.

## `browserConcurrency` (type: `integer`):

How many Chromium contexts may render at once.

## `browserWaitMs` (type: `integer`):

Wait after DOM load (plus a scroll that mounts lazy footers) before reading a rendered page.

## `followWwwAndRootVariants` (type: `boolean`):

If the homepage fails, retry the other host form (www.x.fi <-> x.fi) before declaring the website unreachable.

## `deepJsDiscovery` (type: `boolean`):

Search the page's inline JSON and JavaScript bundles for a legal-page URL (up to 6 extra requests per website, browser-free).

## `respectRobotsTxt` (type: `boolean`):

Fetch and honour each site's robots.txt before requesting anything. Off by default.

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

Default: no proxy — requests go out directly from Apify's network (measured: 91% of live Finnish company sites answer that way), with one RESIDENTIAL (Finland) retry only when a site actually blocks. Supply your own proxy here to route every request through it.

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

Force EVERY request through Apify RESIDENTIAL exit nodes in this country (metered per byte). Leave on None for direct requests with residential only as an escalation.

## `escalateToResidentialOnBlock` (type: `boolean`):

On a 403 / challenge (never on a dead host or a 404), retry the request once through Apify RESIDENTIAL + country FI.

## `escalateToUnblockerOnBlock` (type: `boolean`):

A second escalation for the Cloudflare-managed tail. Off by default because Unblocker requests are billed to your Apify account on top of the row price.

## `customUserAgent` (type: `string`):

Override the browser User-Agent sent on every request.

## `extraHttpHeaders` (type: `object`):

Additional request headers, merged over the defaults.

## `tldFilterMode` (type: `string`):

No TLD filter by default: Finnish companies trade on .com, .net and .eu as well as .fi.

## `tldFilter` (type: `array`):

The TLDs the mode above applies to, without the dot: fi, com, net.

## `requireBusinessId` (type: `boolean`):

Deliver (and bill) only rows carrying a Y-tunnus (from the website, or from the register in register mode).

## `requireContact` (type: `boolean`):

Deliver (and bill) only rows whose website gave an email or a phone number.

## `requireEmail` (type: `boolean`):

Deliver (and bill) only rows whose website gave an email address.

## `excludeLiquidation` (type: `boolean`):

Drop rows whose website states selvitystila / konkurssi / yrityssaneeraus, or whose PRH record carries one of those situations.

## `minFieldsRequired` (type: `integer`):

Quality floor: a row must carry at least this many of the 16 website value fields to be delivered and billed. 0 = no floor.

## `emailPolicy` (type: `string`):

"Role only" keeps info@ / myynti@ / asiakaspalvelu@ style mailboxes; "Exclude role" keeps only named mailboxes.

## `dedupeBy` (type: `string`):

Which key collapses duplicates BEFORE anything is pushed or charged. Default: the Y-tunnus when there is a checksum-valid one (the register's in register mode), the registrable domain otherwise.

## `validateTaxId` (type: `boolean`):

Run the Y-tunnus mod-11 check digit (weights 7,9,10,5,8,4,2) on every Y-tunnus and on the digits of every FI VAT number, and emit the verdict.

## `extractOfficer` (type: `boolean`):

A toimitusjohtaja / yhteyshenkilö / omistaja named next to a label on the website.

## `extractSocials` (type: `boolean`):

LinkedIn, Facebook, Instagram, X, YouTube and TikTok links on the pages read.

## `extractPolicyUrls` (type: `boolean`):

The site's toimitusehdot and tietosuojaseloste URLs.

## `includeMissRows` (type: `boolean`):

Emit a row for every website / register company that produced no billed lead — dead, blocked, parked, publishes nothing, belongs to another company, register row without a website, filtered, duplicate — with status and missReason. These rows are NEVER charged; in register mode they still carry the full PRH register block.

## `includeFieldSources` (type: `boolean`):

A fieldSources object naming the exact page URL each field was read from.

## `flattenOutput` (type: `boolean`):

On: one flat row for Sheets / Clay / CSV. Off: fields grouped into company, identifiers, contact, eInvoicing, register and meta objects.

## Actor input object example

```json
{
  "mode": "auto",
  "domains": [
    "nyron.fi",
    "marlea.fi",
    "tapahtumaset.fi",
    "cleanor.net",
    "basecatering.fi",
    "porinelainlaakaripalvelu.fi",
    "vuokravarastot.fi",
    "savunpoistokumppani.fi",
    "pego.fi",
    "jouninklapi.fi"
  ],
  "maxRegisterCompanies": 2000,
  "excludeHousingCompanies": true,
  "onlyActiveCompanies": true,
  "verifyAgainstRegister": true,
  "requireRegisterMatch": false,
  "maxItems": 1000,
  "maxDomains": 0,
  "maxDiscoveryRequestsPerDomain": 8,
  "maxPagesParsed": 3,
  "perDomainTimeoutSecs": 60,
  "requestConcurrency": 10,
  "requestTimeoutSecs": 25,
  "maxRequestRetries": 2,
  "discoveryChannels": [
    "homepage",
    "anchor",
    "sitemap",
    "wpJson"
  ],
  "renderJsPages": false,
  "browserConcurrency": 2,
  "browserWaitMs": 2500,
  "followWwwAndRootVariants": true,
  "deepJsDiscovery": false,
  "respectRobotsTxt": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "proxyCountry": "none",
  "escalateToResidentialOnBlock": true,
  "escalateToUnblockerOnBlock": false,
  "tldFilterMode": "none",
  "requireBusinessId": false,
  "requireContact": false,
  "requireEmail": false,
  "excludeLiquidation": false,
  "minFieldsRequired": 0,
  "emailPolicy": "all",
  "dedupeBy": "ytunnus-then-domain",
  "validateTaxId": true,
  "extractOfficer": true,
  "extractSocials": true,
  "extractPolicyUrls": true,
  "includeMissRows": false,
  "includeFieldSources": true,
  "flattenOutput": true
}
```

# Actor output Schema

## `records` (type: `string`):

The dataset: one row per Finnish company whose website yielded its statutory § 176 block (domain-list mode) or a contact for a PRH register company (register mode), with the PRH register block joined.

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

Coverage accounting: websites attempted, rows billed, the PRH sweep evidence (totalResults vs collected per query), register verification counts, and every kind of nothing kept apart — none of them charged.

# 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 = {
    "domains": [
        "nyron.fi",
        "marlea.fi",
        "tapahtumaset.fi",
        "cleanor.net",
        "basecatering.fi",
        "porinelainlaakaripalvelu.fi",
        "vuokravarastot.fi",
        "savunpoistokumppani.fi",
        "pego.fi",
        "jouninklapi.fi"
    ],
    "maxItems": 1000,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/fi-prh-website-ytunnus-contact-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 = {
    "domains": [
        "nyron.fi",
        "marlea.fi",
        "tapahtumaset.fi",
        "cleanor.net",
        "basecatering.fi",
        "porinelainlaakaripalvelu.fi",
        "vuokravarastot.fi",
        "savunpoistokumppani.fi",
        "pego.fi",
        "jouninklapi.fi",
    ],
    "maxItems": 1000,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/fi-prh-website-ytunnus-contact-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 '{
  "domains": [
    "nyron.fi",
    "marlea.fi",
    "tapahtumaset.fi",
    "cleanor.net",
    "basecatering.fi",
    "porinelainlaakaripalvelu.fi",
    "vuokravarastot.fi",
    "savunpoistokumppani.fi",
    "pego.fi",
    "jouninklapi.fi"
  ],
  "maxItems": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call scrapersdelight/fi-prh-website-ytunnus-contact-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapersdelight/fi-prh-website-ytunnus-contact-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/ygrO0EwJ4oQgM2kyE/builds/24mlaBTXhBIjg1Koe/openapi.json
