# Upwork Job Finder (`nahom.network/upwork-job-finder`) Actor

Scrapes job listings from Upwork based on a search query. Returns job title, link, description, type, experience level, and duration. Useful for freelancers to track new opportunities matching their skills.

- **URL**: https://apify.com/nahom.network/upwork-job-finder.md
- **Developed by:** [Nahom D](https://apify.com/nahom.network) (community)
- **Categories:** Automation, Jobs, Lead generation
- **Stats:** 288 total users, 1 monthly users, 97.8% runs succeeded, 11 bookmarks
- **User rating**: 4.92 out of 5 stars

## Pricing

$10.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/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 Finder

<h2>Overview</h2>

<p>
  <strong>Upwork Job Finder</strong> is an Apify Actor that scrapes freelance job listings from
  <a href="https://www.upwork.com/nx/search/jobs/">Upwork's public job search</a> for any keyword you choose.
  It bypasses Cloudflare's anti-bot challenge using a stealth headless browser, extracts the embedded
  Nuxt application state, and returns clean, structured job data ready for analysis, alerting, or
  integration into downstream workflows.
</p>

<p>Typical use cases:</p>

<ul>
  <li>Build a daily feed of new freelance opportunities matching your skills.</li>
  <li>Power a job-alert email or Slack notification system.</li>
  <li>Track market rates, demand, and required skills for a niche over time.</li>
  <li>Feed structured job data into a CRM, Airtable base, or BI dashboard.</li>
</ul>

<h2>Features</h2>

<ul>
  <li>Search any Upwork query (skills, keywords, or phrases).</li>
  <li>Paginate through results one page at a time.</li>
  <li>
    <strong>Multi-page range scraping:</strong> fetch up to 10 pages in a single run using
    <code>pageStart</code> and <code>pageEnd</code>.
  </li>
  <li>Automatic Cloudflare Turnstile solving via a stealthy headless browser.</li>
  <li>Optional Apify Proxy support via <code>proxyConfiguration</code>.</li>
  <li>Returns full job metadata: title, description, budget, skills, client info, posting timestamps, and more.</li>
  <li>Output is written to the Actor's default dataset with a typed schema and a pre-built table view.</li>
</ul>

<h2>Input</h2>

<p>
  The Actor accepts <strong>either</strong> a plain search <code>query</code> <strong>or</strong> a
  full Upwork <code>searchUrl</code> &mdash; <strong>not both</strong>. Use <code>query</code> for a
  quick keyword search, or build a URL on
  <a href="https://www.upwork.com/nx/search/jobs/">Upwork's search page</a> (filters for budget,
  experience level, duration, client history, etc.) and paste it into <code>searchUrl</code>.
  Any Upwork URL filter works automatically.
</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Required</th>
      <th>Default</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>query</code></td>
      <td>string</td>
      <td>One of <code>query</code>/<code>searchUrl</code></td>
      <td>&mdash;</td>
      <td>Plain keyword search (3&ndash;100 chars), e.g. <code>"Airtable Automation"</code>.</td>
    </tr>
    <tr>
      <td><code>searchUrl</code></td>
      <td>string</td>
      <td>One of <code>query</code>/<code>searchUrl</code></td>
      <td>&mdash;</td>
      <td>
        Full Upwork search URL. Must match
        <code>https://www.upwork.com/nx/search/jobs/?...</code>. Any other URL is rejected.
      </td>
    </tr>
    <tr>
      <td><code>page</code></td>
      <td>integer</td>
      <td>No</td>
      <td><code>1</code></td>
      <td>Result page to fetch when <code>pageStart</code>/<code>pageEnd</code> are not provided.</td>
    </tr>
    <tr>
      <td><code>pageStart</code></td>
      <td>integer</td>
      <td>No</td>
      <td>&mdash;</td>
      <td>
        First page in the range to fetch. Must be &ge; 1.
      </td>
    </tr>
    <tr>
      <td><code>pageEnd</code></td>
      <td>integer</td>
      <td>No</td>
      <td>&mdash;</td>
      <td>
        Last page in the range to fetch. Must be &ge; <code>pageStart</code>.
      </td>
    </tr>
    <tr>
      <td><code>proxyConfiguration</code></td>
      <td>object</td>
      <td>No</td>
      <td>&mdash;</td>
      <td>
        Optional Apify Proxy configuration. Use the proxy editor in the Apify UI.
      </td>
    </tr>
  </tbody>
</table>

