# Lieferando Menu Scraper - Restaurant Menus, Prices & Delivery (`kamerozkan/german-delivery-menu-price-intelligence`) Actor

Scrape restaurant menus & dish prices from Lieferando.de, Germany's largest food delivery platform. Menu, price, delivery fees & minimum order per German postcode, with change detection across runs. Food delivery menu data as JSON rows. Unofficial, independent tool.

- **URL**: https://apify.com/kamerozkan/german-delivery-menu-price-intelligence.md
- **Developed by:** [Kamer Ozkan](https://apify.com/kamerozkan) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 restaurant or menu results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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

## Lieferando Menu & Price Scraper - German Food Delivery Data

> **Unofficial Actor:** This Actor accesses publicly displayed data from Lieferando.de. It is not affiliated with, endorsed by, or sponsored by Lieferando or Just Eat Takeaway.com.

Production-oriented Apify Actor for postcode-dependent Lieferando.de intelligence. It resolves each German postal code through Lieferando, discovers local restaurant branches, extracts full menus and delivery economics, normalizes equivalent products, retains 90 days of prices, and produces ready-to-use comparison reports.

### Included intelligence

- complete menu rows with product name, description, category, price, availability, image, and source ID
- delivery fee, minimum order, delivery time, rating, offer, address, coordinates, and opening hours
- published cuisine labels plus explainable menu-based cuisine inference when metadata is missing
- stable chain identity for comparing the same restaurant group across branches
- semantic product normalization that maps spelling variants such as `Döner Kebab`, `Doener Kebap`, and `DONER`
- optional custom alias mappings for market- or brand-specific product language
- size-, quantity-, dietary-, and variant-aware comparable product IDs to avoid false matches
- postcode price matrix with min/average/max price, branches, EUR spread, and percentage spread
- per-item rolling 90-day observations, EUR change, and percentage change
- restaurant-level 90-day menu-basket inflation summaries
- HTML intelligence dashboard plus CSV and JSON exports
- session rotation, blocked-request recovery, run-quality scoring, optional HTTPS alerts, and optional failure on degraded data

### Outputs

The default dataset contains five record types:

| `recordType` | Purpose |
| --- | --- |
| `menu_item` | One current menu product with branch, location, normalization, and 90-day history |
| `restaurant` | Restaurant-level fallback row when menus are disabled or empty |
| `postcode_price_matrix` | Same chain and comparable product summarized across postal codes |
| `menu_inflation_summary` | Current versus first retained comparable menu basket |
| `run_summary` | Coverage, failure rates, quality metrics, issues, and final status |

The default key-value store also contains:

- `REPORT.html` - visual executive report
- `POSTCODE_PRICE_MATRIX.csv` - spreadsheet-ready postcode comparison
- `ANALYTICS.json` - matrix and inflation data
- `RUN_SUMMARY.json` - monitoring payload
- `SCHEDULE_RECOMMENDATION.json` - daily production schedule template

### Example output

All records below are unedited output from live runs on 2026-07-28. The Actor emits
four record types into one dataset: `menu_item`, `postcode_price_matrix`,
`menu_inflation_summary`, and `run_summary`.

#### 1. A restaurant listing with its live offer (`menu_item`)

A real Berlin listing: parsed delivery economics (fee, minimum, time window), structured
address and coordinates, cuisine classification with confidence and source, and the
running promotion exactly as the platform shows it. Fields the source does not publish
stay `null`.

```json
{
  "recordType": "menu_item",
  "observedAt": "2026-07-28T20:31:44.957Z",
  "searchPostalCode": "10115",
  "searchResultUrl": "https://www.lieferando.de/en/delivery/food/postcode-10115",
  "restaurantName": "Sushi Yana",
  "restaurantSlug": "sushi-yana-moabit",
  "restaurantUrl": "https://www.lieferando.de/en/menu/sushi-yana-moabit?c_id=019fa26e-3e3a-7697-b664-b06e591dc3fd",
  "chainKey": "lieferando:sushi_yana",
  "chainName": "Sushi Yana",
  "chainSource": "lieferando_chain_asset",
  "rating": 4.6,
  "reviewCount": 0,
  "reviewCountText": "(9.600+)",
  "deliveryTimeText": "25-40 min",
  "deliveryTimeMin": 25,
  "deliveryTimeMax": 40,
  "deliveryFeeText": "Free delivery",
  "deliveryFee": 0,
  "minimumOrderText": "Min. 35,00€",
  "minimumOrder": 35,
  "offer": "50% off selected items",
  "sponsored": true,
  "stampCard": false,
  "address": {
    "@type": "PostalAddress",
    "addressCountry": "Germany",
    "addressLocality": "Berlin",
    "addressRegion": "Berlin",
    "postalCode": "10555",
    "streetAddress": "Gotzkowskystraße 26"
  },
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": 52.524754,
    "longitude": 13.330654
  },
  "openingHours": [
    "Su 00:00-00:45",
    "Su-Th 10:30-23:45",
    "Fr-Sa 10:30-23:59",
    "Sa 00:00-00:45"
  ],
  "cuisineTypes": [
    "Japanese / Sushi",
    "Salads / Healthy"
  ],
  "cuisineClassifications": [
    {
      "cuisine": "Japanese / Sushi",
      "confidence": 0.98,
      "source": "menu_inference",
      "matchedTerms": [
        "sushi",
        "maki",
        "nigiri",
        "sashimi",
        "tempura"
      ]
    },
    "(1 more, shortened here)"
  ],
  "sourceProductId": "fdedc97b-8012-4815-b449-f698e815abbd",
  "category": "Starters",
  "productName": "Vegetarian Spring Rolls - 6 pieces",
  "description": "Served with sweet and sour sauce",
  "priceText": "3,70 €",
  "isFromPrice": false,
  "isAvailable": true,
  "imageUrl": "https://just-eat-prod-eu-res.cloudinary.com/image/upload/c_thumb,w_120/f_auto/q_auto/v1/de/dishes/13026782/5cebe021907a856e78b3bc759b6176ed",
  "price": 3.7,
  "priceCurrency": "EUR",
  "canonicalName": "vegetarian spring rolls",
  "canonicalProductId": "de-menu-v3:37c5ee26528ce1b2027aeeb2",
  "comparableProductId": "de-menu-v3:d189604bdd1b148a3fc3a5fa",
  "productSignature": "rolls spring vegetarian",
  "normalizationConfidence": 0.81,
  "productAttributes": {
    "sizeValue": null,
    "sizeUnit": null,
    "sizeKey": null,
    "quantity": 6,
    "dietary": [
      "vegetarian"
    ],
    "variants": [],
    "semanticTokens": [
      "rolls",
      "spring",
      "vegetarian"
    ]
  },
  "normalizationVersion": "de-menu-v3",
  "priceHistory90d": [
    {
      "observedAt": "2026-07-28T20:31:44.957Z",
      "price": 3.7
    }
  ],
  "priceChange90d": null,
  "priceChangePercent90d": null,
  "firstObservedAt": "2026-07-28T20:31:44.957Z",
  "observationCount": 1
}
````

#### 2. Every run audits itself (`run_summary` and `menu_inflation_summary`)

The run reports its own quality instead of asking to be trusted: request failure and
retry rates, the share of items with missing prices, cuisine classification coverage,
and exactly how many results were charged. A run that fails its quality thresholds says
so in `issues` rather than shipping silently bad data.

```json
{
  "recordType": "run_summary",
  "observedAt": "2026-07-28T12:27:40.624Z",
  "completedAt": "2026-07-28T12:29:09.422Z",
  "status": "healthy",
  "expectedPostalCodes": [
    "10115"
  ],
  "resolvedPostalCodes": [
    "10115"
  ],
  "requestedRestaurantLimit": 2,
  "discoveredRestaurantCount": 2,
  "scrapedRestaurantCount": 2,
  "menuItemCount": 79,
  "unavailableItemCount": 7,
  "matrixRowCount": 72,
  "crossPostcodeMatrixRowCount": 0,
  "inflationSummaryCount": 2,
  "failedRequestCount": 0,
  "failedRequestRate": 0,
  "retriedRequestCount": 0,
  "totalRetryCount": 0,
  "retryRate": 0,
  "nullPriceRate": 0,
  "cuisineCoverage": 1,
  "averageMenuItemsPerRestaurant": 39.5,
  "smallMenuRestaurantCount": 0,
  "issues": [],
  "failures": [],
  "chargedResultCount": 79,
  "stoppedBySpendingLimit": false
}
```

The inflation basket is honest about its composition: unavailable items are excluded
from the tracked basket instead of being carried at stale prices, and the record shows
that accounting (41 items on the menu, 6 unavailable, 35 tracked).

```json
{
  "recordType": "menu_inflation_summary",
  "observedAt": "2026-07-28T12:27:40.624Z",
  "searchPostalCode": "10115",
  "chainKey": "lieferando:burgermeister",
  "chainName": "Burgermeister",
  "restaurantName": "Burgermeister Eberswalder",
  "restaurantUrl": "https://www.lieferando.de/en/menu/burgermeister-5?c_id=019f37d0-05cc-7243-9430-de2821818e9a",
  "menuItemCount": 41,
  "unavailableItemCount": 6,
  "trackedItemCount": 35,
  "changedItemCount": 0,
  "increasedItemCount": 0,
  "decreasedItemCount": 0,
  "unchangedItemCount": 35,
  "currentComparableBasket": 267.84,
  "firstComparableBasket": 267.84,
  "menuInflation90d": 0,
  "medianItemChangePercent90d": 0,
  "maxItemIncreasePercent90d": null,
  "maxItemDecreasePercent90d": null
}
```

#### 3. Cross-postcode price comparison (`postcode_price_matrix`)

The same comparable product is matched across postcodes with per-postcode min, average,
and max plus the contributing branches. In this real observation both areas were served
at identical prices, so the spread is reported as exactly zero: the matrix reports no
difference when there is none, which is what makes a nonzero spread trustworthy.

```json
{
  "recordType": "postcode_price_matrix",
  "observedAt": "2026-07-28T20:31:44.957Z",
  "chainKey": "lieferando:burgermeister",
  "chainName": "Burgermeister",
  "canonicalProductId": "de-menu-v3:77c75ffb312ff5c8a9a65dac",
  "comparableProductId": "de-menu-v3:e0f4f0d4ace60fde53c95650",
  "canonicalName": "hamburger",
  "postalCodeCount": 2,
  "branchCount": 1,
  "priceByPostalCode": {
    "10115": {
      "postalCode": "10115",
      "branchCount": 1,
      "observationCount": 1,
      "minPrice": 7.1,
      "averagePrice": 7.1,
      "maxPrice": 7.1,
      "branches": [
        {
          "restaurantName": "Burgermeister Eberswalder",
          "restaurantUrl": "https://www.lieferando.de/en/menu/burgermeister-5?c_id=019f37d0-05cc-7243-9430-de2821818e9a",
          "price": 7.1
        }
      ]
    },
    "10435": {
      "postalCode": "10435",
      "branchCount": 1,
      "observationCount": 1,
      "minPrice": 7.1,
      "averagePrice": 7.1,
      "maxPrice": 7.1,
      "branches": [
        {
          "restaurantName": "Burgermeister Eberswalder",
          "restaurantUrl": "https://www.lieferando.de/en/menu/burgermeister-5?c_id=019f37d0-05cc-7243-9430-de2821818e9a",
          "price": 7.1
        }
      ]
    }
  },
  "lowestPrice": 7.1,
  "highestPrice": 7.1,
  "priceSpread": 0,
  "priceSpreadPercent": 0
}
```

### Pricing

Pay-per-event: **$0.001 per delivered result row** ($1 per 1,000 rows), covering menu
items, price matrices, inflation summaries, and run summaries alike, plus a negligible
run-start fee. A failed run produces no result charges.

This Actor is also exposed to AI agents through Apify's MCP server
(mcp.apify.com): an agent can discover it by search and run it with the same
pay-per-event billing, with no separate integration.

### Run locally

```bash
npm install
npm run typecheck
npm test
npm run build
npm start
```

Copy [`examples/production-input.json`](examples/production-input.json) to `storage/key_value_stores/default/INPUT.json` before `npm start`. For a short smoke test, reduce `postalCodes` to one value and `maxRestaurantsPerPostalCode` to `1`.

### Production use

Use the input in [`examples/production-input.json`](examples/production-input.json), keep the same named key-value store between runs, and create a daily Apify schedule. Full scheduling, alerting, output, and retention guidance is in [`docs/production.md`](docs/production.md).

For a fixed competitor set, add Lieferando menu URLs to `restaurantUrls`. Direct URLs can be collected without a search-postcode context, but postcode matrices require postcode-routed discovery.

Lieferando blocks data-center and direct Apify cloud IPs. The Actor therefore defaults to a German residential Apify Proxy profile, which is billed separately by Apify. The proxy settings remain visible and editable in the Actor input.

### Comparison behavior

`canonicalProductId` represents the normalized semantic product. `comparableProductId` adds size, quantity, dietary, and variant attributes, so a 0.33 L cola is not compared with a 1 L cola.

Matrix rows are keyed by chain plus `comparableProductId`. A cross-postcode match therefore requires at least two input postal codes and an overlapping chain/product combination. Every source branch remains listed in the matrix cell for auditability.

Price history is keyed by postal code, branch, and Lieferando source product ID in the named store `lieferando-price-history-v1`. Repeated runs retain 90 days and append at most one unchanged observation per day. A first run has no earlier baseline, so change fields remain `null` until a later run supplies one.

`customProductAliases` can extend the built-in German rules without a code change. Aliases are applied safely as normalized phrases; size and quantity safeguards remain part of `comparableProductId`.

### Publish

The Actor definition, Store README, deterministic Docker build, input/output schemas, dataset view, resource limits, and changelog are included. Deploy the folder with `apify push` or connect it as the Actor source in Apify Console, complete the account-owned Store display fields, and click **Publish to Store**.

### Responsible operation

Use conservative concurrency, respect applicable law and platform terms, and avoid collecting personal or account data. The Actor only reads publicly displayed restaurant and menu information.

# Actor input Schema

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

Five-digit German postal codes. Each location is opened through Lieferando's own postcode-routed result page. Cost scales with delivered rows: $0.001 per result row (menu items, matrices, summaries), so postcode count times restaurants times menu size drives the total.

## `maxRestaurantsPerPostalCode` (type: `integer`):

Maximum number of unique restaurant branches collected for each postal code.

## `includeMenus` (type: `boolean`):

When enabled, outputs one dataset row per menu item. Disable for a faster restaurant-list-only run.

## `trackPriceHistory` (type: `boolean`):

Persists item price observations in a named key-value store and adds change metrics to each menu row. Keep the same store across scheduled runs so inflation summaries and change fields build real history.

## `generateAnalytics` (type: `boolean`):

Builds postcode price matrices, menu inflation summaries, CSV/JSON exports, and a visual HTML report.

## `maxConcurrency` (type: `integer`):

Maximum number of browser pages processed in parallel.

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

Maximum retry count for blocked or temporarily failing Lieferando pages.

## `qualityThresholds` (type: `object`):

Thresholds used to mark the run healthy, degraded, or failed.

## `customProductAliases` (type: `object`):

Optional exact phrase mappings for market-specific names, for example {"Hausrolle":"Signature Wrap"}. Built-in German normalization still applies.

## `failOnQualityIssues` (type: `boolean`):

When enabled, marks the Actor run as failed if any quality warning or error is found.

## `alertWebhookUrl` (type: `string`):

Optional HTTPS endpoint that receives the run-quality summary for degraded or failed runs.

## `sendSuccessWebhook` (type: `boolean`):

Also sends the monitoring webhook when the run is healthy.

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

Defaults to a German residential Apify Proxy because Lieferando blocks data-center IPs. Residential traffic is billed separately by Apify.

## `restaurantUrls` (type: `array`):

Optional direct Lieferando menu URLs. Useful for testing or tracking a fixed competitor set.

## Actor input object example

```json
{
  "postalCodes": [
    "10115",
    "10117",
    "10999"
  ],
  "maxRestaurantsPerPostalCode": 2,
  "includeMenus": true,
  "trackPriceHistory": true,
  "generateAnalytics": true,
  "maxConcurrency": 3,
  "maxRequestRetries": 3,
  "qualityThresholds": {
    "maxFailedRequestRate": 0.2,
    "maxRetryRate": 0.5,
    "maxNullPriceRate": 0.05,
    "minCuisineCoverage": 0.8,
    "minMenuItemsPerRestaurant": 5
  },
  "customProductAliases": {},
  "failOnQualityIssues": false,
  "sendSuccessWebhook": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "DE"
  }
}
```

# Actor output Schema

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

No description

## `report` (type: `string`):

No description

## `matrixCsv` (type: `string`):

No description

## `analyticsJson` (type: `string`):

No description

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

No description

## `scheduleRecommendation` (type: `string`):

No description

# 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": [
        "10115"
    ],
    "maxRestaurantsPerPostalCode": 2
};

// Run the Actor and wait for it to finish
const run = await client.actor("kamerozkan/german-delivery-menu-price-intelligence").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": ["10115"],
    "maxRestaurantsPerPostalCode": 2,
}

# Run the Actor and wait for it to finish
run = client.actor("kamerozkan/german-delivery-menu-price-intelligence").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "10115"
  ],
  "maxRestaurantsPerPostalCode": 2
}' |
apify call kamerozkan/german-delivery-menu-price-intelligence --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kamerozkan/german-delivery-menu-price-intelligence",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Lieferando Menu Scraper - Restaurant Menus, Prices & Delivery",
        "description": "Scrape restaurant menus & dish prices from Lieferando.de, Germany's largest food delivery platform. Menu, price, delivery fees & minimum order per German postcode, with change detection across runs. Food delivery menu data as JSON rows. Unofficial, independent tool.",
        "version": "1.0",
        "x-build-id": "gRf25zsfcjdd3TY1R"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/kamerozkan~german-delivery-menu-price-intelligence/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-kamerozkan-german-delivery-menu-price-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/kamerozkan~german-delivery-menu-price-intelligence/runs": {
            "post": {
                "operationId": "runs-sync-kamerozkan-german-delivery-menu-price-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/kamerozkan~german-delivery-menu-price-intelligence/run-sync": {
            "post": {
                "operationId": "run-sync-kamerozkan-german-delivery-menu-price-intelligence",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "postalCodes",
                    "maxRestaurantsPerPostalCode",
                    "includeMenus",
                    "trackPriceHistory",
                    "generateAnalytics",
                    "maxConcurrency",
                    "maxRequestRetries",
                    "qualityThresholds",
                    "customProductAliases",
                    "failOnQualityIssues",
                    "sendSuccessWebhook"
                ],
                "properties": {
                    "postalCodes": {
                        "title": "German postal codes",
                        "maxItems": 100,
                        "uniqueItems": true,
                        "type": "array",
                        "description": "Five-digit German postal codes. Each location is opened through Lieferando's own postcode-routed result page. Cost scales with delivered rows: $0.001 per result row (menu items, matrices, summaries), so postcode count times restaurants times menu size drives the total.",
                        "items": {
                            "type": "string",
                            "pattern": "^[0-9]{5}$"
                        },
                        "default": [
                            "10115"
                        ]
                    },
                    "maxRestaurantsPerPostalCode": {
                        "title": "Restaurants per postal code",
                        "minimum": 1,
                        "maximum": 500,
                        "type": "integer",
                        "description": "Maximum number of unique restaurant branches collected for each postal code.",
                        "default": 2
                    },
                    "includeMenus": {
                        "title": "Collect full menus",
                        "type": "boolean",
                        "description": "When enabled, outputs one dataset row per menu item. Disable for a faster restaurant-list-only run.",
                        "default": true
                    },
                    "trackPriceHistory": {
                        "title": "Track 90-day price history",
                        "type": "boolean",
                        "description": "Persists item price observations in a named key-value store and adds change metrics to each menu row. Keep the same store across scheduled runs so inflation summaries and change fields build real history.",
                        "default": true
                    },
                    "generateAnalytics": {
                        "title": "Generate intelligence reports",
                        "type": "boolean",
                        "description": "Builds postcode price matrices, menu inflation summaries, CSV/JSON exports, and a visual HTML report.",
                        "default": true
                    },
                    "maxConcurrency": {
                        "title": "Maximum concurrency",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "Maximum number of browser pages processed in parallel.",
                        "default": 3
                    },
                    "maxRequestRetries": {
                        "title": "Request retries",
                        "minimum": 0,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Maximum retry count for blocked or temporarily failing Lieferando pages.",
                        "default": 3
                    },
                    "qualityThresholds": {
                        "title": "Quality monitoring thresholds",
                        "type": "object",
                        "description": "Thresholds used to mark the run healthy, degraded, or failed.",
                        "properties": {
                            "maxFailedRequestRate": {
                                "title": "Maximum failed request rate",
                                "description": "Warning threshold expressed as a value from 0 to 1.",
                                "type": "number",
                                "minimum": 0,
                                "maximum": 1
                            },
                            "maxRetryRate": {
                                "title": "Maximum recovered-retry rate",
                                "description": "Warning threshold for requests that succeeded only after retrying, expressed as a value from 0 to 1.",
                                "type": "number",
                                "minimum": 0,
                                "maximum": 1
                            },
                            "maxNullPriceRate": {
                                "title": "Maximum null-price rate",
                                "description": "Warning threshold expressed as a value from 0 to 1.",
                                "type": "number",
                                "minimum": 0,
                                "maximum": 1
                            },
                            "minCuisineCoverage": {
                                "title": "Minimum cuisine coverage",
                                "description": "Required share of restaurants with a published or inferred cuisine.",
                                "type": "number",
                                "minimum": 0,
                                "maximum": 1
                            },
                            "minMenuItemsPerRestaurant": {
                                "title": "Minimum menu size",
                                "description": "Warning threshold for extracted items in each restaurant/postcode menu observation and for the overall average.",
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 1000
                            }
                        },
                        "default": {
                            "maxFailedRequestRate": 0.2,
                            "maxRetryRate": 0.5,
                            "maxNullPriceRate": 0.05,
                            "minCuisineCoverage": 0.8,
                            "minMenuItemsPerRestaurant": 5
                        },
                        "additionalProperties": false
                    },
                    "customProductAliases": {
                        "title": "Custom product aliases",
                        "maxProperties": 500,
                        "type": "object",
                        "description": "Optional exact phrase mappings for market-specific names, for example {\"Hausrolle\":\"Signature Wrap\"}. Built-in German normalization still applies.",
                        "default": {},
                        "additionalProperties": true
                    },
                    "failOnQualityIssues": {
                        "title": "Fail degraded runs",
                        "type": "boolean",
                        "description": "When enabled, marks the Actor run as failed if any quality warning or error is found.",
                        "default": false
                    },
                    "alertWebhookUrl": {
                        "title": "Alert webhook URL",
                        "type": "string",
                        "description": "Optional HTTPS endpoint that receives the run-quality summary for degraded or failed runs."
                    },
                    "sendSuccessWebhook": {
                        "title": "Send healthy-run webhooks",
                        "type": "boolean",
                        "description": "Also sends the monitoring webhook when the run is healthy.",
                        "default": false
                    },
                    "proxyConfiguration": {
                        "title": "Proxy configuration",
                        "type": "object",
                        "description": "Defaults to a German residential Apify Proxy because Lieferando blocks data-center IPs. Residential traffic is billed separately by Apify.",
                        "default": {
                            "useApifyProxy": true,
                            "apifyProxyGroups": [
                                "RESIDENTIAL"
                            ],
                            "apifyProxyCountry": "DE"
                        }
                    },
                    "restaurantUrls": {
                        "title": "Optional restaurant menu URLs",
                        "maxItems": 500,
                        "type": "array",
                        "description": "Optional direct Lieferando menu URLs. Useful for testing or tracking a fixed competitor set.",
                        "items": {
                            "type": "object",
                            "required": [
                                "url"
                            ],
                            "properties": {
                                "url": {
                                    "title": "Lieferando menu URL",
                                    "description": "Direct URL of one Lieferando restaurant menu.",
                                    "type": "string",
                                    "pattern": "^https:\\/\\/www\\.lieferando\\.de\\/(?:[a-z]{2}\\/)?menu\\/.+"
                                }
                            }
                        }
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
