# Upwork Jobs Scraper | Skills, Budgets & Filters (`keystonelabs/upwork-jobs-scraper`) Actor

Find public Upwork jobs by search query, job type, experience and rate filters. Export titles, descriptions, skills, budgets and job links for freelance research. Source availability and filter coverage may change.

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

## Pricing

$3.00 / 1,000 jobs

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

### Find freelance jobs for your target skill

Upwork Jobs Scraper: Find public Upwork jobs by search query, job type, experience and rate filters. Export titles, descriptions, skills, budgets and job links for freelance research. Source availability and filter coverage may change.

### Start with a small example

1. Open Input, switch to JSON and paste the example below. Replace the example searchQueries with your own research targets when ready.
2. Check the live Pricing tab and set a run spending limit.
3. Run the Actor, then open the **Workflow** output view. Inspect `title`, `jobType`, `experienceLevel`, `budget`, `skills`, `url`.
4. Export JSON for nested data or CSV for a spreadsheet. Review a small sample before increasing the scope.

```json
{
  "searchQueries": [
    "web scraping"
  ],
  "maxItems": 20
}
```

At the rate checked September 9, 2026, 20 saved rows cost $0.060 in Actor event fees ($3.00 per 1,000 rows). Actual returned rows determine the event fee. Taxes and other account services may be separate.

### What can I use it for?

- **Job alerts and lead feeds.** Poll your niche keywords sorted by newest and push fresh postings to Slack, email or a CRM minutes after they go live, before the proposal count climbs.
- **Freelance market research.** Measure demand for a skill, compare hourly rate ranges across experience levels, or track how many open jobs mention a tool over time using the `searchTotal` field.
- **Agency and staffing pipelines.** Find companies posting contract-to-hire or full-time freelance work in your service area and qualify them by budget and client hiring history.
- **Pricing benchmarks.** Pull hundreds of fixed-price and hourly budgets for a category to see what clients actually offer before you quote.
- **Training and analysis corpora.** Full description text plus a normalized skill taxonomy with stable skill UIDs is a good input for classifiers, embeddings and trend dashboards.

### What data does it return?

Each job posting is one record. The fields buyers filter on most are at the top level, so the CSV export is usable without post-processing:

| Field | Example |
|---|---|
| `id` | `2095973892295888069` |
| `url` | `https://www.upwork.com/jobs/~022095973892295888069` |
| `title` | `Python developer for e-commerce price monitoring` |
| `description` | Full posting text, newlines preserved |
| `jobType` | `HOURLY` or `FIXED` |
| `experienceLevel` | `Entry level`, `Intermediate` or `Expert` |
| `hourlyBudgetMin` / `hourlyBudgetMax` | `25` / `60` (hourly jobs) |
| `fixedPriceAmount` | `150` (fixed-price jobs) |
| `budget` | `$25-$60/hr` or `$150 fixed` |
| `skills` | `["Python", "Web Scraping", "Data Extraction"]` |
| `skillUids` | Stable Upwork taxonomy IDs matching `skills` |
| `postedAt` | `2026-09-04T20:34:52.288Z` |
| `rank` | Position of the job in the search results |
| `searchQuery` / `sortBy` / `searchTotal` | The query that found it, the sort used, and the total number of matches Upwork reported |
| `scrapedAt` | When the record was collected |

`contractorTier` keeps Upwork's raw experience-level value and `relevance` keeps the raw ranking payload for anyone who needs them. Budgets are in US dollars, which is the only currency Upwork uses for job posts.

#### Illustrative output

Values explain the output shape; they are not a live result or guaranteed field coverage.

```json
{
  "id": "2095973892295888069",
  "url": "https://www.upwork.com/jobs/~022095973892295888069",
  "title": "Scrape supplier catalogs into a Google Sheet weekly",
  "description": "We sell replacement parts and need a weekly export of three supplier catalogs (about 4,000 SKUs) into a shared Google Sheet.\n\nDeliverables:\n- Python script with scheduling\n- Handles login and pagination\n- Documentation for our ops team",
  "jobType": "FIXED",
  "experienceLevel": "Intermediate",
  "contractorTier": "IntermediateLevel",
  "hourlyBudgetMin": null,
  "hourlyBudgetMax": null,
  "fixedPriceAmount": 150,
  "budget": "$150 fixed",
  "skills": ["Python", "Web Scraping", "Data Scraping", "Google Sheets"],
  "skillUids": ["1031626722463629312", "996364628040994822", "1031626722683932672", "1110580755107926016"],
  "postedAt": "2026-09-04T20:34:52.288Z",
  "relevance": { "position": "3" },
  "rank": 4,
  "searchQuery": "web scraping",
  "sortBy": "recency",
  "searchTotal": 382,
  "scrapedAt": "2026-09-04T21:12:40.512Z"
}
```

