# Sentral Godkjenning Scraper - Norway Approved Contractors (`scrapersdelight/sentral-godkjenning-no-scraper`) Actor

From $3 per 1,000 rows. Scrape Norway's Sentral godkjenning register (DiBK, sgregister.dibk.no): all 10,408 centrally approved building enterprises with approval areas, tiltaksklasse, expiry date, insurance flags and address. Optional detail adds employee count. Approval-change monitor included.

- **URL**: https://apify.com/scrapersdelight/sentral-godkjenning-no-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

from $3.00 / 1,000 per firm 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

## 🇳🇴 Sentral Godkjenning Scraper — every centrally approved Norwegian building enterprise

**Turn Norway's Sentral godkjenning register (`sgregister.dibk.no`) into a spreadsheet.**
All **10,408** centrally approved building enterprises, each with its approval areas, tiltaksklasse,
approval expiry date, insurance flags and business address — plus an **approval-change monitor** that
tells you who was newly approved, who renewed, and who quietly dropped out.

Every number on this page was measured against the live register on **2026-09-08**, not estimated.

> **Read this before you buy:** the register carries **no phone number and no e-mail address**.
> Both keys exist in DiBK's own data and are `null` on **all 10,408 rows** — we emit the columns so
> nobody has to guess. What you get instead is the **organisation number** on 100% of rows (which
> joins straight to Brønnøysund/Brreg and to any Norwegian enrichment source you already use), a
> **website on 17.8%**, the **postal address on 99.8%**, and a **dated approval-expiry** on 100%.

***

### 🧭 What does Sentral Godkjenning Scraper do?

Sentral godkjenning ("central approval") is the Norwegian Building Authority's national quality
scheme for the construction industry. A firm that holds it has been vetted by **Direktoratet for
byggkvalitet (DiBK)** for specific *functions* (designing, executing, applying, inspecting) in
specific *subject areas* (carpentry, groundworks, fire safety, sanitary, architecture …) at a
specific *tiltaksklasse* (project-complexity tier 1–3). Approval runs for three years.

This Actor reads the whole register in one request and gives you:

| | |
|---|---|
| 🏗️ **10,408 approved enterprises** | The complete register, not a sample |
| 🔢 **35,726 approval-area rows** | Average 3.4 per firm, maximum 30 |
| 🏅 **Tiltaksklasse 1 / 2 / 3** | 5,258 · 3,157 · **1,993** — the big-project shortlist |
| 📅 **Approval expiry on every row** | 509 expire within 90 days, 2,719 within a year |
| 🧰 **4 functions × 36 subject areas** | Utførende 9,070 firms · Prosjekterende 4,705 · Ansvarlig søker 2,898 · Uavhengig kontrollerende 690 |
| 📍 **1,209 postal towns** | Oslo 1,070 · Trondheim 205 · Stavanger 159 · Sandnes 152 · Kristiansand S 151 · Bergen 125 |
| 🛡️ **Insurance + apprentice flags** | Liability 43.1% · industrial-injury 41.8% · approved training enterprise 43.7% |
| 🌐 **Website** | 1,854 firms (17.8%) |
| 👷 **Employee count** *(optional)* | Only on the detail page — never in the API |
| 🔔 **Approval-change monitor** | New / renewed / shortened / areas changed / lapsed |

***

### 🥇 The two things you cannot get anywhere else

#### 1. Employee count and the approval **start** date

DiBK's API returns the approval *end* date but not the start, and it does not return company size at
all. Both sit on the firm's own register page as **`Antall lønnsmottakere`** and
**`Godkjenningsperiode`**. Turn on **"Fetch employee count + approval start date"** and the Actor
reads them.

Measured on a 20-firm random sample fetched through an Apify datacenter proxy:
**20/20 pages returned HTTP 200** (average 23,381 bytes, 639 ms), **20/20 carried the employee
count**, **20/20 carried the approval period**, and the period's end date agreed with the API's
`approval_period_to` on **20/20** — which is what proves the parse is aligned to the right firm.

Employee count is sometimes a **band** (`"1-4"`) rather than an integer. We keep the published value
verbatim in `employee_count_raw` and put the range in `employee_count_min` / `employee_count_max`.
It is never silently rounded into a single number.

#### 2. The register has no memory — so the monitor *is* the archive

**0 of 10,408 rows carry a past expiry date.** The earliest is 2026-09-08 — tomorrow. DiBK publishes
no historical endpoint and no lapsed-approvals endpoint: a firm is simply **gone** from the register
the day its approval ends, with no record that it was ever there.

So a dated snapshot plus a diff is the only way anyone can observe an approval being **granted** or
**lost**. Run this Actor in **Monitor** mode on a schedule and it keeps that series for you in a
named key-value store, with one history line per run. Every run makes the archive more valuable, and
it is not something a competitor can backfill.

***

### ⚡ Quick start

