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

Scrape Upwork job posts by keyword or search URL: title, full description, skills, budget, hourly rate, experience level, duration and publication time. Export to JSON, CSV or Excel.

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

## Pricing

from $0.27 / 1,000 job posts

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 Jobs Scraper?

**Upwork Jobs Scraper turns any Upwork job search into a clean dataset.** Give it a keyword — or paste a search URL straight from [Upwork](https://www.upwork.com) — and it returns every matching job post with its **full description**, its **skills**, its **budget or hourly range**, the **experience level**, the **project duration** and the **exact publication time**, ready for JSON, CSV or Excel.

It reads the same job posts an anonymous visitor sees on Upwork's search pages. **No Upwork account needed**, and it is quick: **250 job posts in about 20 seconds**, 1,000 in under a minute, which is why it is one of the cheapest Upwork scrapers on the Store.

Try it: type `python` in **Search keyword**, hit Start, and look at the Output tab.

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

One row per job post, **23 fields**:

| Category | What you get |
| --- | --- |
| 🏷️ **Job post** | title, link to the job page, Upwork's own id — `Fix Broken Python Scraper` |
| 📝 **Description and skills** | the full text as the client wrote it, line breaks kept; every skill asked for — `Python, Scrapy` |
| 💰 **Budget** | fixed price, or hourly range, or weekly retainer, in USD — `$20` / `$40-60 an hour` |
| 🎓 **Experience level** | entry level, intermediate or expert |
| ⏳ **Duration and hours** | how long the project lasts, how many hours a week — `1 to 3 months` |
| 🕒 **Dates** | when it was posted, to the millisecond — what the date filters read |
| 🔎 **Where it came from** | which of your keywords found it, the search page, when it was read |

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

### ✅ Why use Upwork Jobs Scraper?

- 🔔 **Monitoring that costs next to nothing when nothing is new.** Turn **Only new job posts** on and a repeat run returns only what appeared since: a run that finds nothing saves 0 rows and is charged only the run-start fee (plus the pages your filters kept nothing of, if you set any).
- 🎯 **Every filter Upwork itself offers**, in plain words: job type, experience level, project duration, hours per week, contract-to-hire, budget, hourly rate, category, skills, client hires, client country, client time zone.
- 🧹 **Filters Upwork does not have**, run on our side: publication date, words a job post must contain, words it must not.
- 🗂️ **Several keywords in one run** — one search each, results deduplicated.
- 🚀 **Fast and cheap.** A run of 250 job posts takes about 20 seconds, 1,000 under a minute — you pay for job posts, not for waiting.
- 🚨 **It tells you when it is wrong.** If Upwork changes its pages, the run stops and says which field vanished, instead of quietly charging you for empty columns.

### 🚀 How to scrape Upwork

1. Click **Try for free**.
2. Type a keyword in **Search keyword** (`python`, `web scraping`, `logo design`) — or paste an Upwork search URL into **Start URLs**.
3. Set **Max job posts** to the number you want.
4. Optionally open **Job filters** and **Client filters** and tick what you need.
5. Hit **Start** and watch the Output tab fill.
6. Download the dataset as JSON, CSV or Excel, or fetch it from the API.

To watch a search instead of scraping it once: turn on **Only new job posts**, give it a **Monitoring key** of its own, and schedule the Actor every hour.

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

This Actor uses **pay per event** pricing: **$0.30 per 1,000 job posts** saved ($0.29 on the Bronze plan, $0.28 on Silver, $0.27 on Gold and above), plus **$0.001 per run start** (10 cents per 100 runs). Platform usage (compute, proxy) is included in the price. Example: 10,000 job posts ≈ $3; an hourly monitor that finds 20 new job posts per run ≈ $5 a month.

**A filtered-out job post is never charged**, and neither is one a previous monitoring run already delivered. A results page on which your filters kept nothing is charged **$0.00015** ($0.15 per 1,000 such pages): a filter that matches almost nothing stops being free to run, never expensive. Pages of job posts already delivered are not charged: a monitoring run without filters that finds nothing new costs only the run-start fee. To cap any run, set **Maximum cost per run** in the run options: the run stops before it would go over.

Upwork itself never returns more than **5 050 job posts for one search**, whatever you set. To go past that, split your run: one search per category, per job type or per budget band.

### ⚙️ Input

You can set everything in the Console form, or pass this JSON to the API. A simple run:

```json
{
    "query": "web scraping",
    "maxItems": 100,
    "sortBy": "recency"
}
```

Hourly work for experienced freelancers, paid at least $40, posted in the last two days:

```json
{
    "query": "python",
    "jobType": "hourly",
    "experienceLevel": ["Intermediate", "Expert"],
    "minHourlyRate": 40,
    "postedAfter": "2 days",
    "excludeKeywords": ["wordpress", "unpaid"],
    "maxItems": 200
}
```

Watching three searches every hour, only the new ones:

```json
{
    "searchQueries": ["web scraping", "data extraction", "crawler"],
    "onlyNew": true,
    "stateKey": "scraping-watch",
    "maxItems": 500,
    "maxItemsPerQuery": 200
}
```

| Field | Notes |
|---|---|
| `startUrls` | Upwork search URLs copied from the site. Max 100 URLs. The keyword, sort and page are read from the URL; the filters below still apply |
| `query`, `searchQueries` | Keyword, and more keywords (one search each), up to 100 per run |
| `maxItems`, `maxItemsPerQuery` | **Max job posts** (100 by default, 0 = no limit), and the cap of each search |
| `sortBy` | `recency` (newest first, the default), `relevance` or `client_total_charge` |
| `jobType` | `hourly` or `fixed`; empty = both |
| `experienceLevel` | `Entry level`, `Intermediate`, `Expert` |
| `projectDuration` | `Less than one month`, `1 to 3 months`, `3 to 6 months`, `More than 6 months` — checked on every job post, fixed-price ones included |
| `workload` | `Less than 30 hrs/week`, `30+ hrs/week`, `As needed` — hourly job posts only, unless a fixed budget is set too |
| `contractToHire` | Only job posts flagged as leading to a long-term contract |
| `minFixedBudget`, `maxFixedBudget` | Fixed price in USD — fixed-price job posts only, unless an hourly filter is set too: then each kind matches its own filter |
| `minHourlyRate`, `maxHourlyRate` | Hourly rate in USD — hourly job posts only, unless a fixed budget is set too |
| `categories`, `subcategories` | Upwork category names, e.g. `Web, Mobile & Software Dev`; a name Upwork does not know stops the run at once and lists the valid ones |
| `skills` | Skills the job post asks for, e.g. `Python`, `Web Scraping`; several = the job post must ask for all of them |
| `clientHires` | `No hires`, `1 to 9 hires`, `10+ hires` |
| `clientLocations` | Country names as Upwork writes them, e.g. `United States` |
| `clientTimezones` | The client's time zone as Upwork names it, e.g. `Europe/Paris`, `America/New_York`; several = any of them |
| `postedAfter`, `postedBefore` | A date (`2026-09-01`, a UTC day, the whole day included) or a period before now (`7 days`, `1 month`; via the API also `2 hours`) |
| `includeKeywords`, `excludeKeywords` | Words a job post must, or must not, contain |
| `maxDescriptionLength` | Cut each description; 0 = the whole text |
| `onlyNew`, `stateKey`, `resetState` | Monitoring mode and its memory |
| `proxyConfiguration`, `minRequestIntervalMs`, `maxRequestRetries`, `debugLog` | Advanced |

### 📦 Output

One item per job post:

```json
{
    "id": "2101592032775799065",
    "ciphertext": "~022101592032775799065",
    "url": "https://www.upwork.com/jobs/_~022101592032775799065/",
    "title": "Fix Broken Python Scraper",
    "description": "Need a quick fix for a broken Python scraper used on an e-commerce site.",
    "skills": ["Python", "Scrapy", "Data Scraping"],
    "skillsDetailed": [{ "uid": "996364628025274386", "name": "Python", "freeText": false }],
    "jobType": "FIXED",
    "budgetType": "FIXED",
    "fixedBudget": 20,
    "hourlyBudgetMin": null,
    "hourlyBudgetMax": null,
    "weeklyRetainerBudget": null,
    "experienceLevel": "Intermediate",
    "contractorTier": "IntermediateLevel",
    "durationLabel": "1 to 3 months",
    "durationWeeks": 9,
    "hourlyEngagementType": null,
    "publishedAt": "2026-09-20T08:40:24.219Z",
    "createdAt": "2026-09-20T08:39:21.353Z",
    "searchQuery": "python",
    "searchUrl": "https://www.upwork.com/nx/search/jobs/?q=python&sort=recency&page=1",
    "scrapedAt": "2026-09-20T09:49:31.185Z"
}
```

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

#### All 23 fields

| Fields | What you get |
| --- | --- |
| `title`, `description` | **Job post**: the text as the client wrote it, line breaks kept |
| `skills`, `skillsDetailed` | Skill names picked from Upwork's list, and the same with Upwork's own ids so you can re-filter. A skill the client typed in by hand is not in Upwork's search, so not here |
| `jobType`, `budgetType` | `FIXED`, `HOURLY` or `WEEKLY_RETAINER` |
| `fixedBudget`, `hourlyBudgetMin`, `hourlyBudgetMax`, `weeklyRetainerBudget` | **Budget**: what the client is offering, in USD |
| `experienceLevel`, `contractorTier` | `Entry level`, `Intermediate` or `Expert`, in words and in Upwork's code |
| `durationLabel`, `durationWeeks`, `hourlyEngagementType` | **Duration and hours**: how long, and how many hours a week |
| `publishedAt`, `createdAt` | ISO 8601 UTC, **to the millisecond** — you can sort a monitoring feed by the second. `publishedAt` = when the post went live (what the date filters read); `createdAt` = what the job page counts "Posted … ago" from, hours earlier for about 1 post in 4 |
| `url`, `id`, `ciphertext` | The public job page and Upwork's own references |
| `searchQuery`, `searchUrl`, `scrapedAt` | Which of your searches found it, and when |

### 💡 Tips

#### How to get more results

Set `maxItems` to `0` and use a broad keyword, or several keywords in **More search keywords**. Upwork stops every search at 5 050 job posts: to go further, split it (see below).

#### How to reduce costs

The price is per job post, so the levers are `maxItems`, `maxItemsPerQuery`, the filters (a filtered-out job post is free; a whole page of them costs $0.00015) and `onlyNew` for recurring runs (you never pay twice for the same job post). With **Sort by: Newest first** and `postedAfter`, a search stops after 5 hours of older posts.

#### Several searches in one run

Put your keywords in **More search keywords**: each one becomes its own search. A job post found by two of them is saved once. **Max job posts per search** stops the first keyword from eating the whole budget. You can also paste several search URLs into `startUrls`: each one is a search of its own, with the same cap.

#### Monitoring: only the new job posts

**Only new job posts** remembers the ids it has delivered under your **Monitoring key**, in a store that never expires (`upwork-jobs-scraper-seen`, up to 150,000 listings per key). Give each watched search its own key. **Reset monitoring memory** forgets everything under that key and returns every match again.

#### Filter by publication date

`postedAfter` and `postedBefore` take a date (`2026-09-01` is a UTC day, and the whole day is included) or a period before now (`7 days`, `2 weeks`, `1 month`; via the API also `2 hours` or a full ISO date-time). The filter reads `publishedAt`. Filtered-out job posts are never charged and do not count in `maxItems`; the run summary tells how many were filtered. With **Sort by: Newest first**, a search stops after 5 hours of older posts (Upwork ranks a few posts up to 3 hours before it publishes them), so a short window costs very little. Upwork shows only the 5 050 newest posts of a search, about the last 31 hours on all of Upwork: `postedBefore` reaches older posts in a narrower search (a keyword, a category).

#### Getting more than 5 050 job posts

That is Upwork's own ceiling for one search, not ours. Split the run: the same keyword with `jobType` `hourly` then `fixed`, or one run per entry of `categories`, or by budget band.

### 🔌 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 job post. Actor id: `nice_dev/upwork-jobs-scraper`; MCP server with this Actor only: `https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/upwork-jobs-scraper`.

Smallest input, for a cheap first call:

```json
{
    "query": "python",
    "maxItems": 10
}
```

Key output fields: `url`, `title`, `description`, `skills`, `jobType`, `fixedBudget`, `hourlyBudgetMin`, `publishedAt`.

Cost: $0.30 per 1,000 job posts plus $0.001 per run start ($0.27 per 1,000 on the Gold plan); a results page on which the filters kept nothing costs $0.00015, 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 describe job posts written by clients and can contain personal data, 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 (without a proxy, after a pause of 5 seconds, doubled at each retry up to 150 seconds).

#### Does it return the client's spending, rating or number of proposals?

**No.** Those figures are not part of what Upwork shows an anonymous visitor on its search pages, so this Actor does not return them. If you need the client's spending history or the number of proposals, this is not the Actor for you — and we would rather say so here than let you find out after a paid run.

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

Titles and descriptions are the clients' own words, copied as they are. A text can begin with `-`, `+`, `=` or `@` (`-20% rate for a long contract`, `=SUM of my needs`): 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. Every URL field holds an `https://www.upwork.com/…` address built from Upwork's own reference, never a link copied from a payload. On a web page, escape every field like any text written by a stranger.

#### Known limitations

- **5 050 job posts per search**, Upwork's own ceiling (a search URL pasted past page 101 returns nothing). Split the run to go further.
- Only the keyword, the sort and the page are read from a pasted search URL: set the filters in the form, they apply to pasted URLs too.
- No client information, no proposal count, no "payment verified" flag — see above.
- There is no filter for the number of proposals or for payment verification, because Upwork's search offers none.
- `onlyNew` remembers job ids, not their content: a job post the client edited is not returned again.
- Two runs sharing the same `stateKey` at the same time may both return the same new job post.

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

- 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 job posts already saved are skipped: none is delivered or charged twice, and `maxItems` still counts them.
- With `onlyNew`, the memory is saved once a minute: resurrect the stopped run and the job posts it had saved meanwhile join the memory; leave it stopped for good, and the next run may return up to a minute of them once more.

#### Something doesn't work?

The last line of the log counts the job posts saved, filtered out and already delivered, and the requests that failed after every retry. Those requests 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 (a search URL that does not exist says so, instead of "run it again"). A run that saved job posts can still fail: when at least as many requests failed for good as were read (page 1 read, the next pages blocked), it keeps what it saved and fails with "Most requests failed", so that a blocked run never looks like a complete one.

If Upwork changes its pages, you are told instead of paying for blank rows. A results page that counts job posts but gives none the Actor can read is an error (listed in `FAILED_REQUESTS`), never a quiet "No listings found". If the first 20 listings read all lack a column every job post fills (`title`, `description`, `publishedAt`, `createdAt`, `jobType`, `experienceLevel`, `durationLabel`, `durationWeeks`, `skills`, `ciphertext`, and `fixedBudget` or `hourlyEngagementType` on the posts of that kind), the run saves nothing more, stops and fails, and its last message names the missing field: at most those first listings are charged. A job post that `postedAfter` / `postedBefore` drops because it has no date at all counts among those 20.

### 🛟 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 search URLs copied from the site (`https://www.upwork.com/nx/search/jobs/?q=python&sort=recency`). Only the keyword, the sort and the page are read from the URL: filters ticked on Upwork's page are not, so set them below — they apply to these URLs too. When this list is not empty, **Search keyword** below is ignored; the caps and monitoring below still apply. Max 100 URLs.

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

Free-text search, exactly as typed on Upwork. Upwork's own search syntax works: `title:(web scraping)`, `python AND NOT wordpress`, `"exact phrase"`. Empty = every open job post (about 108 000).

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

Several keywords in one run: one search per keyword. Added to **Search keyword**. A job post found by several searches is saved once. Max 100.

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

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

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

Cap for EACH search, so the first keyword cannot use up the whole **Max job posts** budget. 0 = no per-search cap. Upwork itself never returns more than 5 050 job posts for one search, whatever you set here.

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

Upwork's own sort order. `recency` (newest first) is the only one that makes **Only new job posts** and early stopping work — keep it for monitoring.

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

Hourly or fixed-price contracts. Empty = both.

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

Client's required level. Several can be ticked.

## `projectDuration` (type: `array`):

How long the client expects the work to last. Checked on every job post, fixed-price ones included.

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

Workload the client asks for. Hourly posts only: fixed-price posts have no hours per week, so they drop out unless a fixed budget is set too.

## `contractToHire` (type: `boolean`):

Only job posts the client flagged as leading to a long-term contract.

## `minFixedBudget` (type: `integer`):

Only fixed-price job posts with a budget at or above this. Hourly posts drop out, unless an hourly rate or hours per week is set too: then you get both kinds, each matching its own filter.

## `maxFixedBudget` (type: `integer`):

Upper bound of the fixed budget. Can be used alone.

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

Only hourly job posts whose rate range reaches this. Fixed-price posts drop out, unless a fixed budget is set too: then you get both kinds, each matching its own filter.

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

Upper bound of the hourly rate. Can be used alone.

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

Upwork category names as shown on the site, e.g. `Web, Mobile & Software Dev`, `Design & Creative`, `Sales & Marketing`. The list of categories is read from Upwork at the start of the run, so it is never out of date. An unknown name stops the run with the list of valid ones.

## `subcategories` (type: `array`):

Upwork subcategory names, e.g. `Web Development`, `Data Extraction`. More precise than **Categories**; both can be used together.

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

Skills the job post asks for, as Upwork names them, e.g. `Python`, `Web Scraping`, `Graphic Design` (case ignored). Several skills = the job post must ask for ALL of them. Each name is looked up on Upwork at the start of the run; an unknown name stops the run with the closest names Upwork knows.

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

How many freelancers the client has hired before.

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

Country names as Upwork writes them, e.g. `United States`, `United Kingdom`, `Canada`. Several countries = job posts from any of them.

## `clientTimezones` (type: `array`):

The client's time zone, as Upwork names it, e.g. `Europe/Paris`, `America/New_York`, `Asia/Calcutta`. Several zones = job posts from any of them. A zone with no open job post right now is kept and said in the log.

## `postedAfter` (type: `string`):

Only job posts published on or after this date (days are UTC): `2026-09-01`, or a period before now such as `7 days`, `2 weeks`, `1 month` (API: `2 hours` and full ISO date-times work too). With **Sort by: Newest first**, the run stops after 5 hours of older posts (Upwork ranks a few posts up to 3 hours before it publishes them), so a short window costs very little.

## `postedBefore` (type: `string`):

Only job posts published on or before this date (days are UTC; the whole day is included), or older than a period such as `30 days`. Same formats as **Posted after**. Upwork shows only the 5 050 newest posts of a search (on all of Upwork, about the last 31 hours): to reach older ones, narrow the search with a keyword or a category.

## `includeKeywords` (type: `array`):

Keep only job posts whose title, description or skills contain at least one of these words. Case and accents are ignored. Runs on our side, after Upwork's own search.

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

Drop job posts whose title, description or skills contain any of these words, e.g. `wordpress`, `unpaid`. Case and accents are ignored.

## `maxDescriptionLength` (type: `integer`):

Cut each description to this many characters (never inside an emoji). 0 = keep the full text, which is what Upwork returns.

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

Return only the job posts never delivered by a previous run with the same **Monitoring key**. A run that finds nothing new saves 0 items and is charged only the run-start fee.

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

Name of the memory used by **Only new job posts**. Use a different key per watched search so two searches do not hide each other's results.

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

Forget everything remembered under this **Monitoring key** before starting, so this run returns every match again.

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

Apify Proxy, included in the price. Keep the default setting: the residential proxy is refused, because it would cost you money this Actor does not need.

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

Milliseconds waited between two requests to Upwork. 500 ms is the measured safe pace (no refusal over 120 calls). Lowering it risks 403s; raising it only makes the run slower.

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

How many times a failed request is retried before it is listed as failed.

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

Verbose log. Useful when opening a support ticket.

## Actor input object example

```json
{
  "startUrls": [],
  "query": "python",
  "searchQueries": [],
  "maxItems": 100,
  "maxItemsPerQuery": 0,
  "sortBy": "recency",
  "jobType": "",
  "experienceLevel": [],
  "projectDuration": [],
  "workload": [],
  "contractToHire": false,
  "categories": [],
  "subcategories": [],
  "skills": [],
  "clientHires": [],
  "clientLocations": [],
  "clientTimezones": [],
  "includeKeywords": [],
  "excludeKeywords": [],
  "maxDescriptionLength": 0,
  "onlyNew": false,
  "stateKey": "default",
  "resetState": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "minRequestIntervalMs": 500,
  "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",
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

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

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

```

## MCP server setup

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