### How to use it

1. Enter one or more **Search queries**, one per line. Keywords, a skill, a category name such as `web development`, a quoted phrase such as `"react native"` and operators such as `python AND scraping` all work the same way they do on Upwork. Enter `*` to list the newest jobs across every category.
2. Set **Max jobs** to the number of unique postings you want across all queries. The default of 100 is a good first run.
3. Optionally open **Filters** to restrict job type, experience level, hourly rate, client hiring history, client country, weekly workload or contract-to-hire.
4. Click **Start**. Results appear in the **Output** tab as they arrive and can be downloaded as JSON, CSV, Excel or XML, or read through the API.

To build a job-alert feed, schedule the actor to run every few minutes with **Sort by** set to newest first and a small **Max jobs**, then deduplicate on `id` downstream.

#### Input example

```json
{
  "searchQueries": ["web scraping", "data extraction"],
  "maxItems": 200,
  "sortBy": "recency",
  "jobType": "any",
  "experienceLevels": ["IntermediateLevel", "ExpertLevel"],
  "hourlyRateMin": 30,
  "clientHires": ["1-9", "10-"],
  "clientLocations": ["United States", "United Kingdom"],
  "contractToHireOnly": false
}
```

You do not need to configure a proxy. Upwork's bot protection scores browser fingerprints and IP ranges differently from hour to hour, so the actor probes a few combinations at the start of each run — a Firefox fingerprint over a direct connection, then through residential and datacenter proxies, then a Chrome fingerprint the same way — and keeps the first one Upwork accepts. When Upwork is challenging everything for a moment, the actor waits and tries again for up to about three minutes before giving up. Leave **Proxy configuration** empty unless you specifically want to force a proxy group.

### How it works

The actor first loads the Upwork homepage the way a normal Chrome browser would, which issues a short-lived anonymous visitor token. It then calls the public job-search endpoint with that token, requesting 50 results per page and walking the pages until it reaches your limit or the end of the results. Pages are fetched a few at a time with automatic retries and token refresh, and each job is written to the dataset the moment it is received. Jobs that appear under more than one query are saved once.

Only fields that Upwork exposes to logged-out visitors are requested. The actor never logs in, never reuses anyone's session and never solves challenges.

### Pricing

You pay per job posting saved to the dataset. There are no subscriptions or minimums, duplicates across queries are not charged twice, and you can cap the maximum spend of any run before you start it.

### Scope and responsible use

This actor reads only the job-posting data that Upwork shows to anyone browsing the site without an account. It requires no login, cookies or credentials, does not collect freelancer profiles or any personal data, and does not attempt to bypass access controls. Job descriptions are written by the clients who post them; use the data in line with the laws that apply to you and with Upwork's terms.

### Limitations

- **No client statistics.** Client total spend, rating, review count, payment-verified status, location and proposal counts are only served to logged-in users, so they are not included. You can still filter by client hiring history and client country, and sort by client spend or rating, because Upwork applies those on its side.
- **5,050 results per query.** Upwork stops paging at offset 5,000. To go deeper, split a broad search into several narrower queries or filter combinations; results are deduplicated across queries automatically.
- **Derived job links.** Upwork does not return job URLs to anonymous visitors. The `url` field is built from the numeric job ID using Upwork's current link format.
- **Hourly rate filters only affect hourly jobs.** Fixed-price postings are still returned when a rate filter is set; combine with **Job type** if you want hourly jobs only.
- **Bot protection.** Upwork uses Cloudflare. If runs fail with HTTP 403, enable Apify residential proxies under **Proxy configuration**.
- Field availability depends on what Upwork exposes to visitors and can change without notice.

