# Fresha Scraper - Salons, Spas, Services & Reviews (`abotapi/fresha-salon-spa-scraper`) Actor

Scrape Fresha salon and spa listings: business profiles, service menus with prices and durations, opening hours, team members, photos, ratings and client reviews. Search by keyword and location or paste venue and professional links. Resume, incremental updates and MCP export included.

- **URL**: https://apify.com/abotapi/fresha-salon-spa-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 listing records

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?

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

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

Fresha Scraper turns Fresha into a clean beauty-and-wellness data API. Get salon and spa listings with ratings, addresses and photos, full service menus with prices and durations, opening hours, team members, amenities and client reviews with salon replies. Search by keyword and city, or paste venue and professional profile links, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways in.** Search Fresha by keyword and location (hair in Sydney, massage in London, barber in New York), or paste venue, professional, SEO landing-page and book-now links for exact scraping. No URL building needed.
- **Deep service menus.** Full service catalog with per-service prices, durations, variants and per-service ratings, plus package offers with included items.
- **Reviews with replies.** Client reviews carry rating, text, date, author, review photos, the service and team member involved, and the venue reply - with the full 1-5 star distribution, Fresha's own review summary, and the most-reviewed services and team members per venue.
- **Lead-gen ready.** Amenities, social links, website, phone, geo coordinates, servicing areas, open/closed status and a derived 0-100 lead score per listing.
- **Built for schedules.** Incremental mode returns only new and changed listings on recurring runs, and a refused run fails loudly instead of returning an empty dataset.

### Use Cases

- **Salon and spa lead generation:** collect venues by city and service with phone numbers, addresses and contact details for outreach.
- **Market research and franchising:** map competitor density, pricing ranges, opening hours and service breadth across cities.
- **Review intelligence:** track ratings, star distributions, review trends and how venues reply to feedback.
- **Price benchmarking:** compare service prices and durations across venues and markets.
- **AI agents and dashboards:** feed structured listing and review data into your own tools.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `recordType` | `"venue"` (also `"professional"`) |
| `name` | `"Sample Hair Studio"` |
| `url` | Fresha link to the venue or professional profile |
| `venueType` | `"Hair Salon"` |
| `rating` / `reviewsCount` | `5.0` / `625` |
| `ratingLevel` / `ratingTitle` | `"HIGHLY_RECOMMENDED"` / `"Highly Recommended"` |
| `reviewRating1Total` ... `reviewRating5Total` | `0` / `0` / `1` / `5` / `619` |
| `city` / `state` / `country` / `postalCode` | `"Sydney"` / `"New South Wales"` / `"AU"` / `"2011"` |
| `street` / `fullAddress` / `mapsUrl` | `"24-30 Sample Avenue"` / full formatted address / Google Maps link |
| `latitude` / `longitude` | `-33.8688` / `151.2093` |
| `contactNumber` | `"+61 400 000 000"` |
| `serviceCount` / `serviceCategoryCount` | `55` / `9` |
| `services` | name, category, price, currency, duration, variants, per-service rating |
| `packages` / `hasPackages` | package offers with included items / `true` |
| `servicePriceMin` / `servicePriceMax` / `priceRange` | `15` / `455` / `"AUD 15 - 455"` |
| `workingHours` / `openingStatus` | per-day hours / `"Closed"` at scrape time |
| `team` / `teamSize` | name, job title, rating, avatar, profile link / `4` |
| `amenities` | `"Pet-friendly"`, `"Wi-Fi"`, `"Wheelchair accessible"` |
| `images` / `portfolioImages` | venue photo URLs / work portfolio photo URLs |
| `instagramUsername` / `socialLinks` / `website` / `facebookUrl` | `"samplehairstudio"` / external links / venue website / Facebook page |
| `hasGiftCards` / `hasVouchers` / `hasMemberships` / `hasFreshaPay` / `hasProductStore` | `true` / `false` / `true` / `true` / product store |
| `reviewSummaryText` / `recentReviewers` | Fresha's summary of what clients praise / recent reviewer names |
| `reviews` / `topReviewedServices` / `reviewTeamMembers` | rating, text, date, author, reply, photos, service and team context (when enabled) / most-reviewed services and team |
| `leadScore` | `87.5` (derived 0-100 score) |
| `searchQuery` / `searchLocation` / `searchSort` / `availabilityDate` / `discoveredFromUrl` | search context of the row / availability filter / harvest source (URL mode) |

Professional records add `headline`, `isVerified`, `appointmentsCompleted`, `clientsServed`, `favouritedBy`, `memberSince`, `languages`, `workingLocations` and `portfolioImages`.