Press **Start**. The Actor ships prefilled with `mode: register`, `maxFirms: 50` and enrichment off —
50 rows, about **7 seconds**, **$0.15**. Then narrow it:

```jsonc
// The big-project key-account list in Oslo, soonest renewals first
{
  "postalTowns": ["Oslo"],
  "minTiltaksklasse": 3,
  "sortBy": "approval_expiry",
  "maxFirms": 0
}                              // → 387 firms, ~4 s, $1.16
```

```jsonc
// Carpentry firms whose approval lapses inside 120 days — a dated renewal trigger
{
  "subjectAreas": ["Tømrerarbeid og montering av trekonstruksjoner"],
  "expiringWithinDays": 120,
  "sortBy": "approval_expiry",
  "maxFirms": 0
}                              // → 252 firms, ~4 s, $0.76
```

```jsonc
// "Does this contractor actually hold central approval?" — no extra requests
{
  "organizationalNumbers": ["811600792", "933697991", "999999999"],
  "maxFirms": 0
}                              // → 3 rows; the third comes back approved:false, in_register:false
```

```jsonc
// Firm size for a shortlist: filter first, THEN enrich the slice
{
  "postalTowns": ["Bergen"],
  "minTiltaksklasse": 2,
  "enrichWithDetails": true,
  "maxFirms": 0
}
```

```jsonc
// Daily watch — schedule this and read the change rows
{ "mode": "monitor", "monitorProfile": "norway-daily" }
```

***

### 📊 What you get — measured field fill over all 10,408 rows

| Field | Fill | Notes |
|---|---|---|
| `organizational_number` | **100%** (10,408) | 9 digits, joins to Brreg. No duplicates in the register. |
| `name` | **100%** | |
| `approved` | **100% true** | The register lists currently-valid approvals only. |
| `approval_period_to` | **100%** | ISO date. Range 2026-09-08 → 2029-09-07. |
| `days_until_expiry` · `renewal_window` | **100%** (derived) | `expired` / `0-30_days` / `31-90_days` / `91-180_days` / `181-365_days` / `over_365_days` |
| `approval_certificate_url` | **100%** | DiBK's official PDF certificate. |
| `business_country` | **100%** | NO 10,387 · SE 4 · DK 4 · FI 4 · LT 4 · PL 2 · LV 2 · HU 1 |
| `business_postal_code` · `business_postal_town` | **99.8%** (10,388) | |
| `business_address_line_1` | **98.8%** (10,288) | Plus `business_address_formatted` for CRM import. |
| `valid_approval_areas[]` | **100%** | 35,726 entries; function, subject area, DiBK codes, tiltaksklasse, planning-act version. |
| `approval_area_count` · `max_tiltaksklasse` | **100%** (derived) | 1: 5,258 · 2: 3,157 · 3: 1,993 |
| `functions[]` · `subject_areas[]` (+ codes) | **100%** (derived) | Distinct values per firm; `approval_areas_text` is the flat CSV-friendly version. |
| `educational_enterprise_approved` | 43.7% true | Approved to take apprentices (lærebedrift). |
| `liability_insurance` | 43.1% true | |
| `industrial_injury_insurance` | 41.8% true | |
| `www` | **17.8%** (1,854) | Say it plainly: fewer than one in five. |
| `postal_address_line_1` | 14.8% (1,542) | Separate postal address when it differs. |
| `likely_sole_proprietorship` · `company_form_suffix` | 7.8% flagged (816) | Heuristic — see honest limits. |
| `phone` | **0%** (0 of 10,408) | The register carries none. The column exists and is null. |
| `email` | **0%** (0 of 10,408) | Same. |
| `employee_count_raw` / `_min` / `_max` | detail only | 20/20 on the sampled pages. Sometimes a band. |
| `approval_period_from` | detail only | 20/20 on the sampled pages. |
| `data_source` · `data_licence` | **100%** | CC BY 4.0 attribution, on every row. |

Monitor mode adds `change_type`, `previous_approval_period_to`, `previous_approval_area_count`,
`previous_max_tiltaksklasse` and `change_detected_at`.

<details>
<summary><b>One real row (trimmed)</b></summary>

