# Preply Scraper \[$1.90/1K💰] | Tutors + Reviews (`ahmed_jasarevic/preply-scraper`) Actor

Extract Preply tutor profiles and reviews via Preply own GraphQL API - hourly price, rating, lessons taught, languages, education, certificates and full review text. No browser, no residential proxy, no Cloudflare fight.

- **URL**: https://apify.com/ahmed_jasarevic/preply-scraper.md
- **Developed by:** [Ahmed Jasarevic](https://apify.com/ahmed_jasarevic) (community)
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 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/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

## Preply Scraper

Extract Preply tutor profiles and student review text into two clean datasets — hourly rate, rating, lessons taught, languages, education, certificates and full review text — using Preply's own GraphQL endpoint, with no browser and no residential proxy.

- **Reviews as a dataset, not a buried string.** Most Preply tools expose profile fields only. This one writes one row per review into a separate `reviews` dataset, joinable to tutors on `tutorId`.
- **No browser, no residential proxy.** Talks to `https://preply.com/graphql/v2/` directly, so there is no Cloudflare browser challenge to pay for or wait out.
- **Cheapest verified Preply tutor dataset** on the Apify Store at `$1.90` per 1,000 tutors on the FREE tier, against `$3.00` per 1,000 for the closest verified alternative.

### Main Use Cases

- **Preply tutor database** — build a structured catalogue of tutors by subject, language and price band
- **Hourly tutor rate benchmarking** — compare `hourlyPrice` and `trialLessonPrice` across hundreds of tutors to map the market
- **Tutor review analysis** — collect full review text with scores, dates and tutor replies for sentiment and quality research
- **Online tutor marketplace research** — study supply, pricing and quality to plan a competing marketplace
- **Language tutor lead lists** — filter by certified tutors, native speakers, country and language to build a qualified prospect pool
- **Tutor credential verification** — extract `educations`, `certificates` and `isCertified` / `isSuperTutor` flags

### How It Works

1. In `search` mode, the Actor pages Preply's tutor directory for a subject slug and language, following `startPage` up to `maxPages`.
2. Each listing is parsed into a flat `TutorRecord` with `detailLevel: "listing"`.
3. For each tutor the Actor optionally calls the `tutor(id)` profile query, which fills in the richer fields — `educations`, `certificates`, `workExperiences`, `reviewDistribution`, `responseTimeHours`, `videoIntroUrl` — producing `detailLevel: "profile"`.
4. When `includeReviews` is on, each review is parsed into a `ReviewRecord` and pushed to the `reviews` dataset, carrying `tutorId` as the join key.

Requests go straight to Preply's GraphQL endpoint over plain HTTPS.

### Extract Preply Tutor Data Without a Browser or Residential Proxy

Preply sits behind Cloudflare, which is why most scrapers for it need a headless browser and a residential proxy — and why the cheapest options still cost the most per result. This Actor queries Preply's own GraphQL API instead. No browser, no proxy, no challenge to solve, and a lower per-result price than the browser-based alternatives.

Reviews are deliberately kept in a separate dataset. A tutor with 400 reviews still produces exactly one tutor row plus 400 review rows, so no single row ever carries an unbounded nested array.

### Input

| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `mode` | string | no | `search` | `search` walks the tutor directory. `profiles` fetches only the URLs/IDs you supply. |
| `subjectSlug` | string | no | `english` | Subject/category slug, e.g. `english`, `mathematics`, `programming`. Used in `search` mode. |
| `languageCode` | string | no | `en` | Site language prefix. |
| `startPage` | integer | no | `1` | First directory page to read. Raise this to resume deeper pagination. |
| `maxPages` | integer | no | `2` | Maximum directory pages to walk. Default is deliberately small — raise it for real collection. |
| `sort` | string | no | `rating` | One of `rating`, `reviews`, `popularity`, `price_lowest`, `price_highest`. |
| `priceRange` | string | no | — | Price range filter, passed through to the directory query. |
| `tutorCountry` | string | no | — | Restrict tutors to a country of birth. |
| `searchText` | string | no | — | Free-text keyword filter on the directory listing. |
| `certifiedOnly` | boolean | no | `false` | Only professional/certified tutors. |
| `nativeSpeakersOnly` | boolean | no | `false` | Only native speakers of the selected language. |
| `alsoSpeaks` | string | no | — | Require an additional spoken language. |
| `startUrls` | array | no | — | Direct tutor profile URLs. Used in `profiles` mode. |
| `tutorIds` | array | no | — | Direct tutor IDs. Used in `profiles` mode. |
| `includeReviews` | boolean | no | `false` | Also fetch reviews into the `reviews` dataset. Costs extra requests per tutor. |
| `maxItems` | integer | no | `20` | Maximum tutors to collect. |
| `proxy` | object | no | — | Proxy configuration. Not needed for the default GraphQL path. |

### Output

Two datasets are written.

**Default dataset — tutors** (one row per tutor):

`tutorId`, `name`, `subjects`, `subjectSlugs`, `hourlyPrice`, `currency`, `trialLessonPrice`, `rating`, `reviewsCount`, `lessonsTaught`, `languages` (`name`, `code`, `level`), `bio`, `headline`, `availability` (`hasAvailability`, `activeStudents`, `lessonsBookedLast48h`), `profileUrl`, `country`, `countryCode`, `avatarUrl`, `lastSeen`, `isOnline`, `yearsOfExperience`, `preplyTenureYears`, `responseTimeHours`, `isCertified`, `isSuperTutor`, `verifiedTutor`, `videoIntroUrl`, `tags`, `metadataTags`, `educations`, `certificates`, `workExperiences`, `reviewDistribution`, `sourceUrl`, `detailLevel`, `scrapedAt`

`detailLevel` tells you how complete a row is: `"listing"` (directory-level fields only) or `"profile"` (the full profile query ran).

**`reviews` dataset** (one row per review):

`reviewId`, `tutorId`, `tutorName`, `tutorProfileUrl`, `score`, `text`, `createdAt`, `language`, `isEdited`, `verifiedByCs`, `relevanceScore`, `reviewerUserId`, `reviewerLessons`, `reviewerLastLessonAt`, `reviewerLearningGoal`, `tutorReplies` (`text`, `createdAt`, `language`), `sourceUrl`, `scrapedAt`

Join the two datasets on `tutorId`.

### Example Input

```json
{
  "mode": "search",
  "subjectSlug": "english",
  "languageCode": "en",
  "sort": "rating",
  "certifiedOnly": true,
  "startPage": 1,
  "maxPages": 5,
  "maxItems": 100,
  "includeReviews": true
}
```

### Example Output

```json
{
  "tutorId": "184023",
  "name": "Amelia Hart",
  "subjects": ["English - Business English", "English - Conversation"],
  "subjectSlugs": ["business-english", "conversation"],
  "hourlyPrice": 27,
  "currency": "USD",
  "trialLessonPrice": 14,
  "rating": 4.98,
  "reviewsCount": 412,
  "lessonsTaught": 3180,
  "languages": [
    { "name": "English", "code": "en", "level": "Native" }
  ],
  "bio": "Cambridge-certified English teacher with 9 years of experience teaching professionals.",
  "headline": "Business English that gets results",
  "availability": {
    "hasAvailability": true,
    "activeStudents": 6,
    "lessonsBookedLast48h": 2
  },
  "profileUrl": "https://preply.com/en/teachers/amelia-hart",
  "country": "United Kingdom",
  "countryCode": "GB",
  "isOnline": true,
  "yearsOfExperience": 9,
  "preplyTenureYears": 5,
  "responseTimeHours": 2,
  "isCertified": true,
  "isSuperTutor": true,
  "verifiedTutor": true,
  "educations": [
    { "degree": "MA English Literature", "school": "University of Cambridge", "dates": "2012-2015" }
  ],
  "certificates": [
    { "name": "CELTA", "dates": "2015" }
  ],
  "workExperiences": [
    { "position": "Corporate Trainer", "company": "Deloitte", "dates": "2017-2021" }
  ],
  "sourceUrl": "https://preply.com/en/teachers/amelia-hart",
  "detailLevel": "profile",
  "scrapedAt": "2026-02-11T09:14:22.301Z"
}
```

And one row from the `reviews` dataset for the same tutor:

```json
{
  "reviewId": "99281744",
  "tutorId": "184023",
  "tutorName": "Amelia Hart",
  "tutorProfileUrl": "https://preply.com/en/teachers/amelia-hart",
  "score": 5,
  "text": "Amelia prepared me for a client presentation in two weeks. Her feedback on my intonation was genuinely useful.",
  "createdAt": "2026-01-28T10:03:00Z",
  "language": "en",
  "isEdited": false,
  "verifiedByCs": true,
  "reviewerLessons": 24,
  "reviewerLearningGoal": "Business English",
  "tutorReplies": [
    {
      "text": "Glad it went well — good luck with the presentation!",
      "createdAt": "2026-01-28T12:41:00Z",
      "language": "en"
    }
  ],
  "sourceUrl": "https://preply.com/en/teachers/amelia-hart",
  "scrapedAt": "2026-02-11T09:14:22.301Z"
}
```

### Pricing

Pay-per-result. A tutor costs `$1.90` per 1,000 on the FREE tier. Setting `includeReviews: true` also bills for each review at `$1.00` per 1,000. Fetching a full profile costs more request budget than reading the directory listing alone, so set `maxItems` deliberately when you need profile-level fields.

### Integrations & Automation

- **Apify API** — run it from Python, Node or any HTTP client, and read the datasets directly.
- **Schedules** — refresh `lastSeen`, `activeStudents` and ratings daily; use a weekly schedule for market snapshots.
- **Webhooks** — trigger a downstream job when a run finishes.
- **MCP** — callable by AI agents through the Apify MCP server.

### Related Actors

- `ahmed_jasarevic/link-preview-api` — pull the Open Graph and JSON-LD metadata for tutor profile URLs as a cheap supplementary signal
- `ahmed_jasarevic/otta-scraper` — comparable job-listing extraction with salary and equity fields, if your research spans tutor and tech hiring markets

### FAQ

#### Why use this actor instead of Preply's own API?

Preply does publish an OpenAPI document at `https://preply.com/openapi.yaml`, but it describes the scoped plugin/partner integration surface, not the tutor directory. The directory and profile data used here come from Preply's internal GraphQL endpoint at `https://preply.com/graphql/v2/`.

#### Do I need a residential proxy?

No. This Actor calls the GraphQL endpoint directly, so the default configuration needs no proxy at all. A `proxy` field exists if you want to add one.

#### Do I get review text, or just review counts?

Full review text. Reviews are written to a separate `reviews` dataset — one row per review, including `score`, `text`, `createdAt`, `reviewerLessons`, `reviewerLearningGoal` and any `tutorReplies`.

#### What is the difference between `listing` and `profile` rows?

`detailLevel` is `"listing"` when the tutor came only from a directory page, and `"profile"` when the full profile query also ran. Profile rows add `educations`, `certificates`, `workExperiences`, `reviewDistribution`, `responseTimeHours`, `isOnline`, `preplyTenureYears` and `videoIntroUrl`. Listing rows return `null` for those fields rather than inventing values.

#### Which mode should I use — `search` or `profiles`?

`search` walks the directory for a `subjectSlug`, which is how you build a broad database. `profiles` fetches only the exact `startUrls` or `tutorIds` you pass, which is how you enrich a list you already have.

#### What are alternatives to this actor?

The main alternatives are browser-and-proxy-based Preply scrapers. They tend to cost more per result, need a residential proxy, and expose review text only if you pay extra. One further option is a narrow email-only extractor, which does not give you profile data at all.

#### Why is my `priceRange` filter ignored?

Preply's directory query does not apply the price filter server-side in all locales, so the Actor filters on price locally. Expect slightly fewer results than an unfiltered page.

### For AI Agents & LLM Apps

**Purpose:** returns Preply tutor profiles as tabular rows in the default dataset and student reviews as rows in a named `reviews` dataset, joinable on `tutorId`.

**Minimal working input** — smallest input that produces a valid run:

```json
{ "subjectSlug": "english", "maxItems": 5 }
```

**Search variant** — paginate the directory and sort by price:

```json
{
  "subjectSlug": "english",
  "languageCode": "en",
  "sort": "price_lowest",
  "startPage": 1,
  "maxPages": 5,
  "maxItems": 200
}
```

**Profiles variant** — fetch specific tutors only, with reviews:

```json
{
  "mode": "profiles",
  "tutorIds": ["184023"],
  "includeReviews": true,
  "maxItems": 5
}
```

**Output fields** — tutors: `tutorId, name, subjects, subjectSlugs, hourlyPrice, currency, trialLessonPrice, rating, reviewsCount, lessonsTaught, languages, bio, headline, availability, profileUrl, country, countryCode, isOnline, yearsOfExperience, preplyTenureYears, responseTimeHours, isCertified, isSuperTutor, verifiedTutor, educations, certificates, workExperiences, reviewDistribution, detailLevel, sourceUrl, scrapedAt`. Reviews (named dataset `reviews`): `reviewId, tutorId, tutorName, tutorProfileUrl, score, text, createdAt, language, reviewerLessons, reviewerLearningGoal, tutorReplies, sourceUrl, scrapedAt`.

**Behaviors an agent should know**

- Reviews land in a **separate named dataset**, not in the tutor row. Read both and join on `tutorId`.
- `mode` defaults to `search`. In `profiles` mode, only `startUrls` and `tutorIds` are used — `subjectSlug` and the filters are ignored.
- `maxPages` defaults to `2`, which yields very few tutors. Raise it alongside `maxItems` or you will under-collect.
- `includeReviews` adds a request per tutor **and** bills per review. Leave it off unless reviews are actually needed.
- Rows with `detailLevel: "listing"` have `null` in the profile-only fields. Treat `null` as "not fetched", not "absent".
- `priceRange` is filtered locally, so result counts can be lower than the directory page size.
- No proxy is required on the default path.

**Billing:** pay-per-result — `$1.90` per 1,000 tutors on the FREE tier, plus `$1.00` per 1,000 reviews when `includeReviews` is on.

### Legal & Compliance

This Actor is an independent tool. It is not affiliated with, endorsed by, or sponsored by Preply.

It reads only publicly accessible tutor listings, public tutor profiles, and public student reviews from Preply's own GraphQL endpoint. It does not bypass authentication, does not attempt to defeat Cloudflare challenges, and does not use CAPTCHA solving.

You are responsible for your own compliance with Preply's Terms of Service and with applicable data-protection law. The output contains personal data — tutor names, biographies, education history, work history and student review text including reviewer names and learning goals. Under GDPR and comparable regimes you need a lawful basis for processing this data, and you must handle it accordingly.

Please do not use extracted personal data for unsolicited commercial outreach in violation of applicable law, including CAN-SPAM, GDPR or TCPA.

### SEO Keywords

preply scraper, preply tutor data, preply tutor profile, preply tutor database, preply api, preply graphql api, preply reviews, preply tutor ratings, hourly tutor rate comparison, preply lesson price data, online tutor database, tutor market research, tutor review text dataset, language tutor leads, esl tutor price research, tutor certification verification, tutor subject catalog, preply alternatives, preply teacher data export, teach english online data, build a language tutor marketplace, preply tutor search

# Actor input Schema

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

How to discover tutors.

## `subjectSlug` (type: `string`):

Slug of the subject to browse, used as https://preply.com/en/online/<slug>-tutors. Language slugs: english, spanish, french, german, japanese, italian, korean, arabic, chinese, portuguese, russian, polish, turkish, ukrainian, dutch, norwegian, armenian, hebrew, greek, hindi, hungarian, finnish, georgian, swedish, czech, croatian, khmer, belarusian, sign, urdu. Category slugs: math_science, arts_music, business, technology, social_sciences, hobbies.

## `languageCode` (type: `string`):

Two-letter UI language prefix used in URLs, e.g. en, de, es, fr.

## `startPage` (type: `integer`):

First result page to fetch. The directory returns 10 tutors per page.

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

How many directory pages to walk. Each page yields up to 10 tutors.

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

Result ordering used by the Preply directory.

## `priceRange` (type: `string`):

Hourly price filter in the form "min-max", e.g. 10-30. Leave empty for no filter. Preply's API ignores its own price-range argument, so this filter is applied locally after each page is fetched.

## `tutorCountry` (type: `string`):

Filter tutors by country of birth. Comma-separated ISO-2 codes, e.g. "US,CA,GB".

## `searchText` (type: `string`):

Free-text term matched against tutor profiles, e.g. "business english" or "ielts".

## `certifiedOnly` (type: `boolean`):

Restrict results to certified/professional tutors.

## `nativeSpeakersOnly` (type: `boolean`):

Restrict results to tutors who teach their native language.

## `alsoSpeaks` (type: `string`):

Two-letter language code for the "also speaks" filter, e.g. "en".

## `startUrls` (type: `array`):

Mode "profiles": profile URLs such as https://preply.com/en/tutor/4752154. Overrides search when non-empty.

## `tutorIds` (type: `array`):

Mode "profiles": numeric tutor IDs, e.g. \["4752154"]. Combined with startUrls.

## `includeReviews` (type: `boolean`):

Also fetch each tutor's full profile and their complete review history. Costs 2 extra API requests per tutor and yields the full bio, education, certificates, work experience, availability and review text. Reviews are written to a second dataset called "reviews" (with its own table view in the Storage tab) and are billed per review as a pay-per-event charge. Tutors go to the main dataset; join the two on tutorId.

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

Hard cap on the number of tutor records written to the dataset.

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

Select proxies to be used by your crawler.

## Actor input object example

```json
{
  "mode": "search",
  "subjectSlug": "english",
  "languageCode": "en",
  "startPage": 1,
  "maxPages": 2,
  "sort": "rating",
  "certifiedOnly": false,
  "nativeSpeakersOnly": false,
  "startUrls": [],
  "tutorIds": [],
  "includeReviews": false,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `tutors` (type: `string`):

No description

## `reviews` (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",
    "subjectSlug": "english",
    "languageCode": "en",
    "startPage": 1,
    "maxPages": 2,
    "sort": "rating",
    "priceRange": "",
    "tutorCountry": "",
    "searchText": "",
    "certifiedOnly": false,
    "nativeSpeakersOnly": false,
    "alsoSpeaks": "",
    "startUrls": [],
    "tutorIds": [],
    "includeReviews": false,
    "maxItems": 20,
    "proxy": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/preply-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",
    "subjectSlug": "english",
    "languageCode": "en",
    "startPage": 1,
    "maxPages": 2,
    "sort": "rating",
    "priceRange": "",
    "tutorCountry": "",
    "searchText": "",
    "certifiedOnly": False,
    "nativeSpeakersOnly": False,
    "alsoSpeaks": "",
    "startUrls": [],
    "tutorIds": [],
    "includeReviews": False,
    "maxItems": 20,
    "proxy": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/preply-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",
  "subjectSlug": "english",
  "languageCode": "en",
  "startPage": 1,
  "maxPages": 2,
  "sort": "rating",
  "priceRange": "",
  "tutorCountry": "",
  "searchText": "",
  "certifiedOnly": false,
  "nativeSpeakersOnly": false,
  "alsoSpeaks": "",
  "startUrls": [],
  "tutorIds": [],
  "includeReviews": false,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": false
  }
}' |
apify call ahmed_jasarevic/preply-scraper --silent --output-dataset

```

## MCP server setup

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