### How to Use

1. Pick a **mode**: `Search by keyword & location` or `Scrape listing URLs`.
2. For search, fill in the keyword and city; for URLs, paste Fresha links - or leave the shipped example links in place for a first test run.
3. Switch on **Full profile enrichment** and **Collect client reviews** as needed.
4. Set **Max items** to control run size and cost, then click **Start**.
5. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword:**

```json
{
  "mode": "SEARCH",
  "searchQuery": "hair",
  "searchLocation": "Sydney",
  "maxItems": 20
}
```

**Highest-rated massage venues only:**

```json
{
  "mode": "SEARCH",
  "searchQuery": "massage",
  "searchLocation": "London",
  "sort": "RATING",
  "freshaVerifiedOnly": true,
  "maxItems": 30
}
```

**Paste links (venue pages, professional profiles, SEO landing pages and book-now links):**

```json
{
  "mode": "LISTING_URLS",
  "listingUrls": [
    { "url": "https://www.fresha.com/a/sample-hair-studio-sydney-example123" },
    { "url": "https://www.fresha.com/p/sample-stylist-1234567" },
    { "url": "https://www.fresha.com/lp/en/bt/spas/in/au-sydney" }
  ],
  "fetchDetails": true,
  "fetchReviews": true
}
```

SEO landing pages (`/lp/...`) are harvested for their venue links; book-now links resolve to their venue automatically.

#### URL mode: supported link shapes

Links of every supported shape can be mixed freely in `listingUrls`:

| Shape | Example | Behavior |
| --- | --- | --- |
| Venue page | `https://www.fresha.com/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f` | Full venue record. |
| Venue link with tracking parameters | same venue link + `?utm_source=...` | Parameters are ignored; matched as the same venue. |
| Professional profile | `https://www.fresha.com/p/suha-alsayah-5784524` | Professional record. |
| SEO landing page | `https://www.fresha.com/lp/en/bt/spas/in/au-sydney` | Harvested for the roughly 60 venue links it contains; badge placeholders filtered out; rows record the harvest source in `discoveredFromUrl`. |
| Localized venue link (short form) | `https://www.fresha.com/de/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f` | Parsed like the plain venue link. Region-qualified forms such as `/de-DE/a/...` also parse, but the site does not serve those pages, so use the short locale form. |
| Book-now link | `https://www.fresha.com/book-now/...` | Resolves to its venue through the page's own embedded data. |
| Anything else (e.g. the home page) | - | Ignored. If nothing in the list matches a supported shape, the run logs a loud warning and ends cleanly with 0 records. |

If you switch to URL mode and leave the list untouched, the shipped example links run - one per supported shape - so a first run works without editing anything.

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/fresha-salon-spa-scraper").call(run_input={"mode": "SEARCH", "searchQuery": "hair", "searchLocation": "Sydney", "maxItems": 10})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["name"], item["rating"], item["reviewsCount"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/fresha-salon-spa-scraper').call({ mode: 'SEARCH', searchQuery: 'hair', searchLocation: 'Sydney', maxItems: 10 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### How `maxItems` shapes a search