```json
{
  "organizational_number": "811600792",
  "name": "ELTERA AS",
  "company_form_suffix": "AS",
  "likely_sole_proprietorship": false,
  "www": "www.eltera.no",
  "phone": null,
  "email": null,
  "approved": true,
  "in_register": true,
  "approval_period_from": null,
  "approval_period_to": "2028-11-11",
  "days_until_expiry": 795,
  "renewal_window": "over_365_days",
  "approval_certificate_url": "https://sgregister.dibk.no/enterprises/811600792/approval_certificate",
  "business_address_formatted": "Haslevollen 3, 0579 OSLO",
  "business_postal_code": "0579",
  "business_postal_town": "OSLO",
  "business_country": "NO",
  "liability_insurance": false,
  "industrial_injury_insurance": false,
  "educational_enterprise_approved": true,
  "approval_area_count": 4,
  "max_tiltaksklasse": 3,
  "functions": ["Utførende", "Prosjekterende"],
  "function_codes": ["UTF", "PRO"],
  "subject_areas": ["Installasjon av brannalarmanlegg", "Brannalarmanlegg", "Ledesystem", "Installasjon av ledesystem"],
  "valid_approval_areas": [
    { "function": "Utførende", "function_code": "UTF",
      "subject_area": "Installasjon av brannalarmanlegg", "subject_area_code": "UBRALM",
      "grade": 3, "pbl": "PBL 2016", "pbl_code": "pbl2016" }
  ],
  "source_url": "https://sgregister.dibk.no/enterprises/811600792",
  "register_snapshot_date": "2026-09-08",
  "data_source": "Datakilde: Direktoratet for byggkvalitet (hentet 2026-09-08)",
  "data_licence": "CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/deed.no"
}
```

</details>

***

### 🎛️ Filters — all of them run over the **complete** register

DiBK's API **ignores every query parameter**. Verified live on 2026-09-08: `?searchString=…`,
`?organizationalNumber=…`, `?page=2&per_page=10` and `?limit=5` each returned the *identical*
582,138-byte body, and DiBK's own OpenAPI document defines no query parameters at all.

That sounds like a limitation. It is actually the best thing about this Actor's honesty: because the
whole register is downloaded every time, **a filter is never applied to a truncated page**. When the
run says *"387 firms matched"*, that is 387 out of all 10,408 — never 387 out of the first page it
happened to fetch. Sorting works the same way: the sort runs over every match and *only then* is
your row cap applied, so "the 50 soonest renewals" really are the 50 soonest in Norway.

| Filter | What it does |
|---|---|
| Company-name search | Case- and diacritic-insensitive substring; "tomrer" finds "TØMRER" |
| Organisation numbers | Compliance lookup; not-found numbers come back as `approved: false` rows |
| Functions | Utførende / Prosjekterende / Ansvarlig søker / Uavhengig kontrollerende |
| Subject areas | All 36, each labelled with its real firm count. Or pass DiBK's 46 short codes. |
| Minimum tiltaksklasse | 1–3; `3` gives the 1,993-firm key-account list |
| Minimum approval areas | Breadth-of-capability proxy |
| Expiring within N days / before / after | The dated renewal trigger |
| Postal towns · postal-code prefixes · country | "tromso" matches all 64 TROMSØ firms |
| Insurance / apprentice-approval flags | |
| Only firms with a website | Cuts to the 1,854 that publish one |
| Exclude probable sole proprietorships | Drops the 816 personal-name rows |

**Filtered-out rows are never billed.** Filtering happens before delivery, so you pay only for rows
that land in your dataset.

***

### 🔔 Monitor mode

```jsonc
{ "mode": "monitor", "monitorProfile": "norway-daily" }
```

1. **First run** records the baseline: 10,408 firms into the named key-value store
   `sentral-godkjenning-monitor-state`. It returns **no rows** and is **not charged** as a monitor run.
2. **Every run after that** re-reads the register and delivers one row per change:

| `change_type` | Meaning |
|---|---|
| `new_approval` | The firm was not in the register at the previous snapshot |
| `renewed` | The approval end date moved **later** |
| `approval_shortened` | The approval end date moved **earlier** |
| `areas_changed` | Approval areas, their codes or the tiltaksklasse changed |
| `lapsed` | The firm has dropped **out** of the register entirely |

The snapshot always covers the **whole** register even when your filters are narrow — otherwise
"lapsed" would silently mean "stopped matching your filter". Filters are applied to the change rows
you receive, and the log always reports the register-wide change counts too.

**Two rules that stop a monitor from quietly losing an event — both worth reading before you schedule it:**

- **A `lapsed` firm is gone from DiBK, so only what the snapshot recorded can be filtered on:**
  organisation number, name, postal town, approval end date, tiltaksklasse and approval-area count.
  Any *other* active filter — website, insurance, function, subject area, country, postcode,
  expiring-within, exclude-sole-proprietorships — cannot be evaluated for a firm that no longer
  exists in the register. When one of those is on, lapsed rows are **withheld and not charged**
  (never delivered-and-billed on a guess), the log names the filter that blocked them, and their
  snapshot entries are **kept**, so a later run without that filter still reports the lapse.
- **A change your `maxFirms` cap or your `maxTotalChargeUsd` did not deliver is held, not lost.**
  Those firms keep their *previous* snapshot entry, so the **next run reports them again**. The
  status message says how many are held. (Changes your own `changeTypes`/filters excluded are
  deliberate and do advance.)

`monitorProfile` lets one account keep several independent watchlists, each with its own snapshot and
its own run history (default: a year of daily runs).

