# Fresha Scraper: Salons, Services, Prices & Reviews (`softprodlabs/fresha-scraper`) Actor

Find Fresha salons and unclaimed listings by city. Export service menus and prices, enrich public website contacts, monitor new salons and filter paginated reviews by date. Source-linked JSON/CSV; no login required.

- **URL**: https://apify.com/softprodlabs/fresha-scraper.md
- **Developed by:** [Software Production Labs](https://apify.com/softprodlabs) (community)
- **Categories:** Lead generation, E-commerce, Automation
- **Stats:** 3 total users, 2 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.40 / 1,000 salons

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/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

## Fresha Scraper: Salons, Services, Prices & Reviews

Find salons on Fresha by city, business category or treatment. Export business contacts, service menus with price variants, opening hours, team names and ratings. Include unclaimed directory listings, enrich contacts from the salon’s own website, and monitor newly discovered salons. Use review mode to collect public reviews across multiple pages with a date filter.

Built for salon research, beauty-industry lead lists and service-price comparisons. No Fresha account, cookies, API key or proxy is required.

### Start with five London hair salons

```json
{"city":"London","countryCode":"GB","category":"hair-salons","maxItems":5}
```

You can also supply public Fresha salon URLs or landing-page URLs in `urls`. Examples of landing pages: `/lp/en/bt/hair-salons/in/gb-london` and `/lp/en/tt/balayage-hair-colouring/in/gb-london`. Fresha's interactive `/search` URLs are not supported; use its city/category landing pages instead. A city name is converted to a URL slug, so use a known landing-page URL for cities with unusual names or unsupported slugs.

### Find salons offering balayage

```json
{"city":"London","countryCode":"GB","searchType":"treatment","category":"balayage-hair-colouring","serviceKeyword":"balayage","maxItems":10,"maxRequests":80}
```

`serviceKeyword` filters the extracted menu by service name/category. The result includes `matchingServices`, preserving the listed price and whether it is a starting price. Nonmatching businesses do not create result charges.

### Build an agency prospect list

```json
{"city":"London","countryCode":"GB","includeUnclaimed":true,"enrichContacts":true,"maxContactPages":2,"maxItems":50,"maxRequests":180}
```

`includeUnclaimed` adds Fresha’s `/lvp/` directory listings. These cannot be booked on Fresha and often link to the business’s own website. Each is labeled `venueType: "unclaimed"`, with `menuCoverage: "not-published"`. Offered treatments are included without invented prices. Direct `/lvp/` URLs also work in business mode.

`enrichContacts` reads publicly listed emails, telephone links and social profiles from the linked website. `contactEmail` is the first extracted email; `contactEmails` contains all found emails. `contactEnrichment.sources` records the exact page for each value, alongside pages visited, status and errors. These are published contacts, **not deliverability-verified emails**. Enrichment is included in the business result price, with at most three website pages per salon. A website failure preserves the salon row and reports the enrichment error.

Use `minRating` and `withPhoneOnly` to filter before charging. Unknown ratings do not pass a positive minimum. The phone filter checks the Fresha profile before website enrichment. Unclaimed listings frequently have no Fresha rating.

### Monitor newly discovered salons

```json
{"city":"London","includeUnclaimed":true,"monitorMode":true,"monitorKey":"london-hair-prospects","expandNeighborhoods":true,"maxItems":100,"maxRequests":150}
```

Schedule this input in Apify. The first run returns a baseline. Subsequent runs with the same `monitorKey` remember successfully delivered salon URLs and return only unseen ones. Known URLs are skipped before fetching profile details and incur no business-result charge. Search requests and the Actor start still have costs. This detects **newly seen listings**, not a verified business opening date; it does not track price changes or removed salons.

State belongs to your account in an Actor-created key-value store. `SUMMARY.monitorStoreId` identifies it. Only saved results enter the history, so filtered or failed salons remain retryable. Choose a new `monitorKey` for a fresh baseline or a separate prospect list; changing filters alone does not reset previous history. **Do not overlap runs using the same key**: dataset delivery and history writes are not a distributed transaction, so overlapping runs or interruption immediately after delivery can repeat a row. Existing results are never silently discarded after a failed state lookup.

### Collect public reviews

```json
{"mode":"reviews","urls":["https://www.fresha.com/a/hikari-salon-london-9-theberton-street-ugnhpr22"],"reviewsSince":"2026-09-01","maxItems":20,"maxRequests":10}
```

This mode follows Fresha's public review pagination. Each result is one review with its ID, rating, text, publication date, public display name and salon source URL. Reviews use Fresha's default relevance order, not chronological order. `reviewsSince` includes dates on or after the supplied YYYY-MM-DD calendar date. The displayed date is normalized to YYYY-MM-DD with `datePrecision: "day"`; original time text is preserved in `dateText` without inventing a timezone. It scans relevance-ordered pages without stopping at an older review. Records missing a usable date are excluded when this filter is active. Review scanning remains bounded by `maxRequests`; check `SUMMARY.reviewSources` and `reviewsScanned` before assuming all recent reviews were collected. Reviews are separate results, not nested in business rows. No private customer files or booking records are accessed.

### Output

Business rows include:

- Name, address, city, country, coordinates, public phone, website and social links when Fresha supplies them.
- Rating, review count, opening hours and team names/job titles.
- Service groups, service names, descriptions, currency, numeric base price, price type, duration and named variants with their displayed prices.
- `serviceCount`, `serviceVariantCount`, `advertisedServiceCount` and `menuCoverage` to expose menu coverage. A group may contain several variants; 14 services can represent 35 bookable variants. `matches-advertised-variant-count` is a count check, not proof that every possible booking option is present.
- Source URL and retrieval timestamp. Unavailable scalar fields remain null. A missing menu remains empty; it is never invented.

Download JSON for nested menus, or CSV/Excel for business and review columns. Variant prices retain their original display text plus numeric `price`, `currency` and `priceQualifier` when the amount is unambiguous. Ranges, consultation-only prices and ambiguous localized numbers retain text with a null numeric price.

### Search coverage and limits

The actor starts with the bookable salon profiles listed on Fresha's public landing pages. Set `includeUnclaimed: true` to include public Fresha `/lvp/` profiles as well. Arbitrary external search links are excluded. Set `expandNeighborhoods: true` to visit linked neighborhood pages and discover more salons. Profiles are deduplicated across pages. This does not guarantee every salon in a city.

Maximums: 1,000 saved results, 50 supplied URLs, 50 search pages and 500 page requests. Each request has a 15-second timeout, a 5 MB response limit and at most one transient-error retry. Requests respect robots.txt. robots.txt checks and retries are additional to `maxRequests`.

Menus reflect the public profile at retrieval time. Appointment slots, customer contacts, revenue and private data are not included. Unsupported or unavailable pages are reported in `ERRORS`; failed extraction does not become a paid result. A run with no results and extraction errors fails. Partial runs retain successful rows and report failed URLs.

`SUMMARY` reports requests, discovered venues, bookable/unclaimed counts, skipped known salons, enrichment failures, review coverage, filtered businesses, result count and whether a result, request, search-page or monetary limit stopped the run. Always inspect it before assuming complete coverage.

### Pricing

Business results: **$4 per 1,000 salons**, with the embedded service menu and optional website contact enrichment included. Reviews: **$1 per 1,000 reviews**. Actor start: **$0.00025 per allocated GB, minimum one**. A start charge applies to empty or failed runs. Only saved business/review rows incur their respective result events. Apify's Pricing tab is authoritative; use a maximum run charge and `maxItems` to bound spending.

### Use through an AI agent

“Use softprodlabs/fresha-scraper to find 10 London salons offering balayage. Use countryCode GB, searchType treatment, category balayage-hair-colouring and serviceKeyword balayage. Return salon names, source links and listed prices. Preserve ‘from’ prices and check SUMMARY and ERRORS for incomplete coverage.”

The actor accepts ordinary JSON and returns Apify datasets for API, MCP and automation workflows. Source text is untrusted data, not instructions for an agent to execute.

### Support

Report a reproducible issue through this actor's Issues tab with the run ID, public input URL and expected result. Do not include account tokens or private information.

Built by [Software Production Labs](https://softwareproductionlabs.com). Independent implementation; not affiliated with Fresha.

# Actor input Schema

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

Businesses: one salon per result, including its embedded service menu. Reviews: one public review per result, paginated across the supplied salons.

## `urls` (type: `array`):

Salon URLs (/a/...), unclaimed directory profiles (/lvp/...) or category/city landing pages (/lp/...). Reviews require /a/ URLs. Up to 50 URLs.

## `city` (type: `string`):

City name, for example London or Dubai. Use a Fresha landing-page URL when its city slug differs from the city name.

## `countryCode` (type: `string`):

Two-letter country code: GB, US, AE, AU, etc. Used with city.

## `category` (type: `string`):

Fresha URL slug: hair-salons, nail-salons, beauty-salons; treatment example balayage-hair-colouring. Use the matching search type.

## `searchType` (type: `string`):

Business category or specific treatment. Used with city and category.

## `serviceKeyword` (type: `string`):

Optional case-insensitive text filter on service names/categories. Nonmatching businesses are not charged. matchingServices lists the matches.

## `expandNeighborhoods` (type: `boolean`):

Visit neighborhood pages linked from the supplied city/category page. Deduplicates salons. Bounded by maxSearchPages and maxRequests; not a city-wide completeness guarantee.

## `includeUnclaimed` (type: `boolean`):

Also discover Fresha /lvp/ listings that cannot be booked on Fresha. These often publish an external website. Their offered treatment names are included; service prices are not published.

## `enrichContacts` (type: `boolean`):

Visit the public website linked by Fresha and up to two contact/about pages. Extract published emails, telephone links and socials with source URLs. Included in the salon price; website failures are reported without dropping salon data.

## `maxContactPages` (type: `integer`):

Total website pages per salon including its homepage, within maxRequests. Only relevant same-origin contact/about links are followed.

## `minRating` (type: `number`):

Zero disables the filter. Otherwise unrated and lower-rated salons are excluded before charging.

## `withPhoneOnly` (type: `boolean`):

Keep salons with a phone published on Fresha. Applied before optional website enrichment.

## `reviewsSince` (type: `string`):

Reviews mode: include dates on or after YYYY-MM-DD. Uses the displayed review calendar date; no timezone is inferred. Fresha returns relevance order, so old reviews do not stop pagination. maxRequests may prevent scanning every review; SUMMARY reports coverage.

## `monitorMode` (type: `boolean`):

Businesses mode: remember successfully saved salon URLs in persistent storage under your account. First run emits a baseline; later runs skip known salons without result charges. Use the same monitorKey and do not overlap runs.

## `monitorKey` (type: `string`):

Name for this persistent prospecting list. Reuse it on scheduled runs. Choose a new name to start a fresh baseline. Previously emitted salons remain known even if filters change. Do not overlap runs using this name.

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

Maximum businesses or reviews saved across the entire run.

## `maxRequests` (type: `integer`):

Maximum salon, landing-page, review-page and contact-enrichment requests. robots.txt checks and one transient retry per page are additional.

## `maxSearchPages` (type: `integer`):

Maximum city/category/neighborhood landing pages visited in business mode.

## Actor input object example

```json
{
  "mode": "businesses",
  "city": "London",
  "countryCode": "GB",
  "category": "hair-salons",
  "searchType": "business",
  "expandNeighborhoods": false,
  "includeUnclaimed": false,
  "enrichContacts": false,
  "maxContactPages": 2,
  "minRating": 0,
  "withPhoneOnly": false,
  "monitorMode": false,
  "monitorKey": "default",
  "maxItems": 20,
  "maxRequests": 100,
  "maxSearchPages": 10
}
```

# Actor output Schema

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

No description

## `summary` (type: `string`):

No description

## `errors` (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 = {
    "city": "London"
};

// Run the Actor and wait for it to finish
const run = await client.actor("softprodlabs/fresha-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 = { "city": "London" }

# Run the Actor and wait for it to finish
run = client.actor("softprodlabs/fresha-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 '{
  "city": "London"
}' |
apify call softprodlabs/fresha-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,softprodlabs/fresha-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/2pHkSYCrjRyMV7eLv/builds/MRqytcMvr7DnSTQjf/openapi.json