Search results come in pages of 20 listings, most relevant first (or by the sort you picked). A run stops once `maxItems` is reached, so a capped run returns the strongest matches for the keyword. To collect a whole city, raise `maxItems` or set it to `0`, and optionally cap pages with `maxPages`.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same scope. Each listing is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` (a listing previously marked `EXPIRED` that is found again, keeping its original `firstSeenAt`) or `EXPIRED` (only with `emitExpired`, and never billed past your `maxItems`). What counts as a change: every emitted field except a built-in ignore set and run bookkeeping (`scrapedAt`, `changeType`, `changedFields`, `firstSeenAt`, `lastSeenAt`) - e.g. the service menu, rating, review count, address and photos. Never compared, so they never mark a listing updated on their own: portfolio image galleries, recent reviewers, open/closed status and hours, free-running counters, and review summaries (top-reviewed services and team members); review order is ignored too - reviews compare as a set. Extend the ignore set with `ignoreFieldsForChanges`. `stateKey` names or shares the stored state. With incremental mode off, output is exactly as before.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. Notion gets a rich page-per-item export; other connectors get a best-effort write. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

At most `maxNotifyListings` items (default 50, up to 1,000) are written to each connector per run; this cap never affects the dataset. The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `SEARCH` | `SEARCH` by keyword and location, or `LISTING_URLS` for pasted links. |
| `searchQuery` | string | `hair` | Service or business keyword (search mode). |
| `searchLocation` | string | `Sydney` | City or area, resolved to map coordinates automatically. |
| `latitude` | string | (none) | Optional map latitude; with `longitude` overrides the resolved location. |
| `longitude` | string | (none) | Optional map longitude; with `latitude` overrides the resolved location. |
| `maxPages` | integer | unlimited | Result pages (20 listings each) per search; blank or `0` = unlimited. |
| `sort` | string | `RECOMMENDED` | `RECOMMENDED`, `RATING` or `DISTANCE`. |
| `minPrice` | integer | (none) | Minimum service price filter in the local currency. |
| `maxPrice` | integer | (none) | Maximum service price filter in the local currency. |
| `hasDeals` | boolean | `false` | Only venues currently running deals. |
| `hasGroupAppointments` | boolean | `false` | Only venues offering group appointments. |
| `freshaVerifiedOnly` | boolean | `false` | Only Fresha Verified venues. |
| `availabilityDate` | string | (none) | Only venues with bookable availability on this date (`YYYY-MM-DD`). |
| `listingUrls` | array | sample links | Fresha venue, professional, landing-page and book-now URLs (URL mode). Ships with one example link per supported shape; replace them with your own or leave them for a test run. |
| `fetchDetails` | boolean | `true` | Full profile: description, phone, hours, services, team, photos, amenities. |
| `fetchReviews` | boolean | `true` | Client reviews with replies, photos and the star distribution. |
| `maxReviewsPerListing` | integer | `20` | Stop collecting reviews per listing after this many; `0` = no limit. |
| `reviewSorting` | string | `LATEST` | `LATEST`, `BEST`, `WORST` or `RELEVANCE`. |
| `maxItems` | integer | `20` | Stop after this many listings (`0` = no limit). |
| `proxy` | object | Apify datacenter | Connection settings (see Plan Requirement). |
| `resumeFromRunId` | string | (none) | Continue one interrupted run. |
| `incrementalMode` | boolean | `false` | Return only new and changed listings on scheduled runs. |
| `stateKey` | string | (none) | Name or share an incremental-mode state. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged listings. |
| `emitExpired` | boolean | `false` | Also return (and bill) expired listings. |
| `ignoreFieldsForChanges` | array | (none) | Extra fields that never mark a listing as updated. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which records are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

**Venue listing:**

```json
{
  "recordType": "venue",
  "id": "235399",
  "name": "Sample Hair Studio",
  "url": "https://www.fresha.com/a/sample-hair-studio-sydney-example123",
  "venueType": "Hair Salon",
  "rating": 5.0,
  "reviewsCount": 625,
  "ratingLevel": "HIGHLY_RECOMMENDED",
  "reviewRating5Total": 619,
  "city": "Sydney",
  "state": "New South Wales",
  "country": "AU",
  "postalCode": "2011",
  "latitude": -33.8688,
  "longitude": 151.2093,
  "contactNumber": "+61 400 000 000",
  "currency": "AUD",
  "serviceCount": 55,
  "servicePriceMin": 15,
  "servicePriceMax": 455,
  "priceRange": "AUD 15 - 455",
  "workingHours": [{ "day": "Monday", "closed": false, "hours": ["9:00 AM - 6:00 PM"] }],
  "amenities": ["Pet-friendly", "Wi-Fi"],
  "team": [{ "name": "Rachel", "jobTitle": "Director", "rating": 5.0 }],
  "services": [{ "name": "Cutting Packages", "price": 65, "currency": "AUD", "durationMin": 1800 }],
  "reviews": [{ "rating": 5, "text": "Always great", "authorName": "Kate M", "replyText": "Thank you!" }],
  "leadScore": 87.5,
  "searchQuery": "hair",
  "searchLocation": "Sydney",
  "changeType": "NEW"
}
```

The **Reviews** output tab (one row per review) and the **Full profiles** tab (enriched listings) are available as dataset views.

### Plan Requirement

The default connection (datacenter) works on every Apify plan and is the setting this actor ships with. For the most reliable results on large or frequent runs, switch the connection to **Residential** under Proxy & connection. If a connection is refused, the actor escalates automatically through backup connections, including residential ones; residential traffic is billed to your Apify plan when it engages. If every connection is refused, the run fails with a clear message instead of returning an empty result.

### FAQ

#### How much does it cost?

You pay per listing returned, with optional profile enrichment and review collection billed only when you switch them on. The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of any run.

#### Is it legal to scrape Fresha?

This actor collects only publicly available business data: listing cards, service menus, opening hours and public reviews. You are responsible for how you use it: follow Fresha's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Business names, prices and public reviews are generally facts, but photos and texts may be subject to third-party rights.

#### Can I get only new or changed listings on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated listings, and unchanged ones are not billed.

#### Why did a run return fewer listings than the search shows on the website?

The run stops at **Max items**, strongest matches first. Set it higher, or to `0`, to collect more of the city. Reviews attached to a listing do not count toward `maxItems`.

#### Why did my run fail instead of returning an empty dataset?

If not a single page could be read during the run - every connection was refused or the data source did not answer - the run fails with a message describing the connection problem, so "no results" is never confused with "nothing could be read". On a free plan this can mean residential connections were unavailable; the failure message says which case applies and what to do. Run it again in a few minutes. If at least some pages were read, you get what was found: some listings, or - if the pages were read but none matched - an honest empty result.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 💬 Support & custom scrapers

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/fresha-salon-spa-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com> or message [Telegram @abotapi](https://t.me/abotapi).
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

SEARCH scrapes Fresha listings for a keyword and location. LISTING_URLS scrapes the exact venue pages (fresha.com/a/...) and professional profiles (fresha.com/p/...) you paste.

## `searchQuery` (type: `string`):

Service or business keyword, e.g. hair, massage, nails, barber, facial, tattoo. Used in SEARCH mode.

## `searchLocation` (type: `string`):

City or area name, e.g. Sydney, London, New York. Resolved automatically to map coordinates. Leave blank when latitude/longitude or a venue URL list is provided.

## `latitude` (type: `string`):

Optional map latitude. When set together with longitude it overrides the resolved Location.

## `longitude` (type: `string`):

Optional map longitude. When set together with latitude it overrides the resolved Location.

## `maxPages` (type: `integer`):

How many result pages (20 listings each) to walk per search. Leave blank or 0 for unlimited until Max items is reached.

## `sort` (type: `string`):

Ordering of search results. RATING and DISTANCE are narrowed assertions: results come back in that order.

## `minPrice` (type: `integer`):

Optional minimum service price filter in the local currency of the search area.

## `maxPrice` (type: `integer`):

Optional maximum service price filter in the local currency of the search area.

## `hasDeals` (type: `boolean`):

Return only venues currently running deals or offers.

## `hasGroupAppointments` (type: `boolean`):

Return only venues offering group appointments or classes.

## `freshaVerifiedOnly` (type: `boolean`):

Return only venues carrying the Fresha Verified badge.

## `availabilityDate` (type: `string`):

Only venues with bookable availability on this date (YYYY-MM-DD). Used in SEARCH mode.

## `listingUrls` (type: `array`):

Fresha URLs to scrape in LISTING_URLS mode. Supported shapes, which can be mixed freely: venue pages (fresha.com/a/...), professional profiles (fresha.com/p/...), SEO landing pages (fresha.com/lp/..., harvested for the venue links they contain), localized venue links in the short locale form (e.g. fresha.com/de/a/...), and venue links carrying tracking parameters (?utm_source=..., fbclid=... — matched the same as clean venue links). Book-now links (fresha.com/book-now/...) also resolve to their venue when encountered. The URL mode is selected by Mode above.

## `fetchDetails` (type: `boolean`):

Fetch each listing's full profile: description, contact number, opening hours, service menu with prices and durations, packages, team members, photo gallery, Instagram, amenities and review highlights. Adds the detail-enrichment charge per listing. Without it you still get the listing card: name, rating, review count, badges, address, geo and photos.

## `fetchReviews` (type: `boolean`):

Attach client reviews to each listing: rating, text, date, author, salon reply and review photos, plus the 1-5 star distribution. Adds the review-enrichment charge per listing. First page is included; deeper pages are walked automatically.

## `maxReviewsPerListing` (type: `integer`):

Stop collecting reviews for a listing after this many. 0 means no limit (walk every available page).

## `reviewSorting` (type: `string`):

Order in which reviews are collected.

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

Stop after this many listing records (reviews attached to a record do not count as items). Run stops gracefully at the cap.

## `proxy` (type: `object`):

Datacenter proxies are the default and work on every plan (measured working on every endpoint of this site). Residential proxies are recommended for the most reliable results on large or frequent runs. If the connection is refused, the actor escalates automatically through backup connections, including residential ones; that residential traffic is billed to your Apify plan when it engages. The run always uses a proxy unless you explicitly turn it off here or supply your own proxy URLs.

## `resumeFromRunId` (type: `string`):

ID of an interrupted run to continue. The run picks up after the last listing already saved and reuses its key-value state. Leave empty for a fresh run.

## `incrementalMode` (type: `boolean`):

Compare listings against the previous run for the same scope (state key) and only emit NEW or UPDATED rows (plus optional EXPIRED tombstones). Unchanged listings are skipped and not charged. A listing previously marked EXPIRED that is found again is emitted as REAPPEARED with its original firstSeenAt. What drives UPDATED: every emitted listing field except the exclusions below and run bookkeeping (scrapedAt, changeType, changedFields, firstSeenAt, lastSeenAt) - e.g. the services menu (names, prices, durations), rating, reviewsCount, address and coordinates, images, amenities, social links. Never compared, so they never mark a row UPDATED on their own (they rotate or change on their own without the listing changing): portfolioImages, recentReviewers, openingStatus, openingStatusDetails, closed, workingHours, instagramMediaCount, groupingScore, topReviewedServices, reviewTeamMembers. Review order is ignored too: reviews are compared as an orderless set of id, rating, text, date and reply. Extend the exclusions with ignoreFieldsForChanges.

## `stateKey` (type: `string`):

Name of the remembered baseline for incremental mode. Defaults to a hash of the current scope (mode, query, location, URL set), so two runs with the same settings compare against each other automatically. Change it to track a different baseline.

## `emitUnchanged` (type: `boolean`):

In incremental mode, also emit rows classified UNCHANGED (handy for full snapshots or debugging). Unchanged rows still carry changeType so you can filter.

## `emitExpired` (type: `boolean`):

In incremental mode, emit an EXPIRED row for listings present in the baseline but missing from this run. Tombstones respect Max items.

## `ignoreFieldsForChanges` (type: `array`):

Extra dataset fields that must NOT mark a record UPDATED when they change (rotation noise like image CDNs). Sensible defaults are built in; anything added here is appended.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify → Settings → API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "SEARCH",
  "searchQuery": "hair",
  "searchLocation": "Sydney",
  "sort": "RECOMMENDED",
  "hasDeals": false,
  "hasGroupAppointments": false,
  "freshaVerifiedOnly": false,
  "listingUrls": [
    {
      "url": "https://www.fresha.com/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f"
    },
    {
      "url": "https://www.fresha.com/p/suha-alsayah-5784524"
    },
    {
      "url": "https://www.fresha.com/lp/en/bt/spas/in/au-sydney"
    },
    {
      "url": "https://www.fresha.com/de/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f"
    }
  ],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerListing": 20,
  "reviewSorting": "LATEST",
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "mcpConnectors": [],
  "notionParentPageUrl": "",
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (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 = {
    "mode": "SEARCH",
    "searchQuery": "hair",
    "searchLocation": "Sydney",
    "listingUrls": [
        {
            "url": "https://www.fresha.com/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f"
        },
        {
            "url": "https://www.fresha.com/p/suha-alsayah-5784524"
        },
        {
            "url": "https://www.fresha.com/lp/en/bt/spas/in/au-sydney"
        },
        {
            "url": "https://www.fresha.com/de/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f"
        }
    ],
    "fetchDetails": true,
    "fetchReviews": true,
    "maxReviewsPerListing": 20,
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/fresha-salon-spa-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": "SEARCH",
    "searchQuery": "hair",
    "searchLocation": "Sydney",
    "listingUrls": [
        { "url": "https://www.fresha.com/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f" },
        { "url": "https://www.fresha.com/p/suha-alsayah-5784524" },
        { "url": "https://www.fresha.com/lp/en/bt/spas/in/au-sydney" },
        { "url": "https://www.fresha.com/de/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f" },
    ],
    "fetchDetails": True,
    "fetchReviews": True,
    "maxReviewsPerListing": 20,
    "maxItems": 20,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/fresha-salon-spa-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": "SEARCH",
  "searchQuery": "hair",
  "searchLocation": "Sydney",
  "listingUrls": [
    {
      "url": "https://www.fresha.com/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f"
    },
    {
      "url": "https://www.fresha.com/p/suha-alsayah-5784524"
    },
    {
      "url": "https://www.fresha.com/lp/en/bt/spas/in/au-sydney"
    },
    {
      "url": "https://www.fresha.com/de/a/rachel-french-hairdresser-sydney-24-30-springfield-avenue-u0by2m6f"
    }
  ],
  "fetchDetails": true,
  "fetchReviews": true,
  "maxReviewsPerListing": 20,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/fresha-salon-spa-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/fresha-salon-spa-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/3IvA43rK4vhaTOdfR/builds/Qnx6qg1aTLqoFj8U0/openapi.json