> **Verified, not asserted.** The store is *named*, so it survives between runs. Proven on the
> platform: a baseline run returned 0 rows and 0 monitor-run charges, and the next run read the
> snapshot back and reported `No approval changes` with exactly one `monitor-run-completed` charge.
> Proven locally with mutated state: three snapshot entries changed and two phantom firms injected,
> and the next run classified all five cases correctly — 1 `new_approval`, 1 `renewed`,
> 1 `approval_shortened`, 1 `areas_changed`, 2 `lapsed` — and wrote them to the history record.

***

### 💵 Pricing

Pay per event. No monthly rental, no platform-usage surcharge, no charge for starting a run.

| Event | Price | Fires when |
|---|---|---|
| **Per firm returned** | **$3.00 / 1,000** ($0.003) | A firm lands in your dataset |
| **Per detail fetch** | **$3.00 / 1,000** ($0.003) | A detail page actually returned the employee count / start date |
| **Per scheduled run** | **$50 / 1,000** ($0.05) | One monitoring run (never the baseline run) |
| **Per approval change** | **$20 / 1,000** ($0.02) | One real difference against your previous snapshot |

**Worked examples**

| Job | Rows | Cost |
|---|---|---|
| Press Start (the 50-row prefill) | 50 | **$0.15** |
| Oslo, tiltaksklasse 3, by expiry | 387 | **$1.16** |
| Carpentry expiring within 120 days | 252 | **$0.76** |
| Everything with a website + tiltaksklasse ≥ 2 | 1,318 | **$3.95** |
| The entire register | 10,408 | **$31.22** |
| The entire register + employee count on every firm | 10,408 | **$62.45** |
| Daily monitor, 30 runs + ~25 changes/day | — | **≈ $16.50 / month** |

**Billing integrity, in plain terms**

- Rows are written and charged **atomically**. If you set `maxTotalChargeUsd` (or hit a free-tier
  balance), the run stops at your cap — it never hands you rows you were not billed for, and never
  bills you for rows it did not hand you.
- Detail enrichment is charged only when the page **returned the fields**. A not-found page, an
  HTTP 429 or a timeout costs nothing.
- When enrichment is on, the run reserves budget for **row + detail together** before starting a
  batch, so the enrichment can never eat the cap and leave you with three rows.
- Both of Apify's auto-events are removed, including `apify-default-dataset-item` — keeping it would
  double-charge against the per-firm event.
- A run truncated by your cap says so in the status message and **names the cap as the reason** —
  and everything that message says about the delivered rows is counted over the rows you actually
  received, not over the pre-cap set.
- In Monitor mode, a change a cap or a charging limit withheld is **held for the next run**, and a
  lapsed firm whose match cannot be proven is **withheld rather than billed**. See the two rules in
  the Monitor section.

*Measured on the live Actor, 2026-09-08:* the 50-row prefill charged exactly `firm-scraped: 50`
across three runs; an 8-row enriched run charged `firm-scraped: 8` + `firm-detail-enriched: 8`; the
whole-register run charged exactly `firm-scraped: 10,408`. With `maxTotalChargeUsd` set to **$0.06**
and enrichment on, the run delivered **10 rows and charged 10 + 10 events = $0.06 exactly** — the
money-based reservation at work, since sizing that batch on the row price alone would have opened 20
rows and then run out of budget mid-enrichment.

***

### ⚙️ How it works, and what it costs to run

- **One HTTP request** for the whole register — no browser, no login, no API key, no CAPTCHA.
- The body is **20,021,162 bytes** of JSON but only **582,138 bytes on the wire**, because the Actor
  negotiates **brotli** — a 34× saving that makes proxy traffic a rounding error (~0.6 MB/run)
  rather than the main line on your bill.
- That 20 MB is **stream-parsed one record at a time** through an incremental splitter, never
  buffered and `JSON.parse`d whole. Peak memory on the platform for the worst case — all 10,408 rows
  retained, sorted and delivered — measured **159 / 164 / 187 / 229 MB across four runs of the
  512 MB container** (216 MB in a local run with no container limit). The high end of that spread,
  not the low end, is why this Actor declares **512 MB** and not 256.
- Measured on the platform across repeated runs: a 50-row run takes **3.4–8.4 s**; the whole
  10,408-row register takes **19–33 s** end to end, including delivery.
- **What a full run costs you in Apify platform usage — the honest number:** **$0.053**, of which
  **$0.052 is Apify's own dataset-write charge** for 10,408 items and only **$0.0008–0.0014 is
  compute**. Platform usage is separate from this Actor's per-row price; both appear on your bill.
- No anti-bot of any kind. Apify's datacenter proxies returned HTTP 200 on 5 of 5 register fetches
  and 20 of 20 detail pages. Residential proxies are unnecessary here and would only add cost.
- The parser **asserts** on DiBK's response envelope and on the per-record keys. If the register's
  shape ever changes, the run fails loudly and charges nothing rather than delivering a mis-mapped
  dataset.