<p>
  <strong>Validation:</strong> the run fails fast if both <code>query</code> and
  <code>searchUrl</code> are set, if neither is set, or if <code>searchUrl</code> is not a valid
  Upwork search URL. The page range is capped to 10 pages per run.
</p>

<h3>Example: simple keyword query</h3>

```json
{
  "query": "Airtable Automation",
  "page": 1
}
```

<h3>Example: full search URL with filters</h3>

```json
{
  "searchUrl": "https://www.upwork.com/nx/search/jobs/?client_hires=0&contractor_tier=1&duration_v3=semester&nbs=1&q=ai%20automation&sort=relevance%2Bdesc",
  "page": 1
}
```

<h3>Example: with Apify Proxy</h3>

```json
{
  "query": "AI engineer",
  "page": 2,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"]
  }
}
```

<h3>Example: page range (capped to 10 pages)</h3>

```json
{
  "query": "Airtable Automation",
  "pageStart": 1,
  "pageEnd": 5
}
```

<h2>Output</h2>

<p>
  Each run pushes one record per job to the default dataset. A full JSON Schema is defined in
  <code>.actor/dataset_schema.json</code>, and an <strong>Overview</strong> table view is available
  in the Apify console for quick scanning.
</p>

<h3>Key output fields</h3>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>uid</code></td>
      <td>string</td>
      <td>Unique job identifier.</td>
    </tr>
    <tr>
      <td><code>ciphertext</code></td>
      <td>string</td>
      <td>URL slug. Build the job URL with <code>https://www.upwork.com/jobs/{ciphertext}</code>.</td>
    </tr>
    <tr>
      <td><code>title</code></td>
      <td>string</td>
      <td>Job title (may include <code>&lt;span class="highlight"&gt;</code> markup around matched terms).</td>
    </tr>
    <tr>
      <td><code>description</code></td>
      <td>string</td>
      <td>Full job description (may include highlight markup).</td>
    </tr>
    <tr>
      <td><code>type</code></td>
      <td>integer</td>
      <td><code>1</code> = Fixed-price, <code>2</code> = Hourly.</td>
    </tr>
    <tr>
      <td><code>durationLabel</code></td>
      <td>string</td>
      <td>Expected project duration, e.g. <em>"1 to 3 months"</em>.</td>
    </tr>
    <tr>
      <td><code>engagement</code></td>
      <td>string</td>
      <td>Engagement key, e.g. <code>usnuxt_Engagement_421.partTime</code>.</td>
    </tr>
    <tr>
      <td><code>tierText</code></td>
      <td>string</td>
      <td>Required experience tier (entry / intermediate / expert).</td>
    </tr>
    <tr>
      <td><code>amount.amount</code></td>
      <td>number</td>
      <td>Fixed-price budget in USD (<code>0</code> for hourly jobs).</td>
    </tr>
    <tr>
      <td><code>hourlyBudget</code></td>
      <td>object</td>
      <td><code>{ min, max }</code> hourly rate in USD (both <code>0</code> for fixed-price jobs).</td>
    </tr>
    <tr>
      <td><code>publishedOn</code></td>
      <td>string (ISO&nbsp;8601)</td>
      <td>When the job was published.</td>
    </tr>
    <tr>
      <td><code>attrs</code></td>
      <td>array</td>
      <td>Skill tags attached to the job, each with <code>prefLabel</code> and a <code>highlighted</code> flag.</td>
    </tr>
    <tr>
      <td><code>client</code></td>
      <td>object</td>
      <td>Client info: country, payment verification, total spent, reviews, and feedback rating.</td>
    </tr>
  </tbody>
</table>

<h3>Sample output (truncated)</h3>

```json
{
  "uid": "2054172332114949360",
  "ciphertext": "~022054172332114949360",
  "title": "Senior AI/ML Engineer for LLM and RAG Systems",
  "description": "We are seeking a highly skilled Senior AI/ML Engineer ...",
  "publishedOn": "2026-05-12T12:11:45.608Z",
  "type": 2,
  "durationLabel": "More than 6 months",
  "engagement": "usnuxt_Engagement_421.fullTime",
  "tierText": "jsn_Expert_207",
  "hourlyBudget": { "min": 30, "max": 60 },
  "amount": { "amount": 0 },
  "attrs": [
    { "prefLabel": "Machine Learning", "highlighted": true },
    { "prefLabel": "Artificial Intelligence", "highlighted": true },
    { "prefLabel": "Python", "highlighted": false }
  ],
  "client": {
    "location": { "country": null },
    "isPaymentVerified": false,
    "totalSpent": null
  }
}
```

