# Upwork Freelancers Scraper (`nice_dev/upwork-freelancers-scraper`) Actor

Scrape Upwork freelancers by keyword, location and every talent filter, or from profile URLs: rate, exact earnings, Job Success Score, badges, skills, optional full profile with reviews. Export to JSON, CSV or Excel.

- **URL**: https://apify.com/nice_dev/upwork-freelancers-scraper.md
- **Developed by:** [Nice Dev](https://apify.com/nice_dev) (community)
- **Categories:** Jobs, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.79 / 1,000 freelancers

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

### 💼 What is Upwork Freelancers Scraper?

**Upwork Freelancers Scraper** extracts **freelancer profiles from [Upwork](https://www.upwork.com)**: **hourly rate, exact total earnings, Job Success Score, hours billed, badges, location, skills and portfolio titles**, for any keyword, place and filter of Upwork's talent search, or for the profile URLs you paste. Optionally it adds the **full profile**: client reviews, star rating, rehire rate, member since, last worked on, availability and the latest completed and ongoing jobs. It works as an **Upwork API alternative** for talent sourcing, market research and lead lists: run it on a schedule, call it from your code, or plug it into Make, Zapier or n8n.

Type a **keyword** (`python developer`) and a **location** (`United States`) or paste any Upwork talent search or profile URL, click **Start**, and download the freelancers in JSON, CSV or Excel. No Upwork account, nothing to set up. Searches go **beyond Upwork's 10,000 results per search**, several keywords and places run in one go, and a monitoring mode returns only the freelancers you have not had before.

### 📋 What data can you extract from Upwork?

One item per freelancer, 65 fields:

| Category | What you get |
| --- | --- |
| 👤 **Identity** | name (first name + last-name initial), profile title, full profile description, profile link, custom profile link, photo, identity verified — `Dmytro S.`, `Python Full stack developer` |
| 💰 **Rate and earnings** | hourly rate and currency, **exact total earnings** in USD, whether the freelancer hides earnings — `30 USD/h`, `142231.66` |
| ⭐ **Job Success and badges** | Job Success Score, Top Rated, Top Rated Plus, Rising Talent — `100 %`, `Top Rated Plus` |
| ⏱️ **Track record** | hours billed, hourly jobs, fixed-price jobs — `3349 hours` |
| 📍 **Location** | country, state, city, region, sub-region, time zone — `Ukraine`, `Kyiv`, `UTC+02:00` |
| 🧰 **Skills and portfolio** | skills, number of portfolio items, portfolio titles, specialized profiles, consultations offered |
| 🏢 **Agency** | agency name and link for agency members |
| 🔎 **Search context** | rank in the search, keyword, location and results page that found the freelancer |
| 🧾 **Full profile (option)** | star rating, review count, rehire rate, member since, last worked on, response time, availability, agencies, portfolio projects, catalog services, **work history with the client's review and 6 sub-scores** |

Every field, with an example, is listed in the **Output** section below.

The fields marked **Profile option** in the Output tab are filled when **Add full profile details** is on (off by default: it opens one more Upwork page per freelancer). Without it they are `null` or empty.

### ✅ Why use Upwork Freelancers Scraper?

- 💰 **Exact earnings, not a range**: the amount a freelancer earned on Upwork to the cent, and a flag (`earningsHidden`) that tells a hidden amount from a freelancer who has earned nothing yet.
- 🎛️ **16 freelancer filters**: hourly rate range, Job Success Score, minimum earnings, hours billed (also the last 6 months only), English level, other languages, freelancer or agency, Top Rated, Top Rated Plus, Rising Talent, open to contract-to-hire, skills, time zones and excluded keywords. Most are applied by Upwork itself, the others by the Actor on each freelancer it reads.
- 🌍 **Several places and keywords in one run**: every keyword is searched in every place (all 263 places Upwork offers: countries, sub-regions and regions), each with its own cap.
- 📈 **Beyond 10,000 results per search**: Upwork stops at 10,000 freelancers per search; **Go beyond 10 000 results per search** cuts a big search into hourly-rate bands and reads each one.
- 🧾 **Full profile with client reviews**: optional, with the 10 latest completed and 10 ongoing jobs, each with the client's score, comment and 6 sub-scores.
- 🔔 **Monitoring built in**: tick **Only new freelancers**, schedule the Actor, and each run returns (and charges) only the freelancers it has never delivered before.
- 🔗 **Paste profile URLs**: freelancer links (`~01…` key or custom name) are read directly, mixed with search URLs if you like.
- 🔌 API, scheduling, monitoring, integrations (Make, Zapier, n8n, Google Sheets…), proxy rotation and JSON/CSV/Excel export via the Apify platform.

### 🚀 How to scrape Upwork freelancers

1. Create a free Apify account.
2. Open **Upwork Freelancers Scraper** and type a **Search keyword** (e.g. `python developer`) and a **Location** (e.g. `United States`, or leave it empty for the whole world).
3. Set the filters you need in **Freelancer filters** (rate, Job Success Score, earnings, badges, skills…), or paste your own Upwork URLs into **Start URLs**: any talent search page (all filters set on the site are kept, pagination is automatic) or freelancer profile pages.
4. Set **Max freelancers** (100 by default, 0 = no limit) — and **Max freelancers per search** when you run several searches — then click **Start**.
5. Download the dataset in JSON, CSV, Excel or via API.

### 💰 How much does it cost to scrape Upwork freelancers?

This Actor uses **pay per event** pricing. You pay for each **freelancer saved** (the search-card fields, with exact earnings), plus a small fee per run start. The exact prices, for every Apify plan, are on the **Pricing** tab of the Actor. Platform usage (compute, proxy) is included in the price.

The **full profile** option is billed on top, only when you tick it: a fee for each profile page read (the page is read for every freelancer, also for those that a profile filter then drops) and a price for each profile delivered with its details. A profile Upwork hides from visitors comes back as the search card with `profileStatus` set to `private`: only the page read is billed for it, not the profile details.

Filters that Upwork does not offer itself (time zones, excluded keywords, a Job Success Score other than 80 or 90) are applied by the Actor on each freelancer it reads: a freelancer you keep costs its normal price, a freelancer a filter drops costs only the filter fee shown on the Pricing tab. The filters of the full profile (last worked, member since, rehire rate) work on the profile page: see the full profile option above.

### ⚙️ Input

A keyword and a place:

```json
{
    "query": "python developer",
    "location": "United States",
    "maxItems": 200
}
```

Several keywords and places, filters, a cap per search, and only the freelancers not delivered before:

```json
{
    "query": "python developer",
    "searchQueries": ["django developer"],
    "locations": ["France", "Eastern Europe"],
    "minHourlyRate": 20,
    "maxHourlyRate": 80,
    "minJobSuccess": 90,
    "minEarnings": 10000,
    "topRatedOnly": true,
    "maxItemsPerQuery": 50,
    "onlyNew": true,
    "stateKey": "python-eu"
}
```

The full profile of recently active freelancers, with a profile filter:

```json
{
    "query": "translation",
    "includeProfileDetails": true,
    "lastWorkedWithinDays": 90,
    "minHireAgain": 80,
    "maxItems": 20
}
```

Or with your own URLs (a search and two profiles):

```json
{
    "startUrls": [
        { "url": "https://www.upwork.com/nx/search/talent/?q=python&loc=france" },
        { "url": "https://www.upwork.com/freelancers/~01f7f788f7fcd6abae" },
        { "url": "https://www.upwork.com/freelancers/dmytro1serebrych1udata" }
    ],
    "maxItems": 500
}
```

| Field | Notes |
| --- | --- |
| `startUrls` | Upwork talent search URLs (filters kept, pagination automatic) or freelancer profile URLs, up to 1,000. When set, the keyword, place, city and skills fields are ignored (not even checked) and the Upwork filters below are not added to a pasted search URL; caps, filters of the Actor and monitoring still apply. |
| `query`, `searchQueries` | Skill, job title or any text, exactly as typed in Upwork's talent search (`python developer`); `searchQueries` adds more keywords (one search each). Empty = every freelancer of the chosen places. |
| `location`, `locations` | Country, sub-region or region by its English name as on Upwork (`United States`, `Eastern Europe`, `Europe`); `locations` adds more places. Every keyword is searched in every place (max 500 searches per run). An unknown name stops the run and lists the closest names. |
| `city` | The freelancer's city as written on Upwork (`Kyiv`), applied to every search. |
| `maxItems` | Stop after this many freelancers for the whole run, after deduplication and filters (`0` = unlimited). |
| `maxItemsPerQuery` | Cap for EACH search (keyword × place, or search URL). `0` = no per-search cap. |
| `splitLargeSearches` | On by default, no extra charge. A search with more than 10,000 results, when your caps ask for more, is cut into hourly-rate bands; a freelancer found in two bands is delivered and charged once. Off = stop at 10,000 per search. |
| `minHourlyRate`, `maxHourlyRate` | Hourly rate range in USD (`20`, `80`), applied by Upwork. |
| `minJobSuccess` | Lowest Job Success Score in % (`80`, `90`, `95`). 80 and 90 are applied by Upwork; any other value by the Actor, which drops freelancers without a score. With `startUrls`, every value is applied by the Actor, unless the pasted URL already asks Upwork for it. |
| `minEarnings` | Lowest total earnings on Upwork in USD (`1` = has earned something, `10000`), applied by Upwork. |
| `minHours` | Lowest number of hours billed on Upwork: `1`, `100` or `1000` (empty = any). |
| `hoursBilledRecentOnly` | Count only the hours billed in the last 6 months; needs `minHours`. |
| `englishLevel` | Lowest English level shown on the profile: `1` Basic, `2` Conversational, `3` Fluent, `4` Native or bilingual (empty = any). |
| `languages` | Other languages as two-letter codes (`fr`, `es`, `de`); several codes = the freelancer speaks all of them. |
| `talentType` | `all`, `independent` (freelancers only) or `agency` (agency members only). |
| `topRatedOnly`, `topRatedPlusOnly`, `risingTalentOnly`, `contractToHireOnly` | Keep only freelancers with the Top Rated, Top Rated Plus or Rising Talent badge, or open to contract-to-hire. |
| `skills` | Skill names the freelancer must have (`Python`, `Figma`); several skills = ALL of them. Each name is looked up on Upwork before the search; an unknown name stops the run and lists the closest skills. Ignored for pasted search URLs. |
| `timezones` | Keep only freelancers in these UTC offsets (`UTC+02:00`, `UTC-05:00`, `UTC`), applied by the Actor. The offset is the one Upwork prints: standard time, the same all year (no daylight saving). |
| `excludeKeywords` | Drop the freelancers whose title, profile description or skills contain one of these words (`wordpress`); case and accents ignored. |
| `includeProfileDetails` | Open each freelancer's profile page for the Profile option columns (reviews, work history, rating and more). Unticked by default. |
| `lastWorkedWithinDays` | Keep only freelancers whose last job ended within this many days (`90`); needs `includeProfileDetails`. |
| `memberSinceAfter`, `memberSinceBefore` | Keep only freelancers who joined Upwork on or after / on or before a date (`2026-09-01`, or `2 years`); need `includeProfileDetails`. |
| `minHireAgain` | Keep only freelancers whose clients hire them again at least this often, in % (`80`); freelancers without a rehire rate are dropped; needs `includeProfileDetails`. |
| `onlyNew`, `stateKey`, `resetState` | Monitoring: only the freelancers never delivered under this memory key (`python-eu`; one already delivered that a run reads again costs a filter check); `resetState` forgets the memory. |
| Advanced | `proxyConfiguration` (Apify proxy by default, included in the price; the residential proxy is not available), `maxConcurrency`, `maxRequestsPerMinute`, `minRequestIntervalMs`, `maxRequestRetries`, `debugLog`. |

### 📦 Output

One freelancer, shortened (the Profile option columns, the portrait, the portfolio titles and the specialized profiles are left out here):

```json
{
    "id": "1633122670629171200",
    "ciphertext": "~01b73b37302f2cfa92",
    "url": "https://www.upwork.com/freelancers/~01b73b37302f2cfa92",
    "vanityUrl": "https://www.upwork.com/freelancers/elahehmorshedlo",
    "name": "Elaheh M.",
    "firstName": "Elaheh",
    "lastNameInitial": "M.",
    "title": "Brand Specialist | Graphic Designer | Branding & Marketing Materials",
    "description": "With 𝟖+ years of experience across diverse industries…",
    "hourlyRate": 19,
    "currency": "USD",
    "totalEarnings": 302089.66,
    "earningsHidden": false,
    "jobSuccessScore": 93.1,
    "jobSuccessHidden": false,
    "totalHours": 7071.5,
    "totalHourlyJobs": 260,
    "totalFixedJobs": 434,
    "badge": "Top Rated Plus",
    "topRated": true,
    "topRatedPlus": true,
    "risingTalent": false,
    "talentStatus": "top_rated_plus",
    "country": "Georgia",
    "state": "TB",
    "city": "Tbilisi",
    "region": "Asia",
    "subregion": "Western Asia",
    "timezone": "UTC-08:00 Pacific Time (US & Canada); Tijuana",
    "skills": ["Graphic Design", "Logo Design", "Brand Design"],
    "portfolioCount": 676,
    "offersConsultations": true,
    "idVerified": true,
    "agencyName": "TEMlS Design Studio",
    "agencyUrl": "https://www.upwork.com/agencies/~0139b92cdbb03a6fd6",
    "searchRank": 1,
    "searchQuery": "design",
    "searchLocation": null,
    "searchUrl": "https://www.upwork.com/nx/search/talent/?q=design&page=1",
    "scrapedAt": "2026-10-01T17:30:01.294Z"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV or Excel.

#### All 65 fields

| Fields | What you get |
| --- | --- |
| `id`, `ciphertext`, `url`, `vanityUrl` | Upwork's person id, the profile key `~01f7f788f7fcd6abae`, the profile link, the link with the freelancer's chosen name (`null` when none) |
| `name`, `firstName`, `lastNameInitial` | `Dmytro S.`, `Dmytro`, `S.` (Upwork never shows the full last name to visitors) |
| `title`, `description` | the profile title and the full profile overview text |
| `portraitUrl`, `idVerified` | the profile photo, identity badge passed |
| `hourlyRate`, `currency` | `30`, `USD` (`null` when no rate is shown) |
| `totalEarnings`, `earningsHidden` | the exact amount earned on Upwork, `142231.66`; `null` with `earningsHidden` true when the freelancer hides it, `0` = nothing earned yet; a pasted profile URL has no search card: both are `null` (the profile page does not tell) |
| `jobSuccessScore`, `jobSuccessHidden` | `100` (0-100; `null` when there is no score yet or it is hidden), and whether it is hidden |
| `totalHours`, `totalHourlyJobs`, `totalFixedJobs` | hours billed, number of hourly jobs, number of fixed-price jobs |
| `badge`, `topRated`, `topRatedPlus`, `risingTalent`, `talentStatus` | `Top Rated Plus`, `Top Rated`, `Rising Talent` or `null`; the three flags; Upwork's raw status such as `top_rated_plus` |
| `country`, `state`, `city`, `region`, `subregion`, `timezone` | `Ukraine`, `KYIV CITY`, `Kyiv`, `Europe`, `Eastern Europe`, `UTC+02:00 Israel` |
| `skills`, `specializedProfiles` | skill names, titles of the specialized profiles |
| `portfolioCount`, `portfolioTitles`, `offersConsultations` | number of portfolio items, the titles of up to 20 of them, whether consultations are offered |
| `agencyName`, `agencyUrl` | the default agency of the freelancer, from the search results or from his profile page (`null` for independents) |
| `searchRank`, `searchQuery`, `searchLocation`, `searchUrl` | position in the search (1 = first; `null` for a pasted profile URL) — when a large search is cut into hourly-rate bands (`splitLargeSearches`), the position starts again at 1 in each band; the keyword and the place of the search that found the freelancer; the search URL (page 1, as you asked it) |
| `profileStatus` | **Profile option** — `ok`, `private` (profile hidden from visitors) or `failed`; `null` when the option is off |
| `rating`, `reviewCount`, `ratingRecent`, `rehirePercent` | **Profile option** — average client rating `4.83` (1-5), number of reviews, recent rating (`null` when there is no recent review), share of clients who hired again `82.35` |
| `totalJobsWorked`, `recentHours`, `recentJobs` | **Profile option** — jobs worked, hours and jobs of the recent period as counted by Upwork |
| `memberSince`, `lastWorkedOn` | **Profile option** — ISO dates `2020-05-09`, `2026-09-20` |
| `responseTime`, `availability`, `openToContractToHire`, `phoneVerified`, `countryCode`, `videoUrl` | **Profile option** — `within_a_day`, `As Needed - Open to Offers`, open to contract-to-hire, phone verified (a yes / no, never the number), `UA`, intro video link |
| `agencies`, `portfolio`, `catalogServices` | **Profile option** — every agency (name, link, rating, hours), portfolio projects (title, description, role, skills, link, date), fixed-price services and consultations (name, starting price, delivery days, link) |
| `completedJobsCount`, `ongoingJobsCount`, `workHistory` | **Profile option** — job counts and the 10 latest completed and 10 ongoing jobs, newest first: title, type, status, dates, hours, link, client rating, client comment, 6 sub-scores (Skills, Quality, Availability, Deadlines, Communication, Cooperation), the freelancer's own feedback |
| `scrapedAt` | ISO timestamp of the extraction |

### 💡 Tips

#### How to get more results

Upwork shows at most 10,000 freelancers per search (10 freelancers per page). Keep **Go beyond 10 000 results per search** on and set **Max freelancers** high (or `0`): a search with more results is cut into hourly-rate bands (at most 80 bands per search), each read up to its own 10,000, nobody delivered twice. Rates with cents (such as $10.50) are included. In a cut search, `searchRank` is the position in the band and starts again at 1 in each band. A band one dollar wide that still counts more than 10,000 freelancers (a very broad search at $10 an hour, for example) is read up to 10,000. You can also split a search yourself: several keywords, several places (up to 500 per run), a rate range, skills or a city. With **Max freelancers** above 10,000 (and not `0`), the cap can be reached before every band has been read in full: the bands are read side by side and the run stops wherever each one is, so the freelancers delivered lean towards the rates of those bands (12,000 freelancers on `design`: 99 % between $11 and $21 an hour). To cover every rate, set **Max freelancers** to `0`, or narrow the search with a rate range.

#### How to reduce costs

The price is per freelancer, so the levers are `maxItems`, `maxItemsPerQuery`, the filters, and `onlyNew` for recurring runs (you never pay the full price twice for the same freelancer; one a run reads again costs a filter check; one delivered with `profileStatus` `failed` comes back, and is charged again, at the next run). Leave the full profile option off unless you need reviews and work history: it is billed on top and opens one more page per freelancer.

#### Several searches in one run

Fill `searchQueries` and / or `locations`: the Actor runs one search per keyword × place (3 keywords × 4 countries = 12 searches, up to 500 per run), each with its own 10,000-result limit. The single `query` and `location` fields still work and are added to the lists. A freelancer found by several searches is saved — and charged — once. Set `maxItemsPerQuery` to give every search its own cap: without it the first searches can use up the whole `maxItems` budget. You can also paste several search URLs into `startUrls`: each one is a search of its own, with the same cap.

#### Full profile and profile filters

Tick **Add full profile details** to get the star rating, review count, rehire rate, member since, last worked on, availability, agencies, portfolio, catalog services and the work history. The work history holds the 10 latest completed jobs and the 10 latest ongoing jobs, each with the client's score, comment and 6 sub-scores; a job the client or freelancer keeps private has no title. Upwork shows visitors only these first 10 + 10 jobs: the next pages of the history are reserved to Upwork accounts, so they are not available. The filters **Last worked within (days)**, **Member since** and **Min rehire rate (%)** read the profile page: a freelancer they drop is not saved, but its profile page is still billed. Exact total earnings come from the search card: a freelancer read from a pasted profile URL has `totalEarnings` and `searchRank` set to `null` (its job success score is the one of its page).

#### Monitoring: only the new freelancers

Tick **Only new freelancers** (`onlyNew`) and schedule the Actor. The first run returns what the search gives; each later run skips the freelancers already delivered: they are not saved again and their profile is not even opened; each one a run reads again costs one filter check (Upwork sorts by relevance, so a run reads them to find the new ones). The memory lives in a named key-value store of your account (`upwork-freelancers-scraper-seen`, up to 150,000 freelancers per key) and is only updated with freelancers that really reached the dataset, so a failed run never hides anything. Give each schedule its own `stateKey` (two schedules sharing a key would hide each other's freelancers), and tick `resetState` once to start over. Upwork sorts the results by relevance, not by date, so a new freelancer can sit on any page: each run reads the search again, page by page, and only the freelancers not delivered yet are saved and charged in full; a search ends after 20 results pages in a row made only of freelancers already delivered (a first run, with nothing delivered yet, never ends that way). On a broad search that stop rarely comes, because Upwork's order moves and each run meets freelancers the last one never reached: on `python` (175,000 profiles), a second run read all 1,000 pages, delivered 1,772 freelancers and paid 6,157 filter checks. A search cut into hourly-rate bands counts those 20 pages band by band: with **Max freelancers** above 10,000 or at `0`, a run on a very broad search that finds nothing new can read 20 pages in each of its bands (80 at most: 1,600 pages and 16,000 filter checks). Under a cap (**Max freelancers**, 100 by default), the next run goes on with the freelancers the cap left out, as long as they start within the first 20 results pages; past that, the 20-pages-in-a-row stop ends the search. To get everything, set **Max freelancers** to `0`; for a pure watch of new freelancers, use a precise search (under 10,000 results) with **Max freelancers** at `0`, or, on a broad search, keep it at 10,000 or less or untick **Go beyond 10 000 results per search**. With filters on, a freelancer your filters drop is checked (and the check charged) once: the next runs with the same filters skip it for free, even if the freelancer would pass later (new work, a first Job Success Score). Change a filter, or tick `resetState`, to check them again. A freelancer delivered with `profileStatus` `failed` (its profile page could not be read) is not remembered: the next run returns it with its profile, and charges it again. Pair `onlyNew` with a precise search (keyword, place, filters) to keep runs short.

### 🔌 Integrations and API

Call the Actor via the Apify API, the JavaScript or Python clients, or connect it with integrations and webhooks (Make, Zapier, n8n, Google Sheets, Slack, Airtable…). The dataset can be fetched as JSON or CSV from any tool.

### 🤖 Use with AI agents (MCP)

AI agents (Claude, ChatGPT, Cursor…) can find and run this Actor through the [Apify MCP server](https://mcp.apify.com), billed to their Apify account like any run. It returns one item per Upwork freelancer. Actor id: `nice_dev/upwork-freelancers-scraper`; MCP server with this Actor only: `https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/upwork-freelancers-scraper`.

Smallest input, for a cheap first call:

```json
{
    "query": "python developer",
    "location": "United States",
    "maxItems": 10
}
```

Key output fields: `name`, `title`, `url`, `hourlyRate`, `totalEarnings`, `jobSuccessScore`, `badge`, `country` and `skills` (with `includeProfileDetails`: `rating`, `reviewCount`, `workHistory`).

Cost: priced per freelancer plus a small fee per run start, see the pricing section above. Cap each call with `maxItems` and, through the API, with the run option `maxTotalChargeUsd`.

### ❓ FAQ

#### Is it legal to scrape Upwork?

The Actor only reads what Upwork shows publicly to any anonymous visitor. It logs in to nothing. Results contain personal data (a first name and last-name initial, a city, a photo, client reviews), which is protected by GDPR: do not store it without a legitimate reason. You are responsible for using the data in compliance with Upwork's Terms of Use and applicable law. This Actor is not affiliated with Upwork.

#### Does it need a login or a proxy?

No login. The proxy is included in the price: leave the default setting (the residential proxy is not available). A request the site turns away is retried at once on a new proxy session, up to 10 times on top of the retries (without a proxy, after a pause of 5 seconds, doubled at each retry up to 150 seconds).

#### Is the data safe to open in Excel or to show on a web page?

Titles, descriptions, skills and reviews are the freelancers' and clients' own words, copied as they are. A text can begin with `-`, `+`, `=` or `@`: Excel and Google Sheets may read such a cell of a CSV file as a formula or as a number. The Actor leaves the text as it is, so that the JSON and the API give the real value: when you open a CSV, import these columns as text. On a web page, escape every field like any text written by a stranger.

#### Known limitations

- Upwork shows visitors the first name and the **last-name initial** only (`Dmytro S.`), no e-mail address and no phone number: the Actor cannot return them.
- Languages spoken (the profile's own list), education, certificates and employment history are not shown to visitors: they are not in the output. The `languages` filter still works, Upwork applies it itself.
- The order of the results is Upwork's relevance order; it cannot be changed, and it moves while a run reads it: on a broad search, about 1 freelancer in 5 comes back on a later page (measured on `python`, 175,000 profiles: the 10,000 results Upwork lets be read held 7,760 different freelancers). Each one is delivered and charged once, so a run capped close to 10,000 on one search can end short of its cap; **Max freelancers** above 10,000 or at `0` goes further, by cutting the search into hourly-rate bands.
- A freelancer who hides earnings has `totalEarnings` set to `null` (`earningsHidden` true); one with no Job Success Score yet has `jobSuccessScore` set to `null`.
- The work history of a profile is limited to the 10 latest completed and the 10 ongoing jobs; the rest is reserved to Upwork accounts.
- Searching by US state is reserved to Upwork accounts: use `city` or a pasted search URL.
- `onlyNew` remembers freelancers, not their content: a freelancer whose rate changed is not returned again.
- Two runs sharing the same `stateKey` at the same time may both return the same new freelancer.

**A run that reaches its timeout** stops itself about 45 seconds before it: no new page is asked, what it read is saved and, with `onlyNew`, remembered, and the run ends *Succeeded* with "Stopped before the run's timeout". Resurrect it to go on from there, or give the next run a longer timeout (Run options).

**A run the platform stops without warning** (out of memory)

- Resurrect it: it goes on from where it stood at most a minute before the stop. What it had read since is read again, and the freelancers already saved are skipped: none is delivered or charged twice, and `maxItems` still counts them.
- With `onlyNew`, the memory is saved a few seconds after the freelancers are written: resurrect the stopped run and the freelancers it had saved meanwhile join the memory; leave it stopped for good, and the next run may return the last few seconds of them once more.

#### Something doesn't work?

The last line of the log counts the freelancers saved, filtered out and no longer available (removed while the run was reading them), and the requests that failed after every retry. Those requests and the removed profiles are listed, with the reason, in the `FAILED_REQUESTS` record of the run's key-value store. A run that saved nothing and had failed requests fails, and its last message gives the cause.

If Upwork changes its pages, you are told instead of paying for blank rows. A results page that counts freelancers but gives none the Actor can read is an error (listed in `FAILED_REQUESTS`), never a quiet "No results". If the first 20 freelancers read all lack the same field (their hourly rate, country or skills, for example), the run saves nothing more, stops and fails, and its last message names the missing field: at most those first freelancers are charged. A run of fewer freelancers (a scheduled `onlyNew` run that finds a few new ones, a small **Max freelancers**) is checked at its end, from 5 freelancers: its freelancers are already saved, and the run fails with the missing field instead of ending green.

### 🛟 Support

Open an issue in the **Issues** tab with a link to your run: the run log and the `FAILED_REQUESTS` record of the key-value store show exactly which URLs failed and why.

# Actor input Schema

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

Upwork talent-search URLs (`https://www.upwork.com/nx/search/talent/?q=python`, every filter set on the site is kept, pagination is automatic) or freelancer profile URLs (`https://www.upwork.com/freelancers/~01f7f788f7fcd6abae` or the custom profile name). When this list is not empty, the search fields below (keywords, locations, city, skills) are ignored, not checked, and the filters Upwork applies itself are not added to a pasted search URL; caps, post-filters and monitoring still apply. Max 1 000 URLs.

## `query` (type: `string`):

Skill, job title or any text, exactly as typed in Upwork's talent search (e.g. `python developer`). Empty = every freelancer of the chosen locations.

## `searchQueries` (type: `array`):

Several keywords in one run: one search per keyword (times each location below). Added to **Search keyword**; freelancers found by several searches are saved once.

## `location` (type: `string`):

Country, sub-region or region of the freelancer, by its English name as on Upwork (e.g. `United States`, `Eastern Europe`, `Europe`). Empty = worldwide.

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

Several locations in one run: every keyword is searched in every location (3 keywords × 4 countries = 12 searches, max 500), each with its own 10 000-result cap. Added to **Location**.

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

Freelancer's city as written on Upwork (e.g. `Kyiv`, `Lagos`). Applied to every search.

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

Maximum number of freelancers to save for the whole run (after deduplication and filters). 0 = no limit.

## `maxItemsPerQuery` (type: `integer`):

Cap for EACH search (keyword × location, or search URL), so that the first search cannot use up the whole **Max freelancers** budget. 0 = no per-search cap.

## `splitLargeSearches` (type: `boolean`):

Upwork shows at most 10 000 freelancers per search. When a search has more and your caps ask for more, it is split automatically into hourly-rate bands (each band read up to its own 10 000, rates with cents included, nobody delivered twice). In a split search, the rank of a freelancer is its position in its band. Off = stop at 10 000 per search.

## `minHourlyRate` (type: `integer`):

Lowest hourly rate.

## `maxHourlyRate` (type: `integer`):

Highest hourly rate.

## `minJobSuccess` (type: `integer`):

Lowest Job Success Score. 80 and 90 are filtered by Upwork; any other value is applied by the Actor on each result (freelancers without a score are dropped). With start URLs, every value is applied by the Actor, unless the pasted URL already asks Upwork for it.

## `minEarnings` (type: `integer`):

Lowest amount earned on Upwork (e.g. `1` = has earned something, `10000` = $10K+).

## `minHours` (type: `string`):

Lowest number of hours billed on Upwork.

## `hoursBilledRecentOnly` (type: `boolean`):

Count only the hours billed in the last 6 months (needs **Min hours billed**).

## `englishLevel` (type: `string`):

Lowest English level shown on the profile.

## `languages` (type: `array`):

Languages the freelancer speaks, as ISO 639-1 codes (e.g. `fr`, `es`, `de`). Several codes = the freelancer must speak ALL of them (and, not or).

## `talentType` (type: `string`):

Independent freelancers, agency members, or both.

## `topRatedOnly` (type: `boolean`):

Only freelancers with the Top Rated badge.

## `topRatedPlusOnly` (type: `boolean`):

Only freelancers with the Top Rated Plus badge.

## `risingTalentOnly` (type: `boolean`):

Only freelancers with the Rising Talent badge.

## `contractToHireOnly` (type: `boolean`):

Only freelancers open to being hired full-time after a contract.

## `skills` (type: `array`):

Upwork skills the freelancer must have, by name (e.g. `Python`, `Figma`). Several skills = the freelancer must have ALL of them (and, not or). Each name is looked up on Upwork before the search (one extra page per skill); an unknown name stops the run and lists the closest skills Upwork offers. Ignored for pasted search URLs.

## `timezones` (type: `array`):

Keep only freelancers in these UTC offsets (e.g. `UTC+02:00`, `UTC-05:00`, `UTC` for UTC+00:00). The offset is the one Upwork prints on the profile: standard time, the same all year (no daylight saving). Applied by the Actor on each result.

## `excludeKeywords` (type: `array`):

Drop the freelancers whose title, profile description or skills contain one of these words (case and accents ignored).

## `includeProfileDetails` (type: `boolean`):

Open each freelancer's profile (1 extra page per freelancer): star rating and review count, rehire rate, member since, last worked on, availability, response time, agencies, portfolio, catalog services and the 10 latest completed and 10 ongoing jobs with the client's review (score, comment, 6 sub-scores). Off = search-card fields only (faster, cheaper).

## `lastWorkedWithinDays` (type: `integer`):

Keep only the freelancers whose last job on Upwork ended within this many days (read on the profile page: needs **Add full profile details**). Freelancers dropped by a profile filter are not saved; their profile page read is still charged. Empty = no filter.

## `memberSinceAfter` (type: `string`):

Keep only the freelancers who joined Upwork on or after this date (`2026-09-01` or a relative date like `2 years`; needs **Add full profile details**).

## `memberSinceBefore` (type: `string`):

Keep only the freelancers who joined Upwork on or before this date (`2026-09-01` or a relative date like `5 years`; needs **Add full profile details**).

## `minHireAgain` (type: `integer`):

Keep only the freelancers whose clients hire them again at least this often, 1-100 (the profile's rehire rate; a freelancer without one is dropped; needs **Add full profile details**).

## `onlyNew` (type: `boolean`):

Skip the freelancers that a previous run (same **Memory key**) already delivered: they are not saved again and their profile is not even opened; each one a run reads again costs one filter check (Upwork sorts by relevance, so a run reads them to find the new ones). First run = everything is new.

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

Name of the memory used by **Only new freelancers**. Give each schedule / task its own key (e.g. `python-us`) so that they do not share their memory. Letters, digits, `-` and `_`.

## `resetState` (type: `boolean`):

Forget everything remembered under this **Memory key** before the run: this run returns (and charges) every freelancer again. Untick it afterwards.

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

Apify Proxy or your own proxies. Keep the default: it is included in the price. The residential Apify proxy is not available in this Actor.

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

Maximum number of requests processed in parallel.

## `maxRequestsPerMinute` (type: `integer`):

Most requests to the site in any 60 seconds, all of them counted: search pages and profile pages. A budget, not an even pace: up to this many can leave at once when the minute starts (spread them with the minimum delay below). Lower it if the site answers HTTP 429 / 403 in the log.

## `minRequestIntervalMs` (type: `integer`):

Smallest gap between two requests, in milliseconds. Unlike the per-minute rate, this spreads the requests evenly instead of letting them go out in a burst. 0 = no gap.

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

Retries per request before it is marked as failed. Behind a proxy, a request the site turns away is also retried on a new proxy session up to 10 times without using up these retries.

## `debugLog` (type: `boolean`):

Include debug messages in the run log.

## Actor input object example

```json
{
  "startUrls": [],
  "query": "python developer",
  "searchQueries": [],
  "locations": [],
  "maxItems": 100,
  "maxItemsPerQuery": 0,
  "splitLargeSearches": true,
  "minHours": "",
  "hoursBilledRecentOnly": false,
  "englishLevel": "",
  "languages": [],
  "talentType": "all",
  "topRatedOnly": false,
  "topRatedPlusOnly": false,
  "risingTalentOnly": false,
  "contractToHireOnly": false,
  "skills": [],
  "timezones": [],
  "excludeKeywords": [],
  "includeProfileDetails": false,
  "onlyNew": false,
  "stateKey": "default",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 4,
  "maxRequestsPerMinute": 120,
  "minRequestIntervalMs": 0,
  "maxRequestRetries": 5,
  "debugLog": false
}
```

# 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 = {
    "query": "python developer",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("nice_dev/upwork-freelancers-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 = {
    "query": "python developer",
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("nice_dev/upwork-freelancers-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 '{
  "query": "python developer",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call nice_dev/upwork-freelancers-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/upwork-freelancers-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/RNm0oaG05UoaGYxNa/builds/vay7p0T66gSVEUGzG/openapi.json