***

### ⚠️ Honest limits — please read before you buy

1. **No phone. No e-mail.** 0 of 10,408 rows, on both. This is a firm-qualification and
   renewal-timing dataset, not a dialable call list. Join on `organizational_number` if you need
   contact data.
2. **Website on 17.8% only.** 1,854 of 10,408.
3. **No county (fylke) field.** DiBK publishes postal code, postal town and country — nothing else
   geographic. This Actor does **not** invent a county from postcode ranges; use `postalTowns` or
   `postalCodePrefixes`.
4. **Currently-valid approvals only.** The register is a snapshot of who holds approval *today*.
   Every lapsed approval has already vanished. Monitor mode exists precisely because of this — but
   it can only see changes from the day you start running it.
5. **`likely_sole_proprietorship` is a heuristic.** It flags the 816 names carrying no company-form
   suffix (AS, ASA, ANS, DA, NUF, KOMMUNE …). A trade name such as "TARALDSEN ELEKTRO" is also
   flagged and is not a person. `company_form_suffix` shows the raw evidence.
6. **Personal data.** Norwegian sole proprietorships are listed under the individual owner's own
   name, and their registered business address is frequently a home address. This is a statutory
   public register, but those rows are personal data about natural persons. Use
   `excludeSoleProprietorships` if that matters for your use case.
7. **Detail enrichment is deliberately slow.** DiBK publishes a limit of **300 requests per
   5 minutes per IP** (= 1 request/second) for every endpoint and answers HTTP 429 above it. The
   default rate is exactly that published number, from a single pinned proxy session. So: 400 firms
   ≈ 7 minutes, 2,000 ≈ 33 minutes, all 10,408 ≈ 2.9 hours. The right workflow is **filter first,
   then enrich the slice.** `rotateDetailProxySessions` will spread the load over 20 sessions if you
   want it faster — that is your call, and it is labelled for what it is.
8. **Rate-limited or failed detail fetches are never billed**, and the row still arrives with
   `detail_complete: false` and `detail_skipped_reason` naming the cause.

***

### 📜 Data source, licence and site policy

Data is published by **Direktoratet for byggkvalitet (DiBK)** — the Norwegian Building Authority —
at `sgregister.dibk.no`, openly and without registration.

