# Upwork Job Scraper (Pay Per Result) (`matthewjames/upwork-job-scraper-pay-per-result`) Actor

Scrape recent and relevant Upwork.com jobs, filter and only pay for what you need. Unauthed runs = no ban risk.

- **URL**: https://apify.com/matthewjames/upwork-job-scraper-pay-per-result.md
- **Developed by:** [Matthew James](https://apify.com/matthewjames) (community)
- **Categories:** Automation, Lead generation, Jobs
- **Stats:** 78 total users, 6 monthly users, 83.2% runs succeeded, 4 bookmarks
- **User rating**: No ratings yet

## Pricing

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

## 🔎 Upwork Job Scraper

Find Upwork jobs by keyword or search URL. Download results or send them to your workflow. Upwork applies native search filters; use your own workflow for additional filtering.

**No Upwork login needed for basic results.** Add your Upwork authorization header or cookies for available client information. Turn on **Add Extra Details** if you also want job requirements, client history, and hiring activity.

### 📦 What you get

#### 🧾 Example output

##### Without authentication — 16 standard fields

Client information and proposal counts are `null`. There is no `Extra Details` field.

```json
{
    "Job ID": "1000000000000000001",
    "Time Posted": "2026-09-04T14:18:13.670Z",
    "Project Payment Type": "Hourly",
    "Budget": "$8.0 - $12.0",
    "Skill Level": "Intermediate",
    "Title": "Example automation job",
    "URL": "https://www.upwork.com/jobs/~example1",
    "Description": "Build a Node.js integration with clear tests.",
    "Location": null,
    "Total Spent": null,
    "Feedback": null,
    "Proposals": null,
    "Project Length": "1 to 3 months",
    "Weekly Hours": "Less than 30 hrs/week",
    "Skills": ["Node.js", "API Integration"],
    "Date Scraped": "2026-09-12T12:00:00.000Z"
}
```

##### With authentication and Add Extra Details

Authentication fills in available client values in the same 16 standard fields. The example below also has **Add Extra Details** turned on. Without that add-on, the `Extra Details` key is omitted. Some values may still be `null`.

```json
{
    "Job ID": "1000000000000000003",
    "Time Posted": "2026-08-21T17:52:00.733Z",
    "Project Payment Type": "Hourly",
    "Budget": "$30.0 - $123.0",
    "Skill Level": "Intermediate",
    "Title": "Example automation job",
    "URL": "https://www.upwork.com/jobs/~example3",
    "Description": "Build a Node.js integration with clear tests.",
    "Location": "United States",
    "Total Spent": "$18,854.87",
    "Feedback": 4.97,
    "Proposals": 8,
    "Project Length": "Less than 1 month",
    "Weekly Hours": "Less than 30 hrs/week",
    "Skills": ["Node.js", "API Integration"],
    "Date Scraped": "2026-09-12T12:00:00.000Z",
    "Extra Details": {
        "Job": {
            "Status": "Active",
            "Start Date": null,
            "Category": "Scripts & Utilities",
            "Category Group": "Web, Mobile & Software Dev",
            "Occupation": "Scripting & Automation",
            "Additional Skills": [],
            "Tools": ["Zapier"],
            "Screening Questions": ["Describe a similar integration you have built."],
            "Attachments": []
        },
        "Client": {
            "City": "Austin",
            "Timezone": "America/Chicago",
            "Payment Verified": true,
            "Enterprise": false,
            "Company": null,
            "Review Count": 12,
            "Assignments": 15,
            "Active Assignments": 2,
            "Hours Billed": 250,
            "Jobs Posted": 18,
            "Open Jobs": 2,
            "Jobs With Hires": 10,
            "Average Hourly Rate": null,
            "Work History": []
        },
        "Requirements": {
            "Freelancer Type": "Independent",
            "English Level": "Fluent",
            "Languages": ["English"],
            "Min Job Success Score": 90,
            "Min Upwork Hours": 100,
            "Portfolio Requested": false,
            "Rising Talent Preferred": false,
            "Countries": null,
            "Regions": null,
            "States": null,
            "Timezones": null,
            "Location Check Required": false,
            "Location": null,
            "On Site": null
        },
        "Activity": {
            "Last Client Activity At": "2026-09-12T11:30:00.000Z",
            "Hires": 0,
            "Interview Invites": 2,
            "Unanswered Invites": 1,
            "Invites Sent": 3,
            "Positions To Hire": 1
        },
        "Bid Stats": {
            "Average": {
                "Amount": 50,
                "Currency": "USD"
            },
            "Lowest": {
                "Amount": 30,
                "Currency": "USD"
            },
            "Highest": {
                "Amount": 100,
                "Currency": "USD"
            }
        }
    }
}
```

Each job is one row with these **16 standard fields**:

| Group               | Output fields                                           |
| ------------------- | ------------------------------------------------------- |
| Job                 | Job ID, Title, URL, Description, Skills                 |
| Pay and experience  | Project Payment Type, Budget, Skill Level               |
| Schedule            | Project Length, Weekly Hours, Time Posted, Date Scraped |
| Client and activity | Location, Total Spent, Feedback, Proposals              |

**Location** means the client's country, not where applicants must live. Without authentication, client and proposal values are unavailable. The standard fields remain present.

With valid authentication and **Add Extra Details** enabled, **Extra Details** is added to the same row:

| Group        | Examples of available details                      |
| ------------ | -------------------------------------------------- |
| Job          | Category, screening questions, attachment links    |
| Client       | Payment status, work history, jobs posted          |
| Requirements | Languages, location rules, experience requirements |
| Activity     | Hires, invitations, recent client activity         |
| Bid Stats    | Average, lowest, and highest bids                  |

Details vary by job. `null` means unavailable; `[]` means an empty list. Matching details already shown in standard fields are not repeated.

**Extra Details is off by default.** Turn on **Add Extra Details** in the Input form, or set `"paidEnrichment": true` in JSON. Adding authentication alone does not enable the add-on. Native search filters work without the add-on.

**Check Pricing for the enrichment fee.** While no add-on fee is configured, Extra Details is included without an extra charge. Once that fee is active for your run, each saved job with usable Extra Details has the listed fee. Standard result and other listed charges still apply.

For older saved tasks, `enrichDetails` no longer turns on details or authorizes an extra charge. Use the new paid option to opt in. Start a new run when changing these settings.

Download [JSON, CSV, or Excel](https://docs.apify.com/storage/dataset). **Use JSON to keep the full Extra Details structure.**

### 🎯 Choose your search

| Control                                                          | What it does                                                                                                                 | Authentication |
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------- |
| Search Query                                                     | Use keywords, phrases, or `AND`, `OR`, and `NOT`. Example: `(n8n OR zapier) NOT wordpress`.                                  | Not needed     |
| Sort Order                                                       | Ask Upwork for recent or relevant jobs.                                                                                      | Not needed     |
| Maximum Job Age (hours)                                          | Save jobs posted within this many hours before the run starts. `24` means one day; `0.5` means 30 minutes; `0` turns it off. | Not needed     |
| Job type, experience, project length, hours, and rates or budget | Ask Upwork to narrow its search before returning jobs.                                                                       | Not needed     |
| Client Countries and Client Hiring History                       | Ask Upwork for jobs from selected client locations or clients with a chosen number of hires.                                 | Not needed     |
| Contract-to-Hire Only                                            | Find jobs that may lead to a full-time hire.                                                                                 | Not needed     |
| Payment-Verified Clients Only and Proposal Range                 | Ask Upwork for verified clients or jobs in a proposal-count band.                                                            | Required       |

Use a **Custom Search URL** for categories, client timezones, regions, or locations outside the form's country list. Previous-client matching and sorting by client spending or rating also work through supported search URLs and require authentication. Filtering by client location or hiring history does not unlock client values in the output.

The Actor saves the jobs Upwork returns, subject to job age, duplicate removal, and your run limits. It does not apply separate keyword, client-spending, rating, review-count, or proposal-count checks. **Filter those values in your own workflow after delivery.** Missing client values do not remove a returned job.

For more precise keywords, use [Upwork's advanced search guidance](https://support.upwork.com/hc/en-us/articles/1500007921782-How-to-use-advanced-search-techniques-to-find-jobs). The Actor forwards your search expression without adding its own text-matching rules.

### 📝 Input guidance

- **Keep the default Residential proxies** for better reliability.
- **A Custom Search URL replaces the query and all native search filters in the form**, including client countries and payment verification. Job age and run limits still apply. Paste one HTTPS Upwork job-search URL, not an individual job URL.
- **Unsupported URL filters produce an input error.** Remove the unsupported setting or use the form's search controls.
- **Only the marked search filters require authentication.** Without valid access, payment verification, proposal bands, and previous-client matching stop with an error. Visitor-compatible filters still work without login; no filter is silently removed.
- **The job-age cutoff stays fixed throughout a run**, including retries and restarts. Jobs without a usable posting time cannot pass this filter. An older job can appear before newer jobs, even with recent sorting, so the Actor checks every job on each fetched page.
- **Maximum Job Age stops after two consecutive pages with no fresh jobs**, or at your page or result limit if reached sooner. A page with any job within the age range resets the count, even if that job is a duplicate. Saved results remain available. This limits wasted requests, but fresh jobs may still appear on later pages. The count is preserved across retries and restarts.
- **An unavailable value is `null`.** Authentication does not guarantee every field. Basic searches may have fewer available jobs and pages.

#### Older saved tasks

Remove any active Include / Exclude Keywords, excluded countries, minimum client spending, rating, review count, or Maximum Proposals settings. These rules are no longer applied. Active retired rules produce a clear input error; they are not silently ignored. Hidden legacy custom rules also need to be removed. Use **Maximum Job Age (hours)** instead of the older `maxJobAge` setting. Use **Proposal Range** for Upwork's proposal bands rather than an exact maximum.

Project Length is supported in both the search and results. Native filters use Upwork's matching rules. A job returned by Upwork will not be discarded because the Actor interprets its budget, workload, or client information differently. **Every delivered job is a paid result**, even if your workflow later discards it.

### 🔐 Add your Upwork login details

#### 🔑 Authorization Header (recommended)

1. Sign in to Upwork and open your browser's developer tools.
2. Select **Network**, then reload the page or open a job.
3. Open an Upwork request and find `authorization` under **Request Headers**.
4. Copy its value into **Authorization Header (recommended)**. The `Bearer ` prefix is optional.

Use your **Upwork token**, not an Apify API token or a cookie value.

#### 🍪 Cookies (alternative)

Export the full cookies from your signed-in Upwork session as a **JSON array**. Paste the array into **Cookies (alternative)**.

Both inputs are encrypted. Keep them private and replace them when they expire. Renewal is not automatic.

If you supply both, the authorization header takes priority. Rejected cookies can fall back to visitor results when your selected filters support visitor access. Searches with authentication-only filters stop instead. A token rejected during search stops the run.

### ⏱️ Results and run limits

- **Pages to Scrape:** Up to 100 pages, with 50 jobs requested per page. With Maximum Job Age enabled, two consecutive pages with no fresh jobs end the search early. Fewer than 50 returned jobs also ends the search. The returned page size is checked before filters.
- **Maximum Results:** Limits saved jobs after the job-age check and duplicate removal. It does not guarantee that enough jobs are available.
- **Run time:** New requests stop after one hour, including retries and restarts. Current requests and saves may finish afterward.
- **Connection attempts:** Each run allows up to three browser attempts, including retries and restarts. Repeated connection failures stop the run. Any saved jobs remain available.
- **Spending limits:** The remaining spending allowance limits saved jobs. With Extra Details enabled, the Actor budgets for the result fee and any active enrichment fee before fetching details. A small remaining allowance may stop the run before it reaches your result limit.

Page size is fixed. Old page-size inputs and page-size values in custom URLs are ignored. Restarting a run does not reset its detail-request limit.

Each saved job counts as one result. With **Add Extra Details** enabled and an enrichment fee active for your run, each saved job with usable Extra Details also has that fee. The job stays in one row; it is not saved a second time. Missing, empty, or failed details have no enrichment fee. Partial details can be charged when they contain usable information. Check the Actor's Pricing for current fees. Startup and any listed platform-usage charges also apply. Filtering jobs later in your workflow does not reduce the number of delivered results.

### 🔗 Automate your workflow

Use [Apify schedules](https://docs.apify.com/actors/running/schedules) for regular searches. Set a job-age limit to focus on fresh jobs. Keep your Upwork credentials current.

To use [n8n](https://docs.apify.com/integrations/n8n) or [Make](https://docs.apify.com/integrations/make):

1. Connect Apify using your **Apify API token**, not your Upwork token.
2. Run this Actor and wait for success.
3. Use **Get Dataset Items** with the dataset ID from that run.
4. Filter Extra Details if needed, then save jobs or send alerts.

For example, to keep jobs with no hires, require **Extra Details → Activity → Hires** to equal `0`. A missing value is not zero.

Already using a schedule? Add a **Run succeeded** webhook in the Actor's **Integrations** tab. The webhook sends run information, not jobs. Fetch results using `resource.defaultDatasetId`; do not start another scrape. See the [webhook guide](https://docs.apify.com/integrations/webhooks).

Save handled **Job ID** values to avoid duplicate alerts across runs. Ignore repeated webhook run IDs, skip empty datasets, and handle failed runs separately.

### 💡 Troubleshooting

| Problem                         | What to check                                                                                                                                      |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| No results                      | Check the run status. For a successful run, try fewer filters. A failed run does not prove there were no matches.                                  |
| Missing client data             | Replace expired Upwork credentials. Some values may be missing even with valid access. Basic access can return fewer jobs and pages.               |
| Missing Extra Details           | Check credentials and turn on **Add Extra Details**. If details fail or reach their limit, selected jobs are still saved with unavailable details. |
| Run stopped early               | Check its status and saved results. Failed runs can contain useful partial results.                                                                |
| Duplicate jobs across runs      | Each job appears once per run, but can appear again in a later run. Match by **Job ID** when combining results.                                    |
| No alert after a successful run | Check your workflow and webhook delivery. A successful scrape does not guarantee an alert was sent.                                                |

# Actor input Schema

## `searchQuery` (type: `string`):

Use Upwork search keywords, phrases, or AND / OR / NOT expressions. Example: (n8n OR zapier) NOT wordpress. A Custom Search URL takes priority.

## `startUrls` (type: `string`):

Paste one Upwork job-search URL. It replaces the query and all native search filters in the form, including countries and payment verification. Job age and result limits still apply. Unsupported URL filters are rejected. Each page requests 50 jobs.

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

Maximum number of distinct jobs to save. 0 means no result cap. Job age, page limits, and spending limits still apply. Upwork may have fewer jobs available.

## `scrapePages` (type: `integer`):

Maximum number of search pages to fetch. Each page requests 50 jobs; Upwork may return fewer. Result and spending limits can stop the run earlier. With Maximum Job Age enabled, two consecutive pages with no jobs within that age also stop the run.

## `sortOrder` (type: `string`):

Leave empty for the existing most-recent order. A Custom Search URL overrides this control. Existing sort JSON inputs retain precedence. No oldest-first order is supported.

## `authorizationHeader` (type: `string`):

Paste the authorization header from your logged-in Upwork session, with or without the Bearer prefix. Treat it as a password. Tokens can expire and need renewal for scheduled runs. If Upwork rejects the token during search, the run stops without trying cookies or standard results.

## `cookies` (type: `array`):

Export the full cookies from your signed-in Upwork session as a JSON array. Expired cookies can fall back to visitor access when your chosen filters support it. Authentication-only searches stop instead.

## `paymentType` (type: `string`):

Leave empty for either payment type. A Custom Search URL overrides this control. Existing jobType JSON inputs retain precedence over this control.

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

Select the experience levels of the jobs.

## `hoursPerWeek` (type: `array`):

Select hours per week options.

## `projectLength` (type: `array`):

Ask Upwork for jobs with these project lengths. Upwork applies its own matching rules. A Custom Search URL overrides this control.

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

Ask Upwork for jobs that may lead to a full-time hire. False disables this filter. A Custom Search URL overrides this control.

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

Legacy JSON control for exact hourly workload. Use Hours Per Week in the form. If both are set, the choices must agree. A Custom Search URL overrides this field.

## `hourlyJobTypeMin` (type: `integer`):

Enter the minimum hourly rate.

## `hourlyJobTypeMax` (type: `integer`):

Enter the maximum hourly rate.

## `fixedPriceJobTypeMin` (type: `integer`):

Enter the minimum fixed price.

## `fixedPriceJobTypeMax` (type: `integer`):

Enter the maximum fixed price.

## `maxJobAgeHours` (type: `number`):

Save jobs posted within this many hours before the run starts. 24 means one day; 0.5 means 30 minutes; 0 turns this option off. Check jobs up to your page limit, stopping after two consecutive pages with no jobs within this age. A page with any matching job resets that count, even if the job is a duplicate. This saves requests but may miss fresh jobs on later pages. Jobs without a usable posting time are excluded. The cutoff does not move, and retries do not reset the count.

## `clientCountries` (type: `array`):

Ask Upwork for jobs from clients in these countries. Leave empty for any country. A Custom Search URL overrides this selection. Country codes are also accepted in JSON.

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

Ask Upwork for clients with this hiring history. Leave empty for any history. A Custom Search URL overrides this control.

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

Ask Upwork to return only jobs from clients with a verified payment method. False disables this filter. Requires valid authentication. A Custom Search URL overrides this setting.

## `proposalRange` (type: `string`):

Ask Upwork for jobs in this proposal-count band. Leave empty for any count. This is Upwork's native range, not an exact maximum. A Custom Search URL overrides this control. Requires valid Upwork authentication.

## `paidEnrichment` (type: `boolean`):

Add available client history, job requirements, screening questions, and hiring activity to each job. Requires your Upwork Authorization Header or Cookies. Check this Actor's Pricing: if an enrichment fee is listed, each saved job with usable Extra Details has that additional fee. While no enrichment fee is configured, details are included without an add-on charge. Standard result and other listed charges still apply. Unavailable details have no enrichment fee. Leave off for the standard 16 fields.

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

Legacy input retained for saved tasks. This setting no longer enables enrichment or authorizes a charge. Use Add Extra Details to opt in. The paidEnrichment setting takes precedence.

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

Select which proxies to use. Supports custom proxy urls.

## `startUrl` (type: `string`):

Legacy JSON alias for Custom Search URL. Use a supported HTTPS Upwork job-search URL.

## `rawSearchUrl` (type: `string`):

Legacy JSON alias for Custom Search URL. Use a supported HTTPS Upwork job-search URL.

## Actor input object example

```json
{
  "maxResults": 0,
  "scrapePages": 1,
  "contractToHire": false,
  "maxJobAgeHours": 0,
  "verifiedPaymentOnly": false,
  "paidEnrichment": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

The filtered Upwork jobs saved by this run. Includes Extra Details when Enrich With Details is enabled.

## `enrichment` (type: `string`):

Available when Add Extra Details (paid) is enabled and authentication succeeds. The index lists each saved Job ID, detail-record key, and status. Records are in the same run's key-value store.

# 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 = {
    "scrapePages": 1,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("matthewjames/upwork-job-scraper-pay-per-result").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 = {
    "scrapePages": 1,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("matthewjames/upwork-job-scraper-pay-per-result").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 '{
  "scrapePages": 1,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}' |
apify call matthewjames/upwork-job-scraper-pay-per-result --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,matthewjames/upwork-job-scraper-pay-per-result"
        }
    }
}
```

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/87wgKQABseN25kbXs/builds/AeAjrVuvh22gOKezb/openapi.json