<h2>Usage</h2>

<h3>Run on the Apify platform</h3>

<ol>
  <li>Open the Actor in the <a href="https://console.apify.com/">Apify console</a>.</li>
  <li>Set your <code>query</code> and <code>page</code> values in the input form.</li>
  <li>Click <strong>Start</strong>. Jobs appear in the <strong>Dataset</strong> tab when the run finishes.</li>
  <li>Export the dataset as JSON, CSV, Excel, or HTML, or fetch it via the Apify API.</li>
</ol>

<h3>Run via the Apify API</h3>

```bash
curl -X POST "https://api.apify.com/v2/acts/<ACTOR_ID>/runs?token=<APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ "query": "Airtable Automation", "page": 1 }'
```

<h3>Run locally</h3>

<p>Requires Python 3.11+ and the <a href="https://docs.apify.com/cli">Apify CLI</a>.</p>

```bash
apify run
```

<p>Edit <code>storage/key_value_stores/default/INPUT.json</code> to change the input.</p>

<h2 id="paginating-through-all-results">Paginating through all results</h2>

<p>
  To fetch individual pages across separate runs, increment <code>page</code> until an empty
  dataset is returned:
</p>

```bash
for page in 1 2 3 4 5; do
  apify call <ACTOR_ID> --input "{\"query\":\"Airtable Automation\",\"page\":$page}"
done
```

<p>
  Use <code>pageStart</code>/<code>pageEnd</code> to fetch up to 10 pages in a single run, which
  is significantly faster than looping (the Cloudflare challenge is solved once and reused
  across all pages):
</p>

```bash
apify call <ACTOR_ID> --input "{\"query\":\"Airtable Automation\",\"pageStart\":1,\"pageEnd\":5}"
```

<p>
  For ranges larger than 10 pages, loop runs, incrementing
  <code>pageStart</code>/<code>pageEnd</code> by 10 each iteration.
</p>

<h2>Limitations</h2>

<ul>
  <li>Only public search results are returned &mdash; no login-gated data.</li>
  <li>
    Some client fields (<code>totalSpent</code>, <code>country</code>, <code>isPaymentVerified</code>,
    review counts) may be <code>null</code> in the search-listing payload. Fetch the individual
    job page for the full client profile.
  </li>
  <li><code>title</code> and <code>description</code> contain Upwork's search-highlight HTML markup; strip it client-side if you need plain text.</li>
  <li>Heavy use of one IP may trigger Cloudflare. Use Apify Proxy for high-volume scraping.</li>
</ul>

<h2>Support &amp; feedback</h2>

<p>
  Found a bug or have a feature request? Open an issue on the Actor's source repository or contact
  the maintainer through the Apify console.
</p>

Wanna hire me? I'm a freelance software developer specializing in web scraping, automation, data engineering, web development, and much more. Check out my
<a href="https://www.upwork.com/freelancers/~0160beab5535936c73">Upwork profile</a>

# Actor input Schema

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

Plain keyword search (e.g. 'python', 'Airtable Automation'). Leave empty if you are providing a Search URL.

## `searchUrl` (type: `string`):

Full Upwork search URL, e.g. https://www.upwork.com/nx/search/jobs/?q=ai%20automation\&sort=recency. Leave empty if you are providing a Search query. The 'page' or range inputs below override any page parameter in the URL.

## `page` (type: `integer`):

Result page to fetch when pageStart/pageEnd are not provided.

## `pageStart` (type: `integer`):

First page in the range to fetch. Must be >= 1. Range is capped to 10 pages per run.

## `pageEnd` (type: `integer`):

Last page in the range to fetch. Must be >= pageStart. Range is capped to 10 pages per run.

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

Apify Proxy configuration (recommended for higher success rate).

## Actor input object example

```json
{
  "page": 1
}
```

# Actor output Schema

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

Dataset containing all Upwork job listings matched by the search query or URL.

# 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 = {
    "page": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("nahom.network/upwork-job-finder").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 = { "page": 1 }

# Run the Actor and wait for it to finish
run = client.actor("nahom.network/upwork-job-finder").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 '{
  "page": 1
}' |
apify call nahom.network/upwork-job-finder --silent --output-dataset

```

## MCP server setup

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

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/f3zq4fGDMfkTrD3SC/builds/BaqFQCfYl4Xdnvwu9/openapi.json
