# Upwork Job Scraper | Client Intel & Proposals (`corvuslab/upwork-job-scraper`) Actor

Scrape Upwork jobs by keyword, budget, experience or client quality. Every job arrives fully enriched — client country, lifetime spend, rating, hires, average rate paid and exact proposal counts — with incremental monitoring and alerts. No add-ons, no Upwork account.

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

## Pricing

from $2.00 / 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.

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 Job Scraper 💼

Extract structured job data from **[Upwork](https://www.upwork.com)** — with the full
**client-quality panel** (country, lifetime spend, payment verification, rating,
review count), **exact proposal counts**, full descriptions, incremental change
tracking and built-in notifications.

**No Upwork account, cookie or session token required.** The client panel and the
exact number of proposals come back on every public job, at no extra per-job fee.

Great for freelancer lead generation, market and rate research, competitive analysis,
and feeding live job data into dashboards or AI agents.

### ✨ Key features

- 🏅 **Client intelligence on every job** — country, city, timezone, lifetime spend,
  payment-verified flag, rating, review count, total hires, hours billed, industry,
  company size and member-since date.
- 💵 **What the client actually pays** — `clientAvgHourlyRatePaid` is their average
  hourly rate across past contracts, so you can spot a low-paying client before
  spending Connects. Present for clients with hourly history.
- 🧩 **Project shape** — every job is tagged Ongoing, One-time or Complex project.
- 📉 **Exact proposal counts** — the real number of applicants, not a "5 to 10" bucket,
  plus hires, interviews and invitations sent.
- 🎯 **Low-competition filtering** — combine proposal brackets with client-quality
  gates to surface jobs worth writing a proposal for.
- 🔍 **Batch searches** — many keyword queries in one run, deduplicated by job ID so
  overlapping searches never bill you twice.
- 🔗 **Paste-a-URL mode** — drop in any Upwork search URL and every filter in it is
  read and replayed automatically.
- 🧮 **Native Upwork operators** — all-words, any-words, exact-phrase, none-of-these
  and title-only search run **server-side**, so excluded jobs never reach your bill.
- 🔁 **Incremental mode** — recurring runs emit only what changed. Unchanged jobs are
  recognised *before* their detail page is fetched, so you skip the work and the cost.
- ♻️ **Repost detection** — flag or drop jobs re-listed under a new ID.
- 🔔 **Notifications** — push matching jobs to Telegram, Slack, Discord or any webhook.
- 🪶 **AI-friendly output** — `compact` mode, description truncation and
  `excludeEmptyFields` for lean records in LLM and MCP pipelines.
- ⚡ **Fast and cheap** — talks to Upwork's JSON API directly, so there is no browser
  to boot and no HTML to parse. Each job transfers a few KB instead of a rendered page,
  and detail pages are fetched in parallel across multiple proxy IPs.

### 📥 Input

| Field | Type | Description |
|---|---|---|
| `query` | string | Keywords, e.g. `"python"`. Leave blank to browse everything. |
| `queries` | array | Several keyword searches in one run. |
| `searchUrls` | array | Upwork search URLs pasted from your browser; their filters are parsed. |
| `maxResults` | integer | Max jobs per search. Upwork caps any search at 5,000; `0` takes the max. Default 50. |
| `sort` | enum | `recency` (default), `relevance`, `client_total_charge`, `client_rating`. |
| `jobType` | enum | `hourly` or `fixed`. |
| `experienceLevel` | array | `entry`, `intermediate`, `expert`. |
| `category` | enum | Top-level Upwork category, or a raw category UID. |
| `budget` | enum | Fixed-price bracket: `0-99`, `100-499`, `500-999`, `1000-4999`, `5000+`. |
| `hourlyRate` | string | Hourly range, e.g. `"30-80"` or `"50-"` for $50/hr and up. |
| `workload` | enum | `as_needed`, `part_time`, `full_time`. |
| `duration` | enum | `week`, `month`, `semester`, `ongoing`. |
| `contractToHire` | boolean | Only jobs that may convert to a full-time role. |
| `location` | string | Client country or region in full, e.g. `"United States"`, `"Europe"`. |
| `skills` | string | Upwork skill tags, comma-separated. |
| `verifiedPaymentOnly` | boolean | Only clients with a verified payment method. |
| `minClientTotalSpent` | integer | Minimum client lifetime spend (USD). |
| `minClientRating` | integer | Minimum client rating, 0–5. |
| `minClientReviewCount` | integer | Minimum number of client reviews. |
| `clientHires` | enum | `0`, `1-9`, `10+` previous hires. |
| `proposals` | array | Competition brackets: `0-4`, `5-9`, `10-14`, `15-19`, `20-49`, `50+`. |
| `excludeLocations` | array | Drop jobs from these client countries. |
| `allWords` / `anyWords` / `exactPhrase` / `noneWords` / `titleSearch` | string | Upwork's native AND / OR / exact / NOT / title-only search. |
| `includeKeywords` / `excludeKeywords` | array | Keep or drop jobs matching these terms. |
| `keywordMatchTitle` / `keywordMatchDescription` / `keywordMatchSkills` | boolean | Limit keyword matching to certain fields. None on = all fields. |
| `fromDate` / `toDate` | string | Keep jobs posted in this window (`YYYY-MM-DD`; `toDate` inclusive). |
| `maxAgeMinutes` | integer | Keep only jobs posted within N minutes before the run. |
| `customFilters` | array | Rule-based filters on any output field, e.g. `[{"field":"clientTotalSpent","operator":"gte","value":10000}]`. Operators: includes, notIncludes, equals, notEquals, gt, gte, lt, lte, exists, notExists. |
| `enrichDetails` | boolean | Fetch each job's detail page for the client panel and proposal count. Default `true`. |
| `detailConcurrency` | integer | Parallel detail requests, 1–30. Each uses its own proxy IP. Default 20. |
| `descriptionFormat` | enum | `all`, `text`, `html`, `markdown`. |
| `descriptionMaxLength` | integer | Truncate descriptions (0 = full text). |
| `compact` | boolean | Core fields only. |
| `excludeEmptyFields` | boolean | Drop null/empty fields. |
| `incrementalMode` | boolean | Emit only NEW / UPDATED jobs on recurring runs. |
| `stateKey` | string | Stable name for the tracking state (auto-derived if blank). |
| `emitUnchanged` / `emitExpired` | boolean | Also emit UNCHANGED / EXPIRED jobs. |
| `skipReposts` | boolean | Detect and drop reposts. |
| `telegramToken` + `telegramChatId` | string | Notify Telegram (BotFather token + chat ID). |
| `slackWebhookUrl` / `discordWebhookUrl` | string | Notify a Slack / Discord incoming webhook. |
| `webhookUrl` + `webhookHeaders` | string / object | POST structured JSON anywhere, with optional headers. |
| `notificationLimit` | integer | Max jobs listed per message (default 5). |
| `notifyOnlyChanges` | boolean | Only alert on changed jobs (needs incremental mode). |
| `includeRunSummary` | boolean | Prepend a one-line summary to notifications. Default `true`. |
| `sessionToken` | string | Optional. Unlocks the ~10% of jobs whose details are account-gated. |
| `proxyConfiguration` | object | Apify proxy settings. Datacenter is plenty. |

#### Example input

```json
{
    "query": "react developer",
    "maxResults": 100,
    "sort": "recency",
    "jobType": "hourly",
    "experienceLevel": ["intermediate", "expert"],
    "hourlyRate": "40-",
    "verifiedPaymentOnly": true,
    "minClientTotalSpent": 5000,
    "proposals": ["0-4", "5-9"]
}
```

### 📤 Output

Each job is pushed to the run's default dataset. Example record (abridged):

```json
{
    "jobId": "2082001692817092210",
    "title": "React/Supabase Developer Needed for SAAS Web Application",
    "url": "https://www.upwork.com/jobs/~022082001692817092210",
    "jobType": "HOURLY",
    "experienceLevel": "ExpertLevel",
    "hourlyBudgetMin": 40,
    "hourlyBudgetMax": 100,
    "budgetAmount": null,
    "budgetCurrency": "USD",
    "engagementType": "FULL_TIME",
    "engagementDuration": "3 to 6 months",
    "engagementDurationWeeks": 18,
    "projectType": "Ongoing project",
    "projectTypeCode": "EMPLOYMENT_POSITION",
    "skills": ["React", "JavaScript", "Supabase", "API"],
    "category": "Web Development",
    "categoryGroup": "Web, Mobile & Software Dev",
    "publishTime": "2026-07-28T07:16:01.685Z",

    "clientCountry": "Poland",
    "clientCity": "Radwanice",
    "clientTimezone": "Europe/Berlin (UTC+02:00)",
    "clientTotalSpent": 1265.14,
    "clientRating": 5,
    "clientReviewCount": 5,
    "clientTotalHires": 8,
    "clientActiveHires": 1,
    "clientTotalHours": 214.5,
    "clientJobsWithHires": 7,
    "clientAvgHourlyRatePaid": 24.98,
    "clientPaymentVerified": true,
    "clientIndustry": "Automotive",
    "clientCompanySize": 1,
    "clientMemberSince": "2023-05-19T00:00:00.000Z",

    "totalApplicants": 22,
    "totalHired": 0,
    "totalInterviewing": 0,
    "personsToHire": 1,
    "lastClientActivity": "2026-07-28T08:14:48.977Z",

    "minJobSuccessScore": 0,
    "preferredEnglishLevel": "ANY",
    "preferredFreelancerType": "ANY",
    "description": "…",
    "descriptionMarkdown": "…",
    "detailsFetched": true,
    "searchQuery": "react developer",
    "scrapedAt": "2026-07-28T08:27:26.541Z"
}
```

The Output tab also ships three ready-made views: **Overview** (title, budget,
proposals, client quality), **Client intelligence** (the full client panel) and
**Changes** (the incremental tracking fields).

In **compact** mode each record is trimmed to the 17 fields needed to identify, price
and triage a job: `jobId`, `title`, `url`, `jobType`, `experienceLevel`, `budgetAmount`,
`hourlyBudgetMin`/`Max`, `skills`, `publishTime`, `totalApplicants`, `clientCountry`,
`clientTotalSpent`, `clientPaymentVerified`, `clientRating`, `searchQuery` and
`changeType`.

#### Incremental fields

With `incrementalMode` on, every record also carries:

- `changeType` — `NEW`, `UPDATED`, `UNCHANGED` or `EXPIRED`
- `firstSeenAt` / `lastSeenAt` — when the job was first and most recently seen
- `contentHash` — hash of the tracked fields
- `isRepost` / `repostOfId` / `repostDetectedAt` — set when repost detection matches an
  earlier listing

### 🔁 Incremental mode & monitoring savings

Running on a schedule? Turn on `incrementalMode`. The **first run builds a baseline**;
every run after that emits — and charges for — **only the jobs that actually changed**.

Unchanged jobs are recognised from the search response alone, *before* their detail
page is fetched, so you skip both the work and the cost.

Two consecutive runs of the same search, where nothing has changed in between:

| | First run (baseline) | Second run (nothing changed) |
|---|---|---|
| Jobs found | all of them | all of them |
| Detail pages fetched | all | **none** |
| Records emitted & charged | all | **none** |

The second run still *checks* every job — it just recognises each one as unchanged and
charges for none of them.

Because this actor is pay-per-result, your cost after the baseline scales with how much
the market actually changes ("churn"), not with how many listings you monitor:

| Daily churn | Records emitted per run (after baseline) | Approx. saving vs a full re-scrape |
|---|---|---|
| 5% (stable niche) | ~5% | ~90% |
| 15% (moderate) | ~15% | ~80% |
| 30% (fast-moving) | ~30% | ~65% |

Set `emitExpired` to also receive listings that have dropped off, and `emitUnchanged`
if you want the full set every run. Keep `maxResults` consistent between scheduled
runs — it defines the set of jobs being tracked.

### 🔔 Notifications

Get alerted the moment matching jobs appear. Fill in only the channels you want; each
fires independently when a run finishes.

- ✈️ **Telegram** — `telegramToken` (from @BotFather) + `telegramChatId`.
- 💬 **Slack** — `slackWebhookUrl` (Incoming Webhook).
- 🎮 **Discord** — `discordWebhookUrl` (channel webhook).
- 🪝 **Generic webhook** — `webhookUrl` receives structured JSON (run metadata plus a
  `jobs` array) with optional `webhookHeaders`. Ideal for n8n / Make / Zapier.

Pair `maxAgeMinutes` with a schedule for near-real-time alerts, or `incrementalMode` +
`notifyOnlyChanges` to hear only about genuinely new work.

### 💡 Use cases

- **Find work before it gets crowded** — sort by newest, filter to 0–4 proposals and
  payment-verified clients with real spend history.
- **Qualify clients before you spend Connects** — lifetime spend, rating, review count,
  hire rate and last-activity time on every job.
- **Rate and market research** — track hourly ranges and budgets by skill or category
  over time.
- **Skill-demand analysis** — measure which technologies are actually being hired for.
- **Agency lead generation** — build outreach lists from client country, industry and
  spend.
- **Feed AI agents** — compact JSON straight into an LLM, MCP tool or vector store.

### 💰 Pricing

**Pay-per-event — two events, no add-ons.**

| Event | Price |
|---|---|
| Actor start | **$0.001** per run |
| Job record | **$0.002** per job |

| Run | Cost |
|---|---|
| 10 jobs | $0.021 |
| 100 jobs | $0.201 |
| 1,000 jobs | $2.001 |

$0.002 buys the **whole record**, not a starting point. Every job arrives with the
full client panel — country, city, timezone, lifetime spend, rating, review count,
total hires, hours billed, industry, company size, and the average hourly rate that
client actually pays — plus the exact proposal count and the freelancer requirements.

There is **no enrichment add-on, no per-field surcharge and no session cookie to set
up**. What you see in the output example above is what a single $0.002 record
contains, on a run you can start in one click.

You only pay for jobs you actually receive. Server-side filters (`allWords`,
`noneWords`, `exactPhrase`, `titleSearch`) discard non-matching jobs *before* they are
billed, and with `incrementalMode` on, a run that finds nothing new costs only the
$0.001 start fee.

### ❓ FAQ

**Do I need an Upwork account or login?** No. The client panel, proposal counts and
descriptions all come from Upwork's public data. The optional `sessionToken` only adds
the minority of jobs whose detail pages are account-gated.

**Why is `detailsFetched` false on some jobs?** Around 10% of listings hide their
detail page from logged-out visitors. Those records still carry every listing-level
field (title, budget, skills, dates); only the client panel is missing. Supply a
`sessionToken` if you need 100% coverage.

**Why do some jobs have no `clientTotalSpent`?** Clients can enable financial privacy,
which hides lifetime spend from everyone. Similarly, `clientIndustry` and
`clientCompanySize` are only present when the client has filled in a company profile.

**What client data is *not* included?** The aggregate hiring history is complete —
total hires, active hires, hours billed, jobs with hires, rating, review count and
member-since. The one thing Upwork does not expose publicly is the client's
**itemised contract history** (each past job with its individual feedback text); that
sits behind an authenticated scope, so no logged-out scraper can reach it.

**Can I get more than 5,000 results from one search?** No — Upwork itself refuses
paging past 5,000. Split the search (by category, budget bracket or date window) to
cover more ground.

**Does it handle large runs reliably?** Yes. Upwork limits requests per IP, so the
actor spreads a run across one proxy IP per concurrent worker and lets faster IPs take
more of the load. Transient failures are retried with backoff rather than dropped, so
a large run finishes with its client panel intact instead of silently half-empty.
Raise `detailConcurrency` for more parallelism on very large runs.

**Why are `verifiedPaymentOnly` and `proposals` applied after fetching?** Upwork only
honours those filters for signed-in users, so this actor evaluates them against the
client panel instead. It switches detail enrichment on automatically when you use them.

**Is it legal to scrape Upwork?** This actor extracts publicly available data. Web
scraping of public information is generally considered legal, but you should review
the target site's terms of service and ensure your use case complies with applicable
laws and regulations, including GDPR where relevant.

***

*This is an unofficial actor and is not affiliated with, endorsed by, or connected to
Upwork. "Upwork" is a trademark of its respective owner and is used here only to
describe the site this actor works with.*

# Actor input Schema

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

Keywords to search for, e.g. "python" or "shopify developer".

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

Run several keyword searches in one run. Results are deduplicated by job ID across all queries, so overlapping searches never bill you twice.

## `searchUrls` (type: `array`):

Paste Upwork search URLs straight from your browser, e.g. https://www.upwork.com/nx/search/jobs/?q=react\&t=1. Every filter in the URL is read and applied automatically.

## `maxResults` (type: `integer`):

Maximum jobs to return per search. Upwork caps any single search at 5,000 results; set 0 to take that maximum.

## `sort` (type: `string`):

Result ordering. "Newest first" is best for monitoring; "Most relevant" is best for one-off research.

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

Hourly or fixed-price contracts.

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

Select one or more; leave empty for any level.

## `category` (type: `string`):

Top-level Upwork category. You can also paste a raw category UID if you need a narrower one.

## `budget` (type: `string`):

Budget bracket for fixed-price jobs (USD).

## `hourlyRate` (type: `string`):

Hourly budget range in USD, written as "min-max", e.g. "30-80". Open-ended works too: "50-" means $50/hr and up.

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

How many hours per week the client expects.

## `duration` (type: `string`):

Expected engagement length.

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

Only jobs the client may convert into a full-time role.

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

Filter by the client's country or region, written in full, e.g. "United States", "Ukraine", "Europe". Two-letter codes are not accepted by Upwork.

## `skills` (type: `string`):

Filter by Upwork skill tags, comma-separated, e.g. "react,typescript".

## `verifiedPaymentOnly` (type: `boolean`):

Drop jobs from clients who have not verified a payment method.

## `minClientTotalSpent` (type: `integer`):

Only clients who have spent at least this much on Upwork (USD).

## `minClientRating` (type: `integer`):

Only clients rated at or above this score (0–5).

## `minClientReviewCount` (type: `integer`):

Only clients with at least this many reviews — useful for filtering out brand-new accounts.

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

How many freelancers the client has hired before.

## `proposals` (type: `array`):

Keep only jobs in these proposal-count brackets. Pair the low brackets with "Newest first" to find jobs before they get crowded.

## `excludeLocations` (type: `array`):

Drop jobs from these client countries, written in full, e.g. "India", "Pakistan".

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

Keep a job only if it matches at least one of these terms.

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

Drop any job matching one of these terms.

## `keywordMatchTitle` (type: `boolean`):

Restrict keyword matching to the job title. If none of the three match toggles is on, all fields are searched.

## `keywordMatchDescription` (type: `boolean`):

Restrict keyword matching to the job description. If none of the three match toggles is on, all fields are searched.

## `keywordMatchSkills` (type: `boolean`):

Restrict keyword matching to the job's skill tags. If none of the three match toggles is on, all fields are searched.

## `allWords` (type: `string`):

Upwork's native AND search — every word must appear.

## `anyWords` (type: `string`):

Upwork's native OR search — at least one word must appear.

## `exactPhrase` (type: `string`):

Match the phrase exactly, e.g. "machine learning".

## `noneWords` (type: `string`):

Upwork's native NOT search — filtered server-side, before you pay for the result.

## `titleSearch` (type: `string`):

Match these keywords against the job title only.

## `fromDate` (type: `string`):

Keep jobs posted on or after this date (YYYY-MM-DD or ISO-8601).

## `toDate` (type: `string`):

Keep jobs posted on or before this date (inclusive).

## `maxAgeMinutes` (type: `integer`):

Keep only jobs posted within this many minutes before the run. Ideal for near-real-time alerting; 0 disables it.

## `customFilters` (type: `array`):

Rule-based filters on any output field, e.g. \[{"field":"clientTotalSpent","operator":"gte","value":10000}]. Operators: includes, notIncludes, equals, notEquals, gt, gte, lt, lte, exists, notExists.

## `enrichDetails` (type: `boolean`):

Fetch each job's detail page to add the client panel (country, lifetime spend, rating, reviews, payment verification), the exact proposal count and the freelancer requirements. No Upwork account needed, and no extra per-job fee. Turn off for the fastest possible listing-only run.

## `detailConcurrency` (type: `integer`):

Parallel detail requests. Each one uses its own proxy IP, so higher values spread the load across more IPs. 20 is the measured sweet spot; throughput flattens above it.

## `descriptionFormat` (type: `string`):

Which description variants to include in each record.

## `descriptionMaxLength` (type: `integer`):

Cut descriptions to this many characters (0 = keep the full text). Useful for keeping LLM context small.

## `compact` (type: `boolean`):

Return only the core fields — ideal for AI agents and MCP pipelines.

## `excludeEmptyFields` (type: `boolean`):

Remove null and empty values from each record.

## `incrementalMode` (type: `boolean`):

The first run builds a baseline; later runs emit only NEW / UPDATED jobs. Unchanged jobs are recognised before their detail page is fetched, so you skip both the work and the cost.

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

Stable name for the tracking state. Leave blank to derive it from your search settings automatically.

## `emitUnchanged` (type: `boolean`):

Return the full result set every run instead of only the changes.

## `emitExpired` (type: `boolean`):

Emit a record for jobs that were tracked last run but have since dropped off.

## `skipReposts` (type: `boolean`):

Detect jobs re-listed under a new ID and drop them. Kept jobs are tagged with isRepost / repostOfId either way.

## `telegramToken` (type: `string`):

Bot token from @BotFather.

## `telegramChatId` (type: `string`):

Chat, group or channel ID to post into.

## `slackWebhookUrl` (type: `string`):

Slack Incoming Webhook URL.

## `discordWebhookUrl` (type: `string`):

Discord channel webhook URL.

## `webhookUrl` (type: `string`):

Receives a structured JSON payload (run metadata plus a jobs array). Ideal for n8n, Make or Zapier.

## `webhookHeaders` (type: `object`):

Extra HTTP headers for the generic webhook, e.g. an Authorization header.

## `notificationLimit` (type: `integer`):

How many jobs to list in each message. The total matched count is always reported.

## `notifyOnlyChanges` (type: `boolean`):

Only alert on NEW / UPDATED jobs. Requires incremental mode.

## `includeRunSummary` (type: `boolean`):

Prepend a one-line summary to each notification.

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

This actor talks to Upwork's JSON API and transfers only a few KB per job, so the default datacenter proxy is plenty. Switch to residential only if you see repeated blocks.

## `sessionToken` (type: `string`):

Optional. Roughly 10–20% of jobs hide their detail page from logged-out visitors. Supplying the oauth2\_global\_js\_token cookie value from a logged-in Upwork session unlocks those too. Everything else works without it.

## `visitorClientId` (type: `string`):

Override for Upwork's public visitor OAuth2 client ID. Only needed if Upwork rotates it and this actor has not been updated yet.

## `visitorClientSecret` (type: `string`):

Override for Upwork's public visitor OAuth2 client secret. See above.

## Actor input object example

```json
{
  "query": "python",
  "maxResults": 50,
  "sort": "recency",
  "jobType": "",
  "category": "",
  "budget": "",
  "workload": "",
  "duration": "",
  "contractToHire": false,
  "verifiedPaymentOnly": false,
  "clientHires": "",
  "keywordMatchTitle": false,
  "keywordMatchDescription": false,
  "keywordMatchSkills": false,
  "maxAgeMinutes": 0,
  "enrichDetails": true,
  "detailConcurrency": 20,
  "descriptionFormat": "all",
  "descriptionMaxLength": 0,
  "compact": false,
  "excludeEmptyFields": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "skipReposts": false,
  "notificationLimit": 5,
  "notifyOnlyChanges": false,
  "includeRunSummary": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

All scraped jobs as JSON (the run's default dataset).

## `jobsOverview` (type: `string`):

Key columns only — title, budget, proposals, client quality.

## `clientIntelligence` (type: `string`):

Per-job client panel: country, spend, average hourly rate paid, rating, reviews, hires, industry, verification.

## `changes` (type: `string`):

For incremental runs: what changed since last time — NEW, UPDATED, UNCHANGED or EXPIRED, with first/last seen timestamps and repost flags.

# 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"
};

// Run the Actor and wait for it to finish
const run = await client.actor("corvuslab/upwork-job-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" }

# Run the Actor and wait for it to finish
run = client.actor("corvuslab/upwork-job-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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"
}' |
apify call corvuslab/upwork-job-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=corvuslab/upwork-job-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RN9gI0nbPxuH35akp/builds/BB8AlBRAcg366XPUC/openapi.json
