# Denmark Restaurant Inspections: Smiley Food Safety (`nordicdata/dk-smiley-inspections`) Actor

Danish restaurant and food business inspection results from the Fødevarestyrelsen Smiley food safety scheme: up to four inspections per business, result labels, CVR, P-number, address and industry. Filter by postal code or result, or get only changed units. No API key needed.

- **URL**: https://apify.com/nordicdata/dk-smiley-inspections.md
- **Developed by:** [Mikkel Jeppesen Juhl](https://apify.com/nordicdata) (community)
- **Categories:** Business, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 food business units

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

## Denmark Restaurant Inspections: Smiley Food Safety

Get Danish restaurant and food safety inspection results as clean JSON. The actor reads the open Smiley file that Fødevarestyrelsen publishes on findsmiley.dk. It returns one row per food business unit with its latest four inspections, ISO dates, English result labels, CVR and P-number, address, industry and the advertising protection flag.

The file covers about 59,000 restaurants, shops, bakeries, canteens, wholesalers and transport firms. You can filter it by postal code, city, industry, business type, CVR number, latest result and inspection date. You can also run the actor on a schedule and get only the units that changed since the last run, such as a new inspection or a new sour smiley.

### Use cases

- Export Danish restaurant inspection results by postal code or city.
- Monitor new sour smileys and new inspections on a schedule with change detection.
- Look up every food business of a company by CVR number.
- Build food safety datasets for research, insurance, delivery platforms or local news.

### What you get

| Field | Meaning |
| --- | --- |
| `smileyId` | Fødevarestyrelsen's id for the unit, also the findsmiley.dk page number |
| `cvr` | Danish CVR number of the owner, 8 digits. Missing for about 2% of units |
| `pNumber` | Production unit number (P-nummer). Missing for about 3% of units |
| `name`, `address`, `postalCode`, `city` | Unit name and address as registered with Fødevarestyrelsen |
| `businessType` | `retail` (detail, sells to consumers) or `wholesale` (engros) |
| `smileyIndustry` | Industry group on findsmiley.dk, in Danish, for example "Dagligvareforretninger" |
| `fvstIndustryCode`, `fvstIndustry` | Fødevarestyrelsen's finer industry code and name |
| `latestResult` | Result of the latest inspection, 1 to 4, or null with no inspection yet |
| `latestResultLabel` | The result in English, see the table below |
| `latestSmiley` | `happy`, `straight` or `sour` for retail units, null for wholesale |
| `latestInspectionDate` | Date of the latest inspection, YYYY-MM-DD |
| `worstRecentResult` | Highest result among the published inspections |
| `inspectionCount` | How many inspections the file holds for the unit, 0 to 4 |
| `inspections` | Up to four inspections, newest first, each with `date`, `result` and `resultLabel` |
| `advertisingProtected` | `true` when the unit is flagged with reklamebeskyttelse. Always present |
| `findSmileyUrl` | The unit's page on findsmiley.dk with the full inspection reports |
| `changeType` | In change mode: `new`, `changed` or `removed`. Null otherwise |
| `changedParts` | In change mode: `inspections`, `details` and or `advertisingProtection` |
| `previousLatestResult`, `previousLatestInspectionDate` | In change mode: the values in the previous snapshot |

#### Result codes

| Code | Danish | English | Retail smiley |
| --- | --- | --- | --- |
| 1 | Ingen anmærkninger | No remarks | Happy |
| 2 | Indskærpelse | Warning | Straight mouth |
| 3 | Påbud, forbud eller tvangsbøder | Order, ban or daily fines | Straight mouth |
| 4 | Bødeforlæg, politianmeldelse, karantæne, autorisation eller registrering frataget | Fine, police report, quarantine, or approval or registration withdrawn | Sour |

A null result means the unit has not been inspected yet, or sits in the lowest risk group and is rarely inspected. Fødevarestyrelsen publishes a result before appeals end and before a fine is final, so a 4 is not a final verdict. Findsmiley.dk can also withdraw a report, and the unit's inspections then move up a slot.

### Input

```json
{
  "postalCodes": ["1000-2999", "8000"],
  "cities": ["Aarhus C"],
  "smileyIndustries": ["Restauranter, kantiner, takeaway, værtshuse m.fl."],
  "businessTypes": ["retail"],
  "cvrNumbers": ["35954716"],
  "latestResults": ["2", "3", "4"],
  "inspectedSince": "2026-01-01",
  "excludeAdvertisingProtected": false,
  "onlyChangedSinceLastRun": false,
  "snapshotName": "default",
  "includeRemoved": true,
  "maxItems": 1000,
  "sourceFormat": "xlsx"
}
```

- Every filter is optional. Filters combine with AND. Values inside one filter combine with OR.
- `postalCodes` takes single codes or ranges such as `1000-2999`.
- `latestResults` takes `1`, `2`, `3`, `4` and `none`.
- `cvrNumbers` returns every unit the company owns. A supermarket chain owns over 1,000 units.
- `sourceFormat` picks the 7 MB Excel file or the 60 MB XML file. They hold the same data.

### Change detection

Turn on `onlyChangedSinceLastRun` and schedule the actor, for example daily. The first run saves a snapshot of the whole file in a named key-value store (`dk-smiley-snapshots`) and outputs nothing. Each later run compares the file with the snapshot and outputs only:

- `new` units that were not in the last snapshot,
- `changed` units with a new inspection, a withdrawn inspection, new details or a changed advertising flag,
- `removed` units that left the file. Removed rows are free.

Use one `snapshotName` per schedule. Your filters apply to the output, and the snapshot always covers the whole file, so you can change filters between runs. Fødevarestyrelsen rebuilds the file more than once a day.

### Advertising protection (reklamebeskyttelse)

About 40% of the units carry `advertisingProtected: true`. The flag mirrors the CVR register's reklamebeskyttelse. A company sets it in CVR to say it does not want direct marketing. Under section 19 of the Danish CVR Act, a private company must not use CVR data about a protected unit for direct marketing, and anyone who passes on its contact data must mark it as protected. That is why the flag is in every row.

**If you buy this data, you must not use it to direct-market units with `advertisingProtected: true`.** That covers mail, phone calls, visits and any other sales approach built on this data. Separate rules apply to everyone: the Danish Marketing Practices Act section 10 bans unsolicited email and SMS marketing without prior consent, and the GDPR applies when a unit is a sole proprietorship named after a person. Set `excludeAdvertisingProtected` to drop protected units from the output. This is not legal advice. You are responsible for how you use the data.

### Source, terms and no endorsement

Source: Fødevarestyrelsen (the Danish Veterinary and Food Administration), Smiley data from findsmiley.dk, downloaded from https://pub.fvst.dk/publikationer/Smileydata.xlsx or .xml on each run. Fødevarestyrelsen publishes the data under the Danish terms for reuse of public data, with four extra conditions: name Fødevarestyrelsen as the source, do not use its logo, any smiley shown for a single business must match its current smiley, and follow the design rules for the smiley symbols. If you show smiley symbols to others, show the current result and link to `findSmileyUrl`.

The actor renames the fields to English, converts dates to ISO format, adds result labels and computes `latestSmiley`, `worstRecentResult` and `inspectionCount`. Fødevarestyrelsen does not endorse or support this actor. The official results are the ones on findsmiley.dk.

### Limits of the source

- Only the latest four inspections per unit are published, and nothing from before 24 January 2012.
- The file has no inspection report text. Read the reports on findsmiley.dk.
- Units appear by production site. Chains appear once per shop and department.
- The file has no email or phone data.

### Run it locally

No Apify account is needed. You need `uv`.

```bash
cd apps/apify-dk-smiley-inspections
uv sync
./run_local.sh input.example.json
uv run python test/coverage.py storage/datasets/default
uv run python -m test.test_filters
```

Set `SMILEY_SOURCE_FILE=/path/Smileydata.xml` to read a saved file in place of the download. Run stats land in `storage/key_value_stores/default/RUN_STATS`.

### Pricing

Pay per event: 0.002 USD per unit row (2 USD per 1,000). Removed rows in change mode are free. No start fee. A daily change run on all of Denmark returns about 150 rows on a weekday, since Fødevarestyrelsen inspects about 150 units a day.

# Actor input Schema

## `postalCodes` (type: `array`):

Danish postal codes or ranges, for example 8000 or 1000-2999 for central Copenhagen. Leave empty for all of Denmark.

## `cities` (type: `array`):

Postal town names as written in the source, for example Aarhus C or Odense M. Case does not matter.

## `smileyIndustries` (type: `array`):

The industry groups Fødevarestyrelsen uses on findsmiley.dk. Leave empty for all.

## `businessTypes` (type: `array`):

retail (detail) sells to consumers and shows smiley faces. wholesale (engros) sells to other businesses.

## `cvrNumbers` (type: `array`):

Keep only units owned by these CVR numbers (8 digits). One CVR can own many units.

## `latestResults` (type: `array`):

Keep units whose latest inspection has one of these results.

## `inspectedSince` (type: `string`):

YYYY-MM-DD. Keep units whose latest inspection is on or after this date.

## `excludeAdvertisingProtected` (type: `boolean`):

Drop units flagged with reklamebeskyttelse. The flag stays in every row either way.

## `onlyChangedSinceLastRun` (type: `boolean`):

Compare with a snapshot saved by the previous run and output only new, changed and removed units. The first run saves the baseline and outputs nothing. Schedule the actor daily to get new inspections.

## `snapshotName` (type: `string`):

Name of the saved snapshot. Use one name per schedule. Letters, digits, - and \_.

## `includeRemoved` (type: `boolean`):

Add a free row with changeType removed when a unit leaves the file.

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

Stop after this many rows. The whole file holds about 59,000 units.

## `sourceFormat` (type: `string`):

Both files hold the same data.

## Actor input object example

```json
{
  "postalCodes": [
    "1000-2999"
  ],
  "excludeAdvertisingProtected": false,
  "onlyChangedSinceLastRun": false,
  "snapshotName": "default",
  "includeRemoved": true,
  "maxItems": 1000,
  "sourceFormat": "xlsx"
}
```

# Actor output Schema

## `results` (type: `string`):

One row per food business with its latest Smiley inspection results.

## `runStats` (type: `string`):

Summary counts for the run, such as how many rows were written.

# 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 = {
    "postalCodes": [
        "1000-2999"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nordicdata/dk-smiley-inspections").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 = { "postalCodes": ["1000-2999"] }

# Run the Actor and wait for it to finish
run = client.actor("nordicdata/dk-smiley-inspections").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 '{
  "postalCodes": [
    "1000-2999"
  ]
}' |
apify call nordicdata/dk-smiley-inspections --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nordicdata/dk-smiley-inspections"
        }
    }
}
```

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/HyeNyvJnLdP9eG7Bb/builds/uMlvvBi60TIy5NA3u/openapi.json
