# Upwork Jobs Scraper - New Job Alerts, Budgets & Hourly Rates (`s-r/upwork-jobs-scraper`) Actor

Scrape new Upwork jobs by keyword or search URL. Get title, full description, fixed budget or hourly rate, experience level, skills and posting time for each job. Built for freelancers, agencies and lead tools that want Upwork job alerts.

- **URL**: https://apify.com/s-r/upwork-jobs-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (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 $1.50 / 1,000 job postings

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 Scraper: Upwork Jobs Scraper for New Job Alerts

This Upwork scraper finds new public Upwork jobs by search phrase or an Upwork job search URL and exports them as clean rows: job title, full public description, fixed budget or hourly rate, experience level, skills and the time the job was posted. Use it to build your own Upwork job alerts, feed new projects into a CRM or chat channel, or study what clients are hiring for.

Freelancers can monitor new opportunities in their specialism. Agencies can follow several skill combinations and identify work suitable for their teams. Analysts can study the mix of fixed projects and hourly contracts, while developers can use the stable job identifier to maintain their own history of opportunities. Each run retrieves the postings currently shown in public search results.

### What the Upwork jobs scraper returns

One dataset row per unique job posting, with:

- `job_id`, `ciphertext` and `url`: stable identifiers and the link to the original posting
- `title` and `description`: the full public text, with line breaks kept
- `job_type`: `fixed` or `hourly`
- `budget` for fixed projects, or `salary_min` / `salary_max` per hour for hourly contracts
- `experience_level`, `workload`, `duration` and `skills`
- `posted_at`, `created_at` and `renewed_at` in UTC

Only fields that any logged-out visitor can see are collected. Client names, client spending history and other client details are not part of the output.

### How to scrape Upwork jobs

1. Open the actor and enter one or more search phrases, for example `python` or `shopify developer`, or paste an Upwork job search URL.
2. Keep `sort` on `recency` to get the newest jobs first, and set `max_items`.
3. Optionally set `posted_within_hours` to collect only jobs from the last hours.
4. Start the run and download the dataset as JSON, CSV or Excel, or read it through the Apify API.
5. Schedule the run every few minutes or hours to turn it into an Upwork job feed.

### Upwork Jobs Scraper input

Provide `search_terms` or `search_urls`. Choose exactly one of these lists and include at least one non-empty entry. A list can contain up to ten entries. Each search phrase is a separate search, so a phrase with several words is treated as one query. Upwork's search syntax, including quoted phrases and boolean expressions, can be used in a search term.

For example, this input searches for Python opportunities:

```json
{"search_terms": ["python"], "max_items": 50, "sort": "recency"}
```

The default limit is fifty unique jobs per search. Set `max_items` between one and five thousand. Jobs appearing in more than one search are exported once; the first search that finds a job supplies its `search_query`. This means a later overlapping search can return fewer additional jobs than its limit. Whitespace-only entries are ignored, and repeated identical search entries are processed once.

Use `job_type` to select `hourly`, `fixed` or `any`. Use `experience_level` to select `entry`, `intermediate`, `expert` or `any`. These two fields apply to search phrases. When using search URLs, the filters already present in the URLs determine the searches. Only HTTPS URLs pointing to Upwork's `/nx/search/jobs/` page are accepted. Page number and page size in an input URL are replaced automatically during collection.

### Monitoring recent jobs (Upwork job alerts)

The default `sort` is `recency`, which retrieves the newest postings first. Choose `relevance` to use Upwork's relevance order. A sort parameter explicitly supplied in a search URL takes precedence over the input sort field.

Set `posted_within_hours` between one and seven hundred twenty to collect only jobs published within that window. This filter requires recency order for every search. The time window is anchored to the beginning of collection, so a long collection uses one consistent cutoff. Collection stops when it reaches an older posting. A search can therefore return fewer rows than its limit, or no matching recent jobs.

For repeat monitoring, schedule runs in Apify and compare `job_id` with identifiers you saved from earlier runs. The actor removes duplicates within a collection. Your application is responsible for maintaining history between separate runs. `created_at`, `posted_at` and `renewed_at` help you distinguish original creation from subsequent publication or renewal. Connect the dataset to a webhook, Make, Zapier or n8n to send each new job to email, Slack or Telegram; this works as an Upwork RSS feed alternative.

### Output

The default dataset contains one row per unique public posting. Every row includes `job_id`, `ciphertext`, `url`, `title`, `description`, `posted_at`, `created_at`, `renewed_at`, `fetched_at`, `job_type`, `experience_level`, `skills`, `workload`, `duration`, `position` and `search_query`. Rows also carry `source`, `platform` and `employment_type` (always `CONTRACTOR`).