### Something not working?

Open an issue on this actor's **Issues** tab with your input and the run ID. Reproducible problems are usually fixed within days.

### Interpret empty results and errors

Check the run status and log if no records are returned. Restrictive filters, inaccessible content or a changed source page can produce an empty result; empty output does not prove there are no matching records on the source.

Billing follows saved job or company rows. Disclosed salary and optional fields may be missing. Proxy selection and run duration can affect operating costs. Do not increase concurrency or enable a more expensive proxy merely to work around an unclear result.

For a reproducible problem, open this Actor’s Issues tab with a small public input, expected behavior and relevant error text. Remove tokens and confidential information. No response-time or uptime guarantee is offered.

### Use the result in an automation

In Make or n8n, use the Apify integration to run this Actor with the same JSON input, wait for completion, then retrieve its default dataset. Route failed runs and error rows to a review step before sending valid results to your spreadsheet or CRM. Scheduling does not make these Actors emit only new records: deduplicate downstream using the source URL or record ID.

For Node.js, install the official `apify-client` package and set your own `APIFY_TOKEN` environment variable. The following example starts a paid run with a small spending limit.

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const input = {
  "searchQueries": [
    "web scraping"
  ],
  "maxItems": 20
};
const run = await client.actor('keystonelabs/upwork-jobs-scraper').call(input, {
  memory: 512, timeout: 180, maxTotalChargeUsd: 0.10
});
if (run.status !== 'SUCCEEDED') throw new Error('Run did not succeed: ' + run.id);
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 100 });
console.log(items);
```

The first 100 rows are retrieved in this example. Use the dataset API pagination for a larger result. Keep credentials out of shared inputs and source files.

### A useful next step

- [Naukri Jobs Scraper | India Hiring & Salary Data](https://apify.com/keystonelabs/naukri-jobs-scraper): find hiring companies for a role in india. This is a separate Actor with its own input and price.

# Actor input Schema

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

One search per line, exactly as you would type it into the Upwork job search box: keywords, a skill, a category name (web development), a quoted phrase ("react native"), or operators (python AND scraping). Use \* to list the newest jobs across all categories.

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

Stop after this many unique jobs across all queries. Upwork exposes at most 5,050 results per query, so use several narrower queries to go deeper.

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

Order of results within each query. Newest first is best for job-alert feeds; relevance is best for keyword research.

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

Only hourly jobs, only fixed-price jobs, or both.

## `experienceLevels` (type: `array`):

Keep only jobs posted for these experience levels. Leave empty for all.

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

Keep hourly jobs whose rate range reaches at least this much. Fixed-price jobs are not affected by rate filters.

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

Keep hourly jobs whose rate range starts at or below this much.

## `clientHires` (type: `array`):

Keep only jobs from clients with this many previous hires on Upwork. Leave empty for all.

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

Full country names as Upwork spells them, one per line, e.g. United States, United Kingdom, Germany, Canada, Australia. Leave empty for worldwide.

## `workload` (type: `array`):

Keep only hourly jobs with this expected workload. Leave empty for all.

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

Keep only jobs the client flagged as contract-to-hire.

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

How many result pages to request in parallel. The default is polite and still fast; raise it only for very large runs.

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

Optional. The actor finds a working path on its own — it tries Firefox and Chrome browser fingerprints over a direct connection, then datacenter proxies, then residential — and keeps whichever Upwork accepts. Set this only to force one specific proxy group.

## Actor input object example

```json
{
  "searchQueries": [
    "web scraping"
  ],
  "maxItems": 20,
  "sortBy": "recency",
  "jobType": "any",
  "experienceLevels": [],
  "clientHires": [],
  "clientLocations": [],
  "workload": [],
  "contractToHireOnly": false,
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset of Upwork job postings. Each item has the title, full description, skills, job type, experience level, hourly or fixed-price budget and posting time.

## `workflowJson` (type: `string`):

Selected columns for this workflow. Inspect errors and missing values before using the data.

## `workflowCsv` (type: `string`):

Download the workflow columns as CSV. Use JSON for full nested arrays.

# 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": [
        "web scraping"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("keystonelabs/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": ["web scraping"],
    "maxItems": 20,
}

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

```

## MCP server setup

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