# Upwork Jobs Scraper — Listings, Budgets, Skills (`scrapersdelight/upwork-jobs-scraper`) Actor

Scrape Upwork job postings by keyword, category, budget, experience level and client location. Every row carries the FULL job description, hourly or fixed budget, required skills, duration and workload. Clears Upwork's Cloudflare challenge for you. No Upwork account needed.

- **URL**: https://apify.com/scrapersdelight/upwork-jobs-scraper.md
- **Developed by:** [Scrapers Delight](https://apify.com/scrapersdelight) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 per job returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## 💼 Upwork Jobs Scraper — every public job posting, with the full brief

Turn Upwork's job marketplace into a clean, filterable dataset. Give it a keyword — or nothing at
all — and it walks Upwork's search results and returns **one row per job posting**, carrying the
**complete job description** (not the two-line preview), the hourly rate range or fixed budget, the
required skills, the experience level Upwork asks for, the project length and the weekly workload.

Upwork sits behind a **Cloudflare managed challenge**: a plain HTTP request gets a 403 and a 344 KB
"Checking your browser" page, and so does a stock headless browser. This actor clears that challenge
with a real browser **once per run**, then does the actual crawling over cheap plain HTTP. No Upwork
account, no cookies of your own, no CAPTCHA service.

> **Read "Known transport limitation" below before you run this on Apify.** The clearance step is
> currently blocked from inside Apify's Linux container for Upwork's job-search rule, and the actor
> says so loudly rather than returning an empty dataset. The scraper is proven end to end where a
> desktop browser is available.

***

### 🎯 What you get

- 📝 **The full job description** — median 1,002 characters, up to 10,777 measured. Upwork's search
  page clamps the text to two lines; this returns the whole brief, so you can run your own keyword,
  budget or red-flag filters over it.
- 💵 **Real budget numbers** — `hourlyRateMin` / `hourlyRateMax` for hourly contracts,
  `fixedBudget` for fixed-price projects, as separate numeric fields you can sort and chart.
- 🧠 **The skill list Upwork attached to the posting** — a mean of 6.2 tags per job, plus
  `matchedSkills`, the subset your keyword actually hit.
- 🧭 **Experience level, project length and hours per week** — resolved into human labels
  ("Expert", "3 to 6 months", "30+ hrs/week"), not Upwork's internal i18n keys.
- ⏱️ **Posting timestamps** — `postedAt`, `createdAt` and `renewedAt` in ISO 8601, so a scheduled
  run turns straight into a "new since last check" feed.
- 🔗 **A canonical job URL** for every row.

### 🔍 What you can filter on

Everything below is applied **server-side by Upwork**, so you only pay for rows you actually want:

| Filter | Values |
|---|---|
| Keywords | any number; Upwork's search syntax (quotes, `OR`, `NOT`) works |
| Job type | hourly · fixed price |
| Experience level | Entry Level · Intermediate · Expert |
| Category | all 12 Upwork top-level categories |
| Client location | country, sub-region or continent |
| Client hire history | no hires · 1–9 hires · 10+ hires |
| Hours per week | under 30 · 30+ |
| Project length | <1 month · 1–3 · 3–6 · 6+ months |
| Hourly rate | min / max USD |
| Fixed budget | min / max USD |
| Sort | newest first · most relevant |

You can also paste **any Upwork search URL** into `startUrls` and the actor will crawl exactly that
search, filters and all.

***

### 📊 Measured field coverage

Not estimates — the numbers below come from parsing **294 unique live job postings** across six
different searches (`python`, `shopify developer`, `video editor`, `data entry`, and the unfiltered
newest-jobs feed) on **2026-09-02**:

| Field | Filled | Note |
|---|---|---|
| `jobId`, `jobKey`, `title`, `url` | **100%** | |
| `description`, `descriptionLength` | **100%** | mean 1,612 chars · median 1,002 · max 10,777 |
| `jobType` | **100%** | 180 hourly / 114 fixed price in the sample |
| `experienceLevel` | **100%** | Intermediate 181 · Expert 85 · Entry Level 28 |
| `duration` | **100%** | |
| `postedAt`, `createdAt` | **100%** | ISO 8601 |
| `skills` | **98.6%** | mean 6.2 skills per job |
| `currency` | **81.3%** | present whenever a rate or budget is |
| `matchedSkills` | **71.1%** | your keyword's own skill hits |
| `workload` | **61.2%** | hourly jobs carry it; fixed-price ones mostly do not |
| `hourlyRateMin` / `hourlyRateMax` | **42.5%** | = **125 of 180 hourly jobs** advertised a rate |
| `fixedBudget` | **38.8%** | = **114 of 114 fixed-price jobs** — every one |
| `renewedAt` | **3.4%** | only re-posted jobs have one |

***

### ⚠️ Honest limits — read before you buy

**No client spend, hire rate or payment-verification data.** Upwork shows those only to signed-in
users. On the logged-out search payload they are present as **nulls on every single row** — 0 of 300
measured. Rather than ship a column that is always empty, this actor does not emit them at all. The
one client signal you *can* filter on is **hire history** (`clientHires`), which Upwork does apply
server-side. If you need spend and hire-rate columns, no logged-out scraper can honestly give them.

**Three Upwork filters do nothing for logged-out visitors** and are therefore not offered here:
"payment verified", "number of proposals" and "contract-to-hire". Each returned the unfiltered
2,444-row baseline when tested. They are omitted rather than shipped as knobs that silently no-op.

**Residential proxies are required, not recommended.** Cloudflare's challenge was cleared from
residential IPs and **never** from datacenter or static IPs. Measured 2026-09-02, same browser, same
minute: Apify `RESIDENTIAL` (US) cleared in 6.2 s / 8.8 s / 9.2 s; Apify `auto` (datacenter) and a
static-IP group both sat on "Just a moment…" until they were abandoned at 60 s. The input therefore
defaults to `RESIDENTIAL` + country US.

**⚠️ Known transport limitation — read this before running.** The clearance step needs a browser
whose fingerprint Cloudflare accepts, and Apify's Linux container currently does not have one for
Upwork's job-search rule specifically. Measured on 2026-09-02 across **12 configurations** — real
Google Chrome and bundled Chromium, headed under Xvfb, with and without the sandbox, with software
WebGL on and off, at 2/4/8 GB, on US / unpinned / GB residential IPs and on the platform's own
egress, with 90 s of patience plus mouse movement — the container never obtained `cf_clearance` for
`/nx/search/jobs/`. It *does* clear upwork.com's **homepage** (HTTP 200 + `cf_clearance` in 20 s),
and the identical code on a Windows desktop Chrome clears the job search through the *same* Apify
residential pool in 6–9 s, so the wall is the container's browser fingerprint, not the IP and not
the parser. Two ways round it today:

- **Bring your own clearance** (`clearance` input): mint `cf_clearance` in your own browser and pass
  it with the exact User-Agent that solved it, plus a proxy that egresses from that same IP. A
  cf\_clearance is bound to the (IP, User-Agent) pair, so all three must line up — and note that
  Apify's residential sessions do **not** hold an IP long enough to bridge this reliably (a pinned
  session's exit IP changed within minutes in testing); this route needs a sticky/ISP proxy.
- **Run it locally or on your own box**, where a normal desktop Chrome is available. The scraper
  itself is fully proven there: 60 rows in 15 s over Apify residential, one 8.8 s clearance,
  2 HTTP requests.

**A block is never reported as "no jobs".** If Cloudflare blocks everything, the run fails with the
challenged-page count and sample URLs in the message. A genuinely empty search exits successfully
with zero rows. Those two outcomes are never conflated.

***

### 🚀 Example inputs

**Fresh leads for a Python freelancer, cheapest possible run**

```json
{
  "searchQueries": ["python", "django", "fastapi"],
  "jobType": "hourly",
  "hourlyRateMin": 40,
  "experienceLevel": ["expert"],
  "sortBy": "recency",
  "maxItems": 200
}
```

**Serious buyers only — clients who have hired before, decent budgets**

```json
{
  "searchQueries": ["shopify"],
  "clientHires": "10-",
  "fixedBudgetMin": 1000,
  "maxItems": 500
}
```

**Market research: the whole marketplace, newest first**

```json
{
  "searchQueries": [],
  "sortBy": "recency",
  "maxItems": 5000
}
```

**Reuse a search you already built on Upwork**

```json
{
  "startUrls": [
    { "url": "https://www.upwork.com/nx/search/jobs/?q=react&sort=recency&contractor_tier=3&t=0" }
  ]
}
```

***

### 💰 Pricing

**$1.00 per 1,000 jobs** (`$0.001` per job returned) — pay per event, nothing else. No monthly fee,
no charge for starting a run, no charge for pages that came back blocked. Jobs that appear under
more than one of your keywords are **deduplicated before billing**, so you never pay for the same
posting twice. Rows are delivered and billed together, so a spend cap stops the run cleanly instead
of handing you rows you paid for twice or rows you never paid for.

***

### ❓ FAQ

**Do I need an Upwork account?** No. This reads only what Upwork serves to logged-out visitors.

**How many jobs can I get from one keyword?** As many as Upwork matches. Deep pagination is
verified: on a 2,444-result search, page 49 at 50 rows/page returned offset 2,400 — the tail of the
result set, not a truncated first 1,000. The unfiltered marketplace measured **115,014 open jobs**.

**How fast is it?** Upwork serves 50 jobs per request. One browser clearance (~4–5 s measured) is
minted at the start of the run; everything after that is plain HTTP.

**Can I run it on a schedule to get only new jobs?** Yes — sort by `recency`, keep `maxItems`
small, and diff on `jobId` or `postedAt` between runs.

**Why does it use a browser at all if it then uses plain HTTP?** Because Cloudflare's managed
challenge can only be solved by a real browser, but the resulting clearance cookie can be replayed
by a cheap HTTP client. Measured: one clearance served 8 of 8 consecutive search pages with zero
re-challenges.

**Does it work without a residential proxy?** No — see "Honest limits" above.

**Why did my Apify run fail with "Cloudflare blocked every request"?** That is the known transport
limitation above: the clearance step cannot currently be completed from inside Apify's container for
Upwork's job-search rule. The run fails loudly on purpose rather than handing you an empty dataset
that looks like "Upwork has no jobs".

**Is the description the full text?** Yes — the whole posting, HTML highlight markup stripped.

**What happens if Upwork changes their page?** The actor never reports a parse failure as an empty
result: unreadable pages are counted and named in the run's status message.

**Can it scrape freelancer profiles too?** No. One site, one job: this actor does job postings.

**What about proposals count / connects required?** Not exposed to logged-out visitors.

***

### ⚖️ Legal and fair use

This actor collects **publicly visible job postings** — the same pages any visitor sees without
signing in. It does not log in, does not create accounts, does not solve CAPTCHAs on a human's
behalf, and collects no personal data about freelancers. Upwork's `robots.txt` disallows
`/nx/` for crawlers (`Disallow: /nx/`), which is a crawler-etiquette directive, not a technical or
legal access control; you are responsible for your own use of the data and for complying with
Upwork's Terms of Service and any applicable law in your jurisdiction. Use the output for market
research, rate benchmarking and finding work — not for spamming clients.

# Actor input Schema

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

One Upwork search per keyword, e.g. `python`, `shopify developer`, `"video editor"`. Upwork's own search syntax works here — quotes for a phrase, OR/AND, and a leading NOT. Leave empty to scrape the newest jobs across the entire marketplace instead of a keyword.

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

Paste search URLs straight from Upwork (https://www.upwork.com/nx/search/jobs/?...) to reuse filters this input does not expose. Your query string is kept as-is; only the page size is raised to 50 results per request. Used in addition to the keywords above.

## `jobType` (type: `string`):

Restrict to hourly contracts or to fixed-price projects. `any` returns both. Measured on a live 2,444-job search: 1,505 hourly + 939 fixed.

## `experienceLevel` (type: `array`):

Upwork's own experience tiers. Select none (or all three) for no filter.

## `categories` (type: `array`):

Upwork's 12 top-level job categories. Leave empty for every category.

## `clientLocations` (type: `array`):

Only jobs whose client sits in these places. Accepts a country (`United States`, `Germany`), a sub-region (`Northern Europe`) or a continent (`Americas`, `Asia`, `Europe`, `Africa`, `Oceania`). Spelled exactly as Upwork's own location filter spells it.

## `clientHires` (type: `string`):

Filter by how many freelancers the client has hired before — the cheapest proxy for a serious buyer, since Upwork hides client spend from logged-out visitors.

## `workload` (type: `string`):

Upwork's workload filter: part time is under 30 hrs/week, full time is 30+.

## `duration` (type: `array`):

Expected engagement length. Select none for any length.

## `hourlyRateMin` (type: `integer`):

Only hourly jobs whose advertised rate range reaches at least this. 0 = no minimum.

## `hourlyRateMax` (type: `integer`):

Only hourly jobs whose advertised rate range stays at or below this. 0 = no maximum.

## `fixedBudgetMin` (type: `integer`):

Only fixed-price jobs budgeted at or above this. 0 = no minimum.

## `fixedBudgetMax` (type: `integer`):

Only fixed-price jobs budgeted at or below this. 0 = no maximum.

## `sortBy` (type: `string`):

`recency` walks the newest postings first — the right choice for a job-alert or lead feed. `relevance` mirrors Upwork's default keyword ranking.

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

Stop after this many jobs in total, across all keywords. Upwork returns 50 jobs per request, so a higher number simply means more pages. 0 = every matching job (a bare keyword can match thousands).

## `clearance` (type: `object`):

Optional escape hatch. Supply a Cloudflare clearance you minted yourself and the actor skips its own browser step entirely. Shape: {"cookie": "cf\_clearance=...; \_\_cf\_bm=...", "userAgent": "<the exact UA of the browser that solved it>", "proxySessionId": "myid"}. A cf\_clearance cookie is bound to the IP and User-Agent that solved the challenge, so all three must match: mint through an Apify proxy session, then pass that same session id here and the run will egress from the same IP. Leave empty to let the actor mint its own.

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

Upwork is behind a Cloudflare managed challenge. In testing the challenge was cleared ONLY from residential IPs — the same browser on datacenter IPs never passed — so RESIDENTIAL is the default and is strongly recommended. Point this at your own proxy if you prefer.

## Actor input object example

```json
{
  "searchQueries": [
    "python developer"
  ],
  "startUrls": [],
  "jobType": "any",
  "experienceLevel": [],
  "categories": [],
  "clientLocations": [],
  "clientHires": "any",
  "workload": "any",
  "duration": [],
  "hourlyRateMin": 0,
  "hourlyRateMax": 0,
  "fixedBudgetMin": 0,
  "fixedBudgetMax": 0,
  "sortBy": "recency",
  "maxItems": 25,
  "clearance": {},
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

The dataset of scraped Upwork job postings (one item per job).

# 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 = {
    "searchQueries": [
        "python developer"
    ],
    "maxItems": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapersdelight/upwork-jobs-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 = {
    "searchQueries": ["python developer"],
    "maxItems": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapersdelight/upwork-jobs-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 '{
  "searchQueries": [
    "python developer"
  ],
  "maxItems": 25
}' |
apify call scrapersdelight/upwork-jobs-scraper --silent --output-dataset

```

## MCP server setup

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