# Malt Fr Scraper (`ahmed_jasarevic/malt-fr-scraper`) Actor

Scrape freelance profiles from the Malt directory (malt.fr, malt.de, malt.es): name, headline, skills, day rate, location, rating, missions and recommendations. Search by skill, role or city. Backed by Malt's internal JSON search API.

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

## Pricing

from $2.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

## Malt Freelancer Scraper

Scrape freelancer profiles from the Malt directory — [malt.fr](https://www.malt.fr) (France), [malt.de](https://www.malt.de) (Germany) and [malt.es](https://www.malt.es) (Spain). Get names, headlines, skills, day rates (TJM), location, ratings, missions, availability and profile URLs as clean, structured JSON.

Malt is protected by Cloudflare. This Actor talks to Malt's **internal JSON search API** (`/search/api/profiles`) using a Chrome-impersonating HTTP client routed through [Apify Proxy's Unblocker group](https://docs.apify.com/proxy/unblocker), so it returns data without a browser, DOM scraping or a third-party anti-bot service.

### What it does

- Searches Malt's freelancer directory by keyword/role per selected country domain.
- **Expands each keyword into query variants** (`react` → `react freelance`, `react senior`, `react remote`, …). Malt returns a fixed top-24 per exact query but a *different* 24 for each query string, so variants are largely disjoint — this is the main coverage lever (a single keyword easily yields 100+ unique profiles).
- Also repeats each query to surface more of Malt's randomised per-query sample.
- Returns a flat row per freelancer, ready for CSV/Excel/Sheets export.
- Optionally enriches each profile with spoken languages, experience level, category/sub-category, description and availability — fetched in parallel.
- Deduplicates across keywords, query variants, repetitions and domains.
- Handles the platform `aborting` event so a stopped run ends quickly.

### Input

| Field                 | Type     | Description                                                             |
| --------------------- | -------- | ----------------------------------------------------------------------- |
| `searchTerms`         | string\[] | **Required.** Skills/roles to search (e.g. `react`, `data scientist`).   |
| `locations`           | string\[] | Optional `location` filter. Note: Malt's guest API currently ignores it and returns the same randomised sample. |
| `domains`             | string\[] | Which sites to search: `fr`, `de`, `es`. Default `fr`.                   |
| `maxItems`            | integer  | Max freelancers to save. Default `100`.                                 |
| `expandWithVariants`  | boolean  | Expand each keyword into query variants (the main coverage lever). Default `true`. |
| `variantModifiers`    | string\[] | Words appended to each term when expanding (e.g. `senior`, `remote`). Default: freelance, senior, expert, junior, lead, remote, consultant, indépendant. |
| `maxRepetitions`      | integer  | Times to repeat each query to surface more of Malt's randomised sample (1–30). Default `10`. Stops early once a query stops yielding new profiles. |
| `scrapeProfileDetails`| boolean  | Fetch each profile page **in parallel** for languages/experience/category/description. Billed as an extra `profile-detail` event. |
| `maxConcurrency`      | integer  | Parallel search requests (1–8). Default `5`.                            |
| `category`, `speciality`, `businessSector`, `experienceLevel`, `language` | string | Optional filters passed to Malt's search. |
| `minDayRate`, `maxDayRate` | integer | Optional day-rate range.                                          |
| `proxy`               | object   | Apify Proxy config. Defaults to the `UNBLOCKER` group.                  |

> **Coverage note.** Malt's public guest API ignores paging, sorting and `location`, and returns a fixed top-24 per exact query. The reliable way to collect many freelancers is to vary the *query string*: the Actor appends `variantModifiers` to each term (and can also repeat a query), which yields largely-disjoint result sets. Disable `expandWithVariants` if you want a strict exact-match search.

#### Example input

```json
{
    "searchTerms": ["react", "data scientist"],
    "domains": ["fr"],
    "maxItems": 100,
    "expandWithVariants": true,
    "scrapeProfileDetails": false
}
```

### Output

Each item in the default dataset contains (profile-detail fields are only present when `scrapeProfileDetails` is enabled):

`name`, `firstName`, `lastName`, `headline`, `skills`, `certifiedSkills`, `dayRate`, `dayRateCurrency`, `dayRateFormatted`, `dayRateVisibility`, `city`, `country`, `countryCode`, `locationType`, `rating`, `missionsCount`, `recommendationsCount`, `appraisalsWithRatesCount`, `badges`, `availabilityStatus`, `availabilityFrequency`, `languages`, `experienceLevel`, `category`, `subCategory`, `description`, `photoUrl`, `profileUrl`, `sourceDomain`, `searchTerm`, `searchLocation`, `scrapedAt`.

#### Example output item

```json
{
    "name": "Sébastien D.",
    "headline": "Développeur Front-End Senior | React | TypeScript",
    "skills": ["React.js", "Next.js", "TypeScript", "JavaScript"],
    "dayRate": 600,
    "dayRateCurrency": "EUR",
    "city": "Rennes",
    "country": "France",
    "countryCode": "FR",
    "rating": 5,
    "missionsCount": 4,
    "profileUrl": "https://www.malt.fr/profile/sebastienD",
    "sourceDomain": "fr",
    "searchTerm": "react",
    "scrapedAt": "2026-10-12T13:30:00.000Z"
}
```

### How it works

1. Builds the query list: each keyword, plus (by default) one query per variant modifier (e.g. `react senior`), per domain.
2. Calls `GET https://www.malt.{fr|de|es}/search/api/profiles?q=<query>` with `impit` (Chrome TLS impersonation) through Apify Proxy's `UNBLOCKER` group, retrying transient `403`/`429`/`5xx` and Cloudflare HTML challenges.
3. Repeats each query up to `maxRepetitions` times (sequentially, since concurrent draws return the same sample), stopping after three consecutive draws add nothing new.
4. Normalizes the JSON response and pushes one item per freelancer as soon as it is found.
5. Optionally fetches each public profile page and decodes the embedded Nuxt payload for extra fields.

### Pricing

This Actor supports pay-per-event (PPE) monetization. Enable it and define the events in **Apify Console → your Actor → Publication → Monetization**:

| Event                        | Charged when                                                             | Suggested price |
| ---------------------------- | ----------------------------------------------------------------------- | --------------- |
| `apify-actor-start`          | Once per run (synthetic, automatic).                                     | $0.00005        |
| `apify-default-dataset-item` | Per freelancer written to the dataset (synthetic, automatic).           | $0.001          |
| `profile-detail`             | Per profile page fetched when `scrapeProfileDetails` is enabled (custom). | $0.002          |

So the base per-freelancer price stays cheap, and the heavy full-details mode costs extra per profile. If pay-per-event is not enabled, the `Actor.charge` calls are a safe no-op. The Actor respects the run's maximum cost and stops charging/working once the limit is reached.

### Proxy & cost

Cloudflare blocks datacenter IPs and unsupported regions from Malt, so the Actor defaults to the Apify Proxy **Unblocker** group — the piece that makes requests succeed. Unblocker is billed in units (10 units per successful request) and/or residential transfer; see [Apify Proxy pricing](https://apify.com/pricing). You can point the `proxy` input at a different Apify Proxy group or turn it off if you have another working route.

Only publicly available freelancer data is collected. Use it responsibly and in line with Malt's terms of service.

# Actor input Schema

## `searchTerms` (type: `array`):

Skills, roles or keywords to search for (e.g. "react", "data scientist", "ux designer"). Each term is searched per selected domain and, by default, expanded into several query variants (see "Expand keywords into query variants"). Multiple terms are merged and deduplicated.

## `locations` (type: `array`):

Optional cities or regions to pass as a `location` filter. Note: Malt's public search API currently ignores this filter and returns the same randomised sample regardless.

## `domains` (type: `array`):

Which Malt country sites to search.

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

Maximum number of freelancer profiles to save to the dataset.

## `maxRepetitions` (type: `integer`):

Malt's guest search returns a random ~24-profile sample and ignores paging, sorting and location. Repeating the same query draws new profiles from the pool until it dries up, so this is what raises coverage for a keyword. Higher = more results (and more proxy usage) for common skills; the Actor stops early once repetitions stop yielding new profiles.

## `expandWithVariants` (type: `boolean`):

Malt returns a fixed top-24 per exact query, but a *different* 24 for each query string, so this is the main coverage lever. For every search term the Actor also runs the term combined with common qualifiers (see "Variant modifiers"). Example: "react" also searches "react freelance", "react senior", "react remote", etc. — together easily 100+ unique profiles for one keyword. Turn off for a strict, exact-match search.

## `variantModifiers` (type: `array`):

Words appended to each search term when "Expand keywords into query variants" is on. Leave empty to use the built-in list (freelance, senior, expert, junior, lead, remote, consultant, indépendant).

## `scrapeProfileDetails` (type: `boolean`):

Fetch each freelancer's profile page (in parallel) to add spoken languages, experience level, category/sub-category, description and availability. Not part of the search API, so it costs one extra request per freelancer and is billed as an additional "profile-detail" event. Leave off for bulk lists.

## `experienceLevel` (type: `string`):

Optional experience filter passed to Malt's search (e.g. "junior", "confirmed", "senior").

## `language` (type: `string`):

Optional language filter passed to Malt's search (ISO code, e.g. "en", "fr").

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

Optional category filter passed to Malt's search (e.g. "tech", "data").

## `speciality` (type: `string`):

Optional speciality filter passed to Malt's search (e.g. "fullstack").

## `minDayRate` (type: `integer`):

Optional minimum day rate (in EUR) filter.

## `maxDayRate` (type: `integer`):

Optional maximum day rate (in EUR) filter.

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

How many search requests to run in parallel. Higher is faster but uses more proxy bandwidth.

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

Malt is behind Cloudflare and blocks datacenter IPs. The default UNBLOCKER group is required to make the internal API return data. Set "useApifyProxy": false only if you have another working route.

## Actor input object example

```json
{
  "searchTerms": [
    "react"
  ],
  "domains": [
    "fr"
  ],
  "maxItems": 100,
  "maxRepetitions": 10,
  "expandWithVariants": true,
  "variantModifiers": [
    "freelance",
    "senior",
    "expert",
    "junior",
    "lead",
    "remote",
    "consultant",
    "indépendant"
  ],
  "scrapeProfileDetails": false,
  "maxConcurrency": 5,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

## `results` (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 = {
    "searchTerms": [
        "react"
    ],
    "variantModifiers": [
        "freelance",
        "senior",
        "expert",
        "junior",
        "lead",
        "remote",
        "consultant",
        "indépendant"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "UNBLOCKER"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/malt-fr-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 = {
    "searchTerms": ["react"],
    "variantModifiers": [
        "freelance",
        "senior",
        "expert",
        "junior",
        "lead",
        "remote",
        "consultant",
        "indépendant",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["UNBLOCKER"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/malt-fr-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 '{
  "searchTerms": [
    "react"
  ],
  "variantModifiers": [
    "freelance",
    "senior",
    "expert",
    "junior",
    "lead",
    "remote",
    "consultant",
    "indépendant"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}' |
apify call ahmed_jasarevic/malt-fr-scraper --silent --output-dataset

```

## MCP server setup

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