`job_id` is Upwork's numeric identifier represented as a string. `ciphertext` is the identifier used in the job URL. Both are provided so integrations can join by a stable identifier and still open the original posting directly. Search highlighting is removed from titles and descriptions, and description line breaks are preserved.

For fixed projects, `budget` contains the total project amount. For hourly contracts, `salary_min` and `salary_max` contain the published hourly range, with `salary_period` set to `HOUR` and `salary_currency` to `USD`. An hourly posting without a published range has null salary fields. Fixed budgets also have null salary fields because a total project budget is not an hourly wage. `currency` is `USD` for both job types.

#### Output example

Two real rows from a `python` search on 5 October 2026, a fixed-price project and an hourly contract:

```json
[
  {
    "source": "upwork-jobs-scraper",
    "platform": "upwork",
    "employment_type": "CONTRACTOR",
    "fetched_at": "2026-10-05T19:34:00.000Z",
    "job_id": "2107126667072165637",
    "ciphertext": "~022107126667072165637",
    "url": "https://www.upwork.com/jobs/~022107126667072165637",
    "title": "AI Backend Engineer Needed",
    "description": "We need a backend engineer to build and maintain the server-side infrastructure for an AI-powered application. The role includes developing APIs, integrating AI models, and supporting database operations. You will work on improving system performance, reliability, and scalability while collaborating with the team to deliver a stable backend solution. Experience with Python, Django, and PostgreSQL is important, along with a strong understanding of backend architecture and deployment workflows.",
    "posted_at": "2026-10-05T15:12:32.865Z",
    "created_at": "2026-10-05T15:12:01.035Z",
    "renewed_at": null,
    "job_type": "fixed",
    "budget": 10,
    "currency": "USD",
    "salary_min": null,
    "salary_max": null,
    "salary_currency": null,
    "salary_period": null,
    "experience_level": "intermediate",
    "workload": null,
    "duration": "1 to 3 months",
    "skills": ["PHP", "JavaScript", "Reverse Engineering", "Embedded C"],
    "position": 26,
    "search_query": "python"
  },
  {
    "source": "upwork-jobs-scraper",
    "platform": "upwork",
    "employment_type": "CONTRACTOR",
    "fetched_at": "2026-10-05T19:34:00.000Z",
    "job_id": "2107170088531208224",
    "ciphertext": "~022107170088531208224",
    "url": "https://www.upwork.com/jobs/~022107170088531208224",
    "title": "Send an AI Agent to a specific URL",
    "description": "Hi there\n\nI would like to see what we can measure when an \"AI Agent\" visits our website. We have the measurement tools - we need you to launch a visit using an AI Agent.\n\nPlease state in your bid which AI agent you will use. \n\nWe will pay $35 per agent. \n\nThanks\n.",
    "posted_at": "2026-10-05T18:06:14.975Z",
    "created_at": "2026-10-05T18:04:33.514Z",
    "renewed_at": null,
    "job_type": "hourly",
    "budget": null,
    "currency": "USD",
    "salary_min": 35,
    "salary_max": 35,
    "salary_currency": "USD",
    "salary_period": "HOUR",
    "experience_level": "expert",
    "workload": "not_sure",
    "duration": "Less than 1 month",
    "skills": ["AI Agent Development", "AI Agents", "Cybersecurity Tool"],
    "position": 8,
    "search_query": "python"
  }
]
```

The `OUTPUT` record includes `searches`, with the latest source total and the cumulative number returned per search. A search marked `partial` has additional source results beyond what was delivered. The overall `partial` flag indicates unfinished collection or a source interruption. Read `warnings` when a run is incomplete. Source counts can change while jobs are added or removed, and public result availability can vary. For the search above it reported:

```json
{"query": "python", "url": "https://www.upwork.com/nx/search/jobs/?q=python&sort=recency", "total_results": 1883, "returned": 50, "partial": true}
```

### Use cases

- **Freelancers**: a personal Upwork job alert for your skills that fires before the posting has dozens of proposals.
- **Agencies**: follow several skill searches at once and route matching jobs to the right team.
- **Lead generation tools**: feed new Upwork jobs into a CRM, a scoring model or an AI proposal assistant.
- **Market research**: track demand for skills, the share of fixed and hourly work and published budgets over time.
- **Recruiting and training**: see which technologies clients ask for this week.

### Upwork scraper compared with other options

