# Upwork Jobs Scraper (`usestring/upwork-jobs`) Actor

Collect Upwork job posts - budget, hourly range, skills - by keyword search.

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

## Pricing

from $2.25 / 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/platform/actors/running/actors-in-store#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 — budget, hourly rate, skills

The Upwork Jobs Scraper collects freelance job posts from Upwork search. Give it keywords, and it
returns the newest matching posts with the job's description, whether it is fixed-price or hourly,
the fixed budget or the hourly range as numbers, the required experience level, Upwork's own skill
tags and the posted timestamp.

To scrape Upwork jobs, put one or more keywords in `queries` — `"react developer"`,
`"shopify"`, `"technical writer"`. Every search is sorted by recency, so the newest posts come
first. No Upwork account, login or subscription is used; this Actor reads what an anonymous visitor
can search. Around **37 jobs per keyword**.

### What it returns

| Field | Type | Notes |
| --- | --- | --- |
| `jobId` | string | Upwork's own job ID — stable, use it to de-duplicate |
| `title` | string | |
| `description` | string | The job post's description text, as Upwork's own search returns it |
| `jobType` | string | `"FIXED"` or `"HOURLY"` |
| `budget` | number | The fixed-price total, e.g. `40`. Populated only when `jobType` is `"FIXED"` |
| `hourlyMin`, `hourlyMax` | number | The hourly range, e.g. `15` and `40`. Populated only when `jobType` is `"HOURLY"` |
| `currency` | string | ISO code of the fixed-price budget, e.g. `"USD"` |
| `experienceLevel` | string | The tier the client asked for, e.g. `"IntermediateLevel"`, `"ExpertLevel"` |
| `skills` | string\[] | Upwork's own skill labels, e.g. `["Data Scraping", "Python"]` |
| `postedAt` | string | ISO 8601 timestamp of when the post was published |
| `jobUrl` | string | Direct link to the Upwork job post |
| `query`, `sourceUrl`, `collectedAt` | string | Provenance for every row |

A job is priced one way or the other, so `budget` and the hourly range are never both filled. Each
is gated on the `jobType` Upwork assigned, rather than carrying a value across and inventing a
budget the client never set.

### Input

```json
{ "queries": ["react developer", "shopify", "technical writer"] }
```

| Field | Description |
| --- | --- |
| `queries` | Keywords to search Upwork for. Required, 1–50. Duplicates collapse to one fetch. |
| `maxItems` | Cap on dataset items. Default 1000. Free plans stop at 250 requests and 250 results — see below. |
| `concurrency` | Searches run in parallel. Default 3, maximum 5. |

There is no location input: the Upwork Jobs Scraper searches Upwork's global marketplace, and
`queries` is the only filter.

### Use cases

- Freelance rate benchmarking — what clients actually pay for a skill, fixed and hourly
- Demand tracking for a skill or stack over time, by re-running on a schedule
- Lead lists of open projects for an agency or freelancer
- Sizing a service market from `skills`, `budget` and `experienceLevel`
- Feeding a job-alert product with the newest Upwork posts, joined on `jobId`

### Reliability

The Upwork Jobs Scraper calls the same search endpoint Upwork's own site calls, so the response is
typed JSON: budgets as numbers, skills as labels, an explicit job type and a real publish timestamp.
The rendered search page is not used — its state blob is a minified bundle that reads back as no
usable structure, and the tiles it renders carry less than the search response does.

Reaching that endpoint anonymously needs Upwork's visitor token, which the search page hands out as
a cookie. Each run mints one token up front, which costs one extra request per run and no account of
any kind.

A keyword search that cannot be read is recorded in the run's `SUMMARY` under `failures`, together
with Upwork's own explanation where it gave one, and a run where every keyword failed exits with an
error.

### Frequently asked questions

**Do I need an Upwork account, login or Freelancer Plus plan?** No. The Upwork Jobs Scraper uses no
account and no login — it searches as an anonymous visitor, using the visitor token Upwork's own
search page issues.

**Does it return the client's country, total spend or hire rate?** No. Client identity and history
sit behind a scope Upwork's anonymous visitor token does not hold, so those fields are deliberately
not returned rather than returned empty.

**How many Upwork jobs does one keyword return?** One request per keyword, asking Upwork for its
50 newest matches; the measured average is around 37 jobs per keyword. Ten keywords therefore return
roughly 370 jobs.

**Are the results the newest posts?** Yes. Every search is sorted by recency, and `postedAt` carries
the publish timestamp so you can filter further.

**Is the budget a number?** Yes. `budget` is the fixed-price total as a number and `hourlyMin` /
`hourlyMax` are the hourly bounds, so nothing has to be parsed out of a display string.

**Can I filter by client location or budget?** No. `queries` is the only filter this Actor takes;
filter the returned rows on `budget`, `hourlyMin`, `experienceLevel` or `skills` afterwards.

**What happens on a free Apify plan?** The run stops at 250 requests and 250 results and reports
that it hit the limit. Any paid plan runs the full input.

### Limitations

The Upwork Jobs Scraper collects one page of results per keyword, so it captures the newest posts
rather than a full crawl of the marketplace; run several narrower keywords to widen coverage. Client
country, client total spend, hire rate, proposal and applicant counts, and freelancer profiles are
out of scope, and there is no location or budget filter on the input.

### Free plan limit

Runs started from an Apify **free plan** stop at **250 requests and 250 results**, and the run
reports that it reached the limit. Any paid plan runs the full input and `maxItems` you set.

The limit exists because this Actor fetches through our own infrastructure, which Apify does not
cover for free-plan runs. It binds on requests as well as results so that a large input list cannot
spend those fetches for rows the run will not return.

# Actor input Schema

## `queries` (type: `array`):

Keywords to search Upwork for.

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

Global cap on dataset items. Runs started from an Apify free plan stop at 250 requests and 250 results; any paid plan runs the full amount.

## `concurrency` (type: `integer`):

Targets fetched in parallel.

## Actor input object example

```json
{
  "queries": [
    "react developer"
  ],
  "maxItems": 1000,
  "concurrency": 3
}
```

# Actor output Schema

## `results` (type: `string`):

Collect Upwork job posts - budget, hourly range, skills - by keyword search.

## `summary` (type: `string`):

Item count, failure count and every target that failed, with its error.

# 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 = {
    "queries": [
        "react developer"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("usestring/upwork-jobs").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 = { "queries": ["react developer"] }

# Run the Actor and wait for it to finish
run = client.actor("usestring/upwork-jobs").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 '{
  "queries": [
    "react developer"
  ]
}' |
apify call usestring/upwork-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,usestring/upwork-jobs"
        }
    }
}

```

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/ftFqxVMUxusOf5qxz/builds/VCMeb0qvHiQa2ISsl/openapi.json