**Licence: [Creative Commons Navngivelse 4.0 Internasjonal (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.no).**
Commercial reuse is permitted. **Attribution is a condition, not a courtesy**, and DiBK specifies the
form. Every single row this Actor returns therefore carries:

```
"data_source": "Datakilde: Direktoratet for byggkvalitet (hentet 2026-09-08)",
"data_licence": "CC BY 4.0 — https://creativecommons.org/licenses/by/4.0/deed.no"
```

Keep that attribution on anything you publish from this data.

**`robots.txt`**: `https://sgregister.dibk.no/robots.txt` is 99 bytes — a single comment line
pointing at robotstxt.org documentation, with **zero rules**. There is no `Disallow` here.

**What DiBK asks, verbatim from its own API documentation** (our translation follows):

> «APIet er åpent og gratis tilgjengelig for alle som ønsker å bruke det. Systemeiere trenger ikke å
> registrere sin bruk hos Direktoratet for byggkvalitet. **Vi oppfordrer dog brukere til å ikke lagre
> data fra registeret permanent, men heller bruke APIet til direkte eller regelmessige oppslag.**»

*"The API is open and freely available to anyone who wishes to use it. System owners do not need to
register their use with DiBK. We do, however, **encourage users not to store data from the register
permanently, but rather to use the API for direct or regular lookups**."*

We quote that in full because this Actor writes register data into a dataset you keep, and Monitor
mode deliberately builds a dated archive. It is an **encouragement** ("oppfordrer"), not a licence
condition, and it does not override CC BY 4.0 — but you should know it is there and decide for
yourself. If direct lookups suit you better, run the Actor with `organizationalNumbers` against a
short list: it costs one request either way and returns a live answer.

This Actor sends one request per run for the register itself, negotiates compression so it moves
0.6 MB instead of 20 MB, and throttles optional detail fetches to DiBK's own published rate.

***

### ❓ FAQ

**What exactly is "sentral godkjenning"?**
A national approval scheme run by DiBK. A firm holding it has documented the education, practice and
quality-system requirements for named functions and subject areas at a given tiltaksklasse. It is
voluntary but widely used as a qualification filter by developers and public buyers.

**What is a "tiltaksklasse"?**
The complexity tier of the work a firm is approved for: 1 (simple) to 3 (the most demanding). A firm
can hold different classes in different subject areas; `max_tiltaksklasse` is its highest. Measured
distribution: 5,258 firms top out at 1, 3,157 at 2, and 1,993 at 3.

**Do I get phone numbers or e-mail addresses?**
No — and neither does anyone else. Both fields are `null` on all 10,408 rows in DiBK's data. You get
the organisation number on every row, which is the key to Brreg and to any Norwegian enrichment
provider.

**How do I get company size?**
Turn on *"Fetch employee count + approval start date"*. It reads each firm's own register page,
where `Antall lønnsmottakere` lives. It costs one extra request per firm at 1 request/second, so
filter first and enrich the slice.

**Why is a full enrichment run so slow?**
Because DiBK publishes a 300-requests-per-5-minutes-per-IP limit and we respect it by default. All
10,408 firms would take about 2.9 hours. Almost nobody needs that; a filtered slice of a few hundred
takes minutes.

**Can I check whether one specific contractor is approved?**
Yes — put its organisation numbers in `organizationalNumbers`. Numbers that are not in the register
come back as rows with `approved: false` and `in_register: false`, which is the answer. No extra
requests are made: the register is downloaded either way.

**How do I find firms whose approval is about to expire?**
Set `expiringWithinDays` (90 gives ~509 firms; 365 gives ~2,719), or use
`approvalExpiresBefore` / `approvalExpiresAfter` for an exact window, and sort by
`approval_expiry`.

**Does the monitor really remember between runs?**
Yes. State lives in a *named* key-value store (`sentral-godkjenning-monitor-state`), not the default
one — an unnamed store is wiped after every run, which would report every firm as new every time.
All five change types are covered by an end-to-end test before every release.

**Why does the first monitor run return nothing?**
It records the baseline. There is nothing to compare against yet, so it returns no rows — and it is
not charged as a monitor run. The next run reports real changes.

**Can I run several separate watchlists?**
Yes. Give each one a different `monitorProfile`; each keeps its own snapshot and its own run history.

**Can I export to CSV / Excel / Google Sheets?**
Yes — Apify exports every dataset to CSV, XLSX, JSON, XML and HTML, or via API. If you want a narrow
CSV, set `includeApprovalAreas: false`: the flat `approval_areas_text`, `functions`,
`subject_areas`, `approval_area_count` and `max_tiltaksklasse` columns stay.

**Do I need a proxy?**
No. The register is a public, keyless GET with no anti-bot. The default is Apify Proxy on `auto`
because it is polite and cheap here (~0.6 MB per run); you can turn it off entirely.

**Does it need residential proxies?**
No. Datacenter proxies returned HTTP 200 on 5 of 5 register fetches and 20 of 20 detail pages.
Residential would only add cost.

**What happens if DiBK changes the API?**
The Actor asserts on the response envelope and on the keys of every record. On a shape change it
fails loudly with an explanation and charges nothing, rather than quietly delivering a mis-mapped
dataset. DiBK's published spec already disagrees with its live deployment in two places, so this is
not hypothetical.

**Is scraping this legal?**
The data is an open public register published under CC BY 4.0, which explicitly permits commercial
reuse with attribution — and this Actor puts that attribution on every row. `robots.txt` carries no
rules. See the section above for DiBK's own note about permanent storage, quoted in full, and make
your own call on it.

***

### 🔗 Related Actors

Looking for other national registers and contractor directories? Browse the rest of the
**ScrapersDelight** store profile.

# Actor input Schema

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

Register mode downloads the whole register (10,408 firms, measured 2026-09-08), applies your filters locally and returns matching firms. Monitor mode does the same read but delivers only the differences against a snapshot kept in a named key-value store; the FIRST monitor run just records the baseline, returns no rows and is not charged.

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

Keep only firms whose name contains one of these (case- and diacritic-insensitive, so "tomrer" matches "TØMRER"). Several terms are OR-ed. Leave empty for every firm. Matched against the enterprise name only — use the field below for organisation numbers.

## `organizationalNumbers` (type: `array`):

Look up specific Norwegian organisation numbers — 9 digits each. Costs no extra requests: the whole register is downloaded either way, so this is a filter, not a lookup fee. Numbers that are NOT in the register come back as rows with `approved: false` and `in_register: false`, which is the answer to "does this contractor hold central approval?"

## `reportMissingOrganizationalNumbers` (type: `boolean`):

On (default): every organisation number you asked for gets a row, including the ones with no central approval (`approved: false`, `in_register: false`). Off: only approved firms come back and a missing number is silently absent. Only applies when you filled in organisation numbers above. Those answer rows are appended after the register scan, so set maxFirms to 0 when you want an answer for every number you asked about — a cap can cut them, and the run says so.

## `functions` (type: `array`):

The role the firm is approved for. Utførende = it builds; Prosjekterende = it designs/engineers; Ansvarlig søker = it is the responsible applicant on a building application; Uavhengig kontrollerende = it does independent inspection. Several are OR-ed. Empty = every function.

## `subjectAreas` (type: `array`):

The trade the approval covers. All 36 subject areas that exist in the register are listed with their real firm counts, so nothing here can return zero by typo. Several are OR-ed. Empty = every subject area.

## `subjectAreaCodes` (type: `array`):

DiBK's own short codes, if you would rather pass those than the Norwegian names — e.g. UTOMRE (carpentry, 3,852 firms), UVEGRU (roads and groundworks, 2,067), PARKIT (architecture, 1,554), USANI (sanitary installation, 1,465). 46 codes exist across the 36 subject areas, because some areas have a separate code per function. OR-ed with the picker above.

## `minTiltaksklasse` (type: `integer`):

Keep firms whose HIGHEST approval class is at least this. Tiltaksklasse is the project-complexity tier — 3 is the largest and most demanding work. Measured 2026-09-08: 5,258 firms top out at 1, 3,157 at 2 and 1,993 at 3. Set 3 for the key-account list. 0 = no filter.

## `minApprovalAreas` (type: `integer`):

Keep firms holding at least this many valid approval areas — a rough breadth-of-capability proxy. The register holds 35,726 approval-area rows over 10,408 firms (average 3.4, maximum 30). 0 = no filter.

## `requireLiabilityInsurance` (type: `boolean`):

DiBK's `liability_insurance` flag. True on 4,489 of 10,408 firms (43.1%), measured 2026-09-08.

## `requireInjuryInsurance` (type: `boolean`):

DiBK's `industrial_injury_insurance` flag. True on 4,354 of 10,408 firms (41.8%).

## `educationalEnterprisesOnly` (type: `boolean`):

DiBK's `educational_enterprise_approved` flag — the firm is approved to take on apprentices. True on 4,553 of 10,408 firms (43.7%).

## `expiringWithinDays` (type: `integer`):

Keep firms whose approval expires in this many days or fewer. 90 gives ~509 firms, 365 gives ~2,719 (measured 2026-09-08 — these move every day). 0 = no filter. Dates are compared in UTC.

## `approvalExpiresBefore` (type: `string`):

Absolute upper bound on the approval end date, e.g. 2027-06-30. Empty = no bound.

## `approvalExpiresAfter` (type: `string`):

Absolute lower bound on the approval end date. Combine with the field above for an exact window. The whole register currently sits between 2026-09-08 and 2029-09-07 (measured 2026-09-08). Empty = no bound.

## `postalTowns` (type: `array`):

Exact postal-town match, case- and diacritic-insensitive — typing "tromso" returns all 64 TROMSØ firms and "alesund" returns the ÅLESUND ones. Several towns are OR-ed. Empty = the whole country.

## `postalCodePrefixes` (type: `array`):

Keep firms whose business postcode starts with any of these, e.g. "0" for the Oslo series or "50" for central Bergen. Useful when you want a region rather than a town. OR-ed with each other, AND-ed with the town list above.

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

21 of the 10,408 approved enterprises are registered outside Norway (measured 2026-09-08). Empty = all countries.

## `onlyWithWebsite` (type: `boolean`):

Keep only the 1,854 firms (17.8%, measured 2026-09-08) that publish a `www` value. Everything else is filtered out BEFORE billing, so you never pay for a row you then discard.

## `excludeSoleProprietorships` (type: `boolean`):

Norwegian sole proprietorships are listed under the owner's own name, and their business address is often a home address. This drops the 816 rows (7.8%, measured 2026-09-08) whose name carries no company-form suffix (AS, ASA, ANS, DA, NUF, KOMMUNE …). It is a HEURISTIC and it over-reaches slightly: a trade name like "TARALDSEN ELEKTRO" also has no suffix. Every row carries `likely_sole_proprietorship` and `company_form_suffix` so you can judge for yourself.

## `enrichWithDetails` (type: `boolean`):

Adds `employee_count_raw`, `employee_count_min`, `employee_count_max` and `approval_period_from` by fetching each delivered firm's register page. Charged per successful fetch — a not-found page, a rate-limited request or a timeout adds nothing and bills nothing. Filter FIRST, then enrich the slice: at the default 1 request/second, 400 firms take about 7 minutes and all 10,408 take about 2.9 hours.

## `maxDetailFetches` (type: `integer`):

Hard ceiling on how many detail pages this run may fetch, regardless of how many rows it delivers. Rows past the ceiling are still delivered, with `detail_complete: false` and `detail_skipped_reason` saying why. 0 = enrich every delivered row.

## `detailRequestsPerSecond` (type: `integer`):

DiBK publishes a limit of 300 requests per 5 minutes per IP — exactly 1 request/second sustained — and answers HTTP 429 above it. The default of 1 is that published number. Raising it is your call; the Actor honours 429 plus Retry-After either way and never bills a rate-limited fetch.

## `detailConcurrency` (type: `integer`):

How many detail fetches may be in flight at once. The requests-per-second limiter above is the real gate, so this only smooths latency; raising it does not raise the request rate.

## `rotateDetailProxySessions` (type: `boolean`):

Off (default): every detail request goes through ONE pinned proxy session, so the per-IP limit DiBK publishes applies to us as they intend. On: requests are spread over 20 proxy sessions, which is faster but deliberately routes around a published per-IP limit. Stated plainly so the choice is yours.

## `maxFirms` (type: `integer`):

Ceiling on delivered rows. **0 = every match** — the full register is 10,408 rows, which at $0.003/row is $31.22, so the default is deliberately 100 to stop an accidental full pull. When the cap truncates, the run log and the status message say so and name the cap as the reason. In Monitor mode the cap also applies to change rows — but a change it withheld keeps its PREVIOUS snapshot entry and is reported again on the next run, so a cap can never silently destroy an approval change.

## `sortBy` (type: `string`):

Sorting is applied to ALL matching firms and only then is the cap applied, so "top 50 by expiry" really is the top 50 of the whole register — never the top 50 of an arbitrary first slice.

## `sortOrder` (type: `string`):

Ignored when sorting by register order.

## `includeApprovalAreas` (type: `boolean`):

On (default): every row carries `valid_approval_areas` — one entry per approval with function, subject area, DiBK codes, tiltaksklasse and the planning-act version. Off: the row keeps the flat summary fields (`functions`, `subject_areas`, `approval_area_count`, `max_tiltaksklasse`, `approval_areas_text`) and drops the nested array, which makes a CSV export much narrower.

## `includeRawRecord` (type: `boolean`):

Adds `raw_record` — DiBK's untouched JSON object for that firm — next to the normalised fields, for auditing or for fields you would rather map yourself. Roughly doubles the dataset size.

## `monitorProfile` (type: `string`):

Lets one account keep several independent watchlists — each profile has its own snapshot and its own run history. Change it and the next run starts a fresh baseline.

## `changeTypes` (type: `array`):

Empty = all five. Change counts across the whole register are always reported in the log, even for types you filter out of the dataset. Note for `lapsed`: the firm is gone from DiBK, so only what the snapshot recorded can be filtered on (organisation number, name, postal town, approval end date, tiltaksklasse, approval-area count). If any other filter is active — website, insurance, function, subject area, country, postcode, expiring-within, exclude-sole-proprietorships — lapsed rows are withheld and NOT charged rather than delivered on a guess, and their snapshot entries are kept so a later run without that filter still reports them.

## `monitorKeepHistoryRuns` (type: `integer`):

The named key-value store also keeps a one-line summary of each monitoring run (date, register size, changes by type). At one run a day, 365 keeps a year of register history — which, since DiBK publishes no historical endpoint, exists nowhere else.

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

Because the body is brotli-compressed the whole register costs only ~0.6 MB of proxy traffic per run, not 20 MB. Turn the proxy off entirely to go out from the Apify container IP — that works too.

## Actor input object example

```json
{
  "mode": "register",
  "searchTerms": [],
  "organizationalNumbers": [],
  "reportMissingOrganizationalNumbers": true,
  "functions": [],
  "subjectAreas": [],
  "subjectAreaCodes": [],
  "minTiltaksklasse": 0,
  "minApprovalAreas": 0,
  "requireLiabilityInsurance": false,
  "requireInjuryInsurance": false,
  "educationalEnterprisesOnly": false,
  "expiringWithinDays": 0,
  "approvalExpiresBefore": "",
  "approvalExpiresAfter": "",
  "postalTowns": [],
  "postalCodePrefixes": [],
  "countries": [],
  "onlyWithWebsite": false,
  "excludeSoleProprietorships": false,
  "enrichWithDetails": false,
  "maxDetailFetches": 0,
  "detailRequestsPerSecond": 1,
  "detailConcurrency": 3,
  "rotateDetailProxySessions": false,
  "maxFirms": 50,
  "sortBy": "register_order",
  "sortOrder": "asc",
  "includeApprovalAreas": true,
  "includeRawRecord": false,
  "monitorProfile": "default",
  "changeTypes": [],
  "monitorKeepHistoryRuns": 365,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (type: `string`):

The dataset of scraped register rows — one centrally approved Norwegian building enterprise per row, with its approval areas, tiltaksklasse, approval expiry date and address. In Monitor mode each row is one approval CHANGE (newly approved, renewed, shortened, areas changed, or lapsed).

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

Register-wide totals for this run — how many firms the register held, how many matched your filters, how many were delivered, and whether a cap or a charging limit truncated the result.

# 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 = {
    "mode": "register",
    "enrichWithDetails": false,
    "maxFirms": 50,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/sentral-godkjenning-no-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 = {
    "mode": "register",
    "enrichWithDetails": False,
    "maxFirms": 50,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/sentral-godkjenning-no-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 '{
  "mode": "register",
  "enrichWithDetails": false,
  "maxFirms": 50,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call scrapersdelight/sentral-godkjenning-no-scraper --silent --output-dataset

```

## MCP server setup

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