| Option | What you get | Limits |
|---|---|---|
| Upwork's own job alerts and saved searches | Notifications inside your account | Needs a freelancer account, no export, no API access to the data |
| Upwork API | Official GraphQL access | Requires an approved API key and OAuth; aimed at apps working on behalf of a user |
| Browser extensions | Alerts while your browser is open | Run on your own machine and account, no scheduled export |
| Writing your own scraper | Full control | You maintain parsing, paging and deduplication yourself |
| Other Upwork scrapers in the Apify Store | Similar job data | Prices run from about $0.001 to $0.01 per job; field sets and client data differ |
| **This actor** | Clean job rows, fixed and hourly pay split into separate fields, deduplication, recency window | Public, logged-out fields only |

### Pricing

The proposed launch model is $0.0015 per delivered job and $0.0005 per valid run. Confirm the active event prices in the actor's Pricing tab before running; that tab is authoritative. A run returning no jobs incurs no job-result charges. Invalid input is rejected before the run event is charged. A valid search that returns no matching jobs retains its run event.

Users without a paid Apify plan receive at most ten dataset rows per run. Result charges apply only to the rows delivered after that cap. Paying users can use their requested limits. Larger collections are continued automatically, and each delivered job is counted once rather than once per page.

### Public search limits

The actor collects fields available to a logged-out visitor. It does not request account cookies, client profiles or private hiring information. Budget-range, hourly-rate-range, payment-verification and proposal-count URL filters require login and are rejected. Other supported public URL filters include job type, experience tier, workload, duration, location and category identifiers.

Public search exposes at most the first five thousand positions per query. New postings can shift page boundaries during collection. Duplicate identifiers are removed, but a posting can move past a boundary while pages are being fetched. Regular overlapping monitoring runs help cover that movement. A posting may also close after it was retrieved; open its original URL to check its current availability.

### FAQ

#### Can I retrieve client contact information?

This actor exports public job postings. It does not provide structured client names, contacts, spending history or payment-verification fields. Descriptions retain the text published in the job search result.

#### Why are some hourly rates empty?

Some hourly jobs do not publish a rate range. Their salary fields are null. This means the rate is unspecified and does not mean the work is unpaid.

#### Can I supply an existing Upwork search URL?

Yes. Copy an HTTPS job search URL into `search_urls`. Supported public filters are preserved. Do not also supply search phrases, and remove filters that require an account login.

#### Why did a run return fewer jobs than requested?

The source may contain fewer matches, your publication window may exclude older postings, searches may overlap, or the free usage cap may apply. Check the search summaries and warnings for source limits or incomplete collection.

#### Do I need an Upwork account or API key?

No. The actor reads public job search results. You do not log in, and you do not need an Upwork API key.

#### How do I get only new jobs on every run?

Keep `sort` on `recency`, set `posted_within_hours` slightly longer than your schedule interval, and skip any `job_id` you already stored. The overlap makes sure no posting falls between two runs.

#### Can I use this as a replacement for the Upwork RSS feed?

Yes. Schedule the actor with your search phrases or search URL and send the dataset to a webhook or integration. Each row contains the job link, so the result works like a job feed you control.

# Actor input Schema

## `search_terms` (type: `array`):

Search phrases, one Upwork search per phrase. Use either search_terms or search_urls.

## `search_urls` (type: `array`):

HTTPS Upwork job search URLs. Public filters are preserved; page and per_page are controlled automatically. Login-only filters are rejected.

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

Newest first by default. A URL sort overrides this field; relevance omits the upstream sort parameter.

## `max_items` (type: `integer`):

Maximum unique jobs per search, up to 5000. Duplicate jobs across searches are delivered once. Follow next_cursor for unfinished collection.

## `job_type` (type: `string`):

Job type filter for search_terms. Search URLs use their own filters.

## `experience_level` (type: `string`):

Experience filter for search_terms. Search URLs use their own filters.

## `posted_within_hours` (type: `integer`):

Only deliver jobs published within this many hours of the first request. Requires recency sorting; stops at the first older job.

## `cursor` (type: `string`):

Signed next_cursor returned by the preceding response. Keep all original search options unchanged. Apify follows this automatically.

## Actor input object example

```json
{
  "search_terms": [
    "python"
  ],
  "sort": "recency",
  "max_items": 50,
  "job_type": "any",
  "experience_level": "any"
}
```

# Actor output Schema

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

One row per unique job.

## `output` (type: `string`):

Search totals, completion state, warnings and event counts.

## `errors` (type: `string`):

Source failures if any.

# 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 = {
    "search_terms": [
        "python"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = { "search_terms": ["python"] }

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "search_terms": [
    "python"
  ]
}' |
apify call s-r/upwork-jobs-scraper --silent --output-dataset

```

## MCP server setup

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