---
title: "Upwork scraper"
description: "Scrape Upwork job posts as JSON: titles, budgets, hourly ranges, experience level, duration and skills. One API request from Python, Node.js, the CLI or MCP."
canonical: https://www.zenrows.com/scrape/upwork
updated: 2026-10-20
---

# Upwork scraper

The Zenrows Upwork scraper turns an Upwork job search URL into structured JSON: job title, budget, hourly range, payment type, experience level, duration, required skills, description and the search's total job count. You send the URL, Fetch in Adaptive Stealth Mode opens the page, and Extract returns the fields. Run it from an API call, the CLI or an AI agent.

- Site: upwork.com
- Returns: Freelance job posts, budgets, skills and experience levels

## Run it: one Upwork job search in, JSON out

```json Input
{
  "url": "https://www.upwork.com/nx/search/jobs/?q=web%20scraping",
  "mode": "auto",
  "extract": "auto"
}
```

```json Output
{
  "category": {
    "name": "web scraping",
    "total_jobs": 14,
    "url": "https://www.upwork.com/nx/search/jobs/"
  },
  "job_postings": [
    {
      "title": "Python Developer for Document Pipeline",
      "budget": "Hourly: $90.00 - $120.00",
      "payment_type": "hourly",
      "experience_level": "expert",
      "duration": "1 to 3 months, Hours to be determined",
      "skills": ["Python", "Jinja2", "anthropic", "Claude Code", "Data Extraction", "Python Script", "ETL Pipeline"],
      "description_snippet": "Task Duration 1 - 2 weeks. \nWhat we have\nAn interactive US map (a single self-contained HTML file) with about 236 water sites: rivers, lakes, wastewater treatment plants and drinking-water plants…"
    },
    {
      "title": "Browser Automation Developer",
      "budget": "Hourly",
      "payment_type": "hourly",
      "experience_level": "intermediate",
      "duration": "Less than 1 month, Less than 30 hrs/week",
      "skills": ["Selenium", "Python", "Automation"],
      "description_snippet": "I am looking for an experienced freelance developer who specializes in browser automation to build a small custom program for my personal use…"
    }
  ]
}
```

Request and output of a live call to an Upwork job search for "web scraping" on Oct 20, 2026, trimmed to two of the 10 posts on the page. A field is returned when the page shows it.

## What data can you scrape from Upwork?

An Upwork job search returns one record per job post, plus the search it answers. These are the fields teams use most, with the path where each lives in the JSON.

| Field | What it holds | Example |
|---|---|---|
| `job_postings[].title` | Job post title as the client wrote it | `Python Developer for Document Pipeline` |
| `job_postings[].budget` | Budget label: fixed price, or hourly with the rate range | `Hourly: $90.00 - $120.00` |
| `job_postings[].payment_type` | Contract type, normalized | `hourly` or `fixed` |
| `job_postings[].experience_level` | The level the client asks for | `expert` |
| `job_postings[].duration` | Expected length and weekly hours, for hourly work | `1 to 3 months, Hours to be determined` |
| `job_postings[].skills[]` | The skill tags on the post | `["Selenium", "Python", "Automation"]` |
| `job_postings[].skills[]` (tools) | Named tools and frameworks clients ask for | `["Supabase", "n8n", "PostgreSQL"]` |
| `job_postings[].description_snippet` | The description shown in the results, often the full brief | `I am looking for an experienced freelance developer…` |
| `category.name` | The search term or category the page answers | `web scraping` |
| `category.total_jobs` | Number of open jobs Upwork counts for that search | `14` |
| `category.url` | The search page's base URL | `https://www.upwork.com/nx/search/jobs/` |
| `category.description` | Upwork's description of the search page | `Start your freelance job search on Upwork…` |

## How to scrape Upwork with Zenrows

1. **Create a free Zenrows account** and copy your API key from the dashboard.
2. **Build the search URL.** `https://www.upwork.com/nx/search/jobs/?q=<keywords>` is the base. Add `&sort=recency` for the newest posts first and `&page=2` for the next page.
3. **Send it with the default configuration**: Adaptive Stealth Mode to open the page and Extract to return the fields.
4. **Read `parsed.job_postings`** from the response. To keep only some fields, list them with their paths and the response returns just those.
5. **Run every keyword in Batch** once one search returns what you need, and repeat it on a schedule to catch new posts.

```python Python
# pip install requests
import requests

params = {
    "apikey": "YOUR_ZENROWS_API_KEY",
    "url": "https://www.upwork.com/nx/search/jobs/?q=web%20scraping",
    "mode": "auto",          # Adaptive Stealth Mode
    "extract": "auto",       # structured JSON
}
response = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
response.raise_for_status()

data = response.json()["parsed"]
print(data["category"]["name"], data["category"]["total_jobs"], "open jobs")
for job in data["job_postings"]:
    print(job["payment_type"], "|", job["budget"], "|", job["experience_level"], "|", job["title"])
```

```javascript Node.js
// Node 18+ (built-in fetch)
const params = new URLSearchParams({
  apikey: "YOUR_ZENROWS_API_KEY",
  url: "https://www.upwork.com/nx/search/jobs/?q=web%20scraping",
  mode: "auto",
  extract: "auto",
});

const response = await fetch(`https://api.zenrows.com/v1/?${params}`, {
  signal: AbortSignal.timeout(180_000),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);

const { parsed } = await response.json();
const skills = {};
for (const job of parsed.job_postings) {
  for (const s of job.skills ?? []) skills[s] = (skills[s] ?? 0) + 1;
}
console.log(Object.entries(skills).sort((a, b) => b[1] - a[1]).slice(0, 10));
```

```bash cURL
curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.upwork.com%2Fnx%2Fsearch%2Fjobs%2F%3Fq%3Dweb%2520scraping&mode=auto&extract=auto"

# Only the fields you need
curl "https://api.zenrows.com/v1/?apikey=YOUR_ZENROWS_API_KEY&url=https%3A%2F%2Fwww.upwork.com%2Fnx%2Fsearch%2Fjobs%2F%3Fq%3Dweb%2520scraping&mode=auto&extract=auto&extract_fields=job_postings.title%2Cjob_postings.budget%2Cjob_postings.skills%2Ccategory.total_jobs"
```

```python Python SDK
# pip install zenrows
from zenrows import ZenRowsClient

client = ZenRowsClient("YOUR_ZENROWS_API_KEY")
for query in ("web%20scraping", "python%20automation", "data%20extraction"):
    response = client.get(
        f"https://www.upwork.com/nx/search/jobs/?q={query}&sort=recency",
        params={"mode": "auto", "extract": "auto"},
    )
    parsed = response.json()["parsed"]
    hourly = [j for j in parsed["job_postings"] if j["payment_type"] == "hourly"]
    print(query, parsed["category"]["total_jobs"], "jobs,", len(hourly), "hourly on page 1")
```

## Input

The scraper takes one Upwork URL per request, plus the options below. A Batch job applies the same parameters to every task, so the input for fifty keywords is a list of search URLs with these settings.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `url` | string | required | An Upwork job search (`/nx/search/jobs/?q=…`), with sorting and paging in the URL |
| `mode` | string | `auto` | Adaptive Stealth Mode: starts with the lightest setup and adds rendering and protected-site handling only when Upwork needs them |
| `extract` | string | `auto` | Returns the page's fields as JSON in `parsed`, next to the raw `html` |
| `extract_fields` | string | all fields | Comma-separated paths to keep, such as `job_postings.title,job_postings.budget` |
| `proxy_country` | string | any | Two-letter country code, when you want the search as a visitor from that country sees it |
| `wait_for` | string | none | A CSS selector to wait for before the page is read |
| `session_id` | integer | none | Keep the same session for up to 10 minutes while you page through one search |

```json Batch input
{
  "type": "regular",
  "status": "closed",
  "zenrows_params": {
    "mode": "auto",
    "extract": "auto"
  },
  "tasks": [
    { "url": "https://www.upwork.com/nx/search/jobs/?q=web%20scraping", "external_id": "web-scraping" },
    { "url": "https://www.upwork.com/nx/search/jobs/?q=python%20automation&sort=recency", "external_id": "python-automation" },
    { "url": "https://www.upwork.com/nx/search/jobs/?q=data%20extraction&sort=recency", "external_id": "data-extraction" }
  ]
}
```

## Output

Every response is JSON with two keys: `html`, the page as Upwork served it, and `parsed`, the structured record. A job search returns `category`, with the search name and job count, and a `job_postings` array in the order Upwork lists them. The sample below holds two more posts from the same live search, with the fields the cards filled.

```json Job search results
{
  "category": {
    "name": "web scraping",
    "description": "Start your freelance job search on Upwork. Apply filters by skill, budget, and experience to find the perfect project for you.",
    "total_jobs": 14,
    "url": "https://www.upwork.com/nx/search/jobs/"
  },
  "job_postings": [
    {
      "title": "Full-Stack Developer Needed for Web Crawler & Accessibility SaaS MVP",
      "budget": "Hourly",
      "payment_type": "hourly",
      "experience_level": "expert",
      "duration": "1 to 3 months, Less than 30 hrs/week",
      "skills": ["Web Development", "Web Scraping", "Web Accessibility", "JavaScript", "React", "API Development", "PostgreSQL", "Python", "SaaS Development", "REST API"],
      "description_snippet": "Build MVP SaaS Web Crawler & Accessibility Testing Platform\nAbout the Project\nI’m building an early-stage SaaS product focused on helping organizations monitor and understand digital accessibility across websites and digital products…"
    },
    {
      "title": "Real Estate Data Engineer, Commercial Mortgage & Loan Data APIs",
      "budget": "Fixed price",
      "payment_type": "fixed",
      "experience_level": "intermediate",
      "duration": null,
      "skills": ["Data Scraping", "Data Mining", "Real Estate", "Data Entry", "API Development"],
      "description_snippet": "Project Overview\nWe are developing a commercial real estate intelligence platform focused on multifamily properties.\nWe are looking for an experienced data engineer or API integration specialist…"
    }
  ]
}
```

Fixed-price posts carry `"Fixed price"` in `budget` and no `duration`; hourly posts carry the rate range when the client set one, and the expected length. Need a spreadsheet? Load `parsed["job_postings"]` into pandas with `pd.json_normalize(...)` and write CSV or Excel.

## Scrape Upwork from your coding agent

Claude Code, Cursor, Codex and any MCP client can scrape Upwork job searches from a prompt.

The agent sends the same Fetch and Extract request a developer would, so it reads the job records above and can filter, count or summarize them for you. Connect the Zenrows MCP server once, or install the CLI and let the agent run it in the terminal.

```bash Connect the MCP server (Claude Code)
claude mcp add zenrows -e ZENROWS_API_KEY=YOUR_ZENROWS_API_KEY -- npx -y @zenrows/mcp
```

```json MCP config (Cursor, Claude Desktop, Windsurf)
{
  "mcpServers": {
    "zenrows": {
      "command": "npx",
      "args": ["-y", "@zenrows/mcp"],
      "env": { "ZENROWS_API_KEY": "YOUR_ZENROWS_API_KEY" }
    }
  }
}
```

```bash Or run the CLI
npx -y @zenrows/cli init
zenrows extract "https://www.upwork.com/nx/search/jobs/?q=web%20scraping"
```

```text Then ask your agent
Use Zenrows to scrape the newest Upwork jobs for "web scraping" and
"python automation". Keep the hourly posts at expert level, list the
skills each one asks for, and return title, budget, duration and skills
as a table.
```

## Scrape Upwork at scale with Batch and Crawl

**Batch** runs a list of Upwork searches as one job, in parallel, with your own ID on each result. Put one URL per keyword or skill, sorted by recency, tag each with the keyword, and run the job every few hours to see new posts as they appear.

**Crawl** follows links from an Upwork page and returns every page it reaches. Start it on a job search, keep the links that contain `/jobs/`, and it collects the search pages and job pages linked from it. Send the job pages to Batch with Extract.

```bash Batch (cURL)
curl -X POST "https://async.api.zenrows.com/v1/jobs" \
  -H "X-API-Key: YOUR_ZENROWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "regular",
    "status": "closed",
    "zenrows_params": { "mode": "auto", "extract": "auto" },
    "tasks": [
      { "url": "https://www.upwork.com/nx/search/jobs/?q=web%20scraping&sort=recency", "external_id": "web-scraping" },
      { "url": "https://www.upwork.com/nx/search/jobs/?q=python%20automation&sort=recency", "external_id": "python-automation" }
    ]
  }'

# Read the results when the job finishes
curl "https://async.api.zenrows.com/v1/jobs/JOB_ID/results?status=successful" \
  -H "X-API-Key: YOUR_ZENROWS_API_KEY"
```

```bash Batch (CLI)
# searches.jsonl: one {"url": "...", "external_id": "...", "extract": "auto"} per line
zenrows batch create searches.jsonl --wait
zenrows batch results <job-id> --out upwork-jobs.jsonl
```

```bash Crawl (cURL)
curl -X POST "https://api.zenrows.com/v1/crawls" \
  -H "X-API-Key: YOUR_ZENROWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.upwork.com/nx/search/jobs/?q=web%20scraping",
    "depth": 2,
    "max_pages": 100,
    "include_patterns": ["/jobs/"]
  }'
```

**Scheduled jobs** run the same Batch job on a timetable, on Zenrows' side: every few hours, once a day, at set times on chosen days, or once at a future date. Each run keeps its own results, a webhook can tell your app when a run finishes, and you can pause or resume the schedule at any time, with no cron server to keep alive.

```bash Scheduled job (cURL)
# Runs every day on Zenrows' side; each run keeps its own results
curl -X POST "https://async.api.zenrows.com/v1/jobs" \
  -H "X-API-Key: YOUR_ZENROWS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "type": "scheduled",
      "status": "closed",
      "schedule": {
        "rate": {
          "every": 1,
          "unit": "day"
        }
      },
      "zenrows_params": {
        "mode": "auto",
        "extract": "auto"
      },
      "tasks": [
        {
          "url": "https://www.upwork.com/nx/search/jobs/?q=web%20scraping",
          "external_id": "upwork"
        }
      ]
    }'

# Or at set times: "schedule": { "calendar": { "times_of_day": ["09:00"],
#   "cadence": { "weekly": { "days": ["mon", "thu"] } } }, "timezone": "Europe/Madrid" }
```

## What teams do with Upwork data

| Use case | Data they pull | Who runs it |
|---|---|---|
| Job alerts for freelancers and agencies | New posts for a keyword, with budget, level and skills, every few hours | Freelancers, agencies and their tooling |
| Skill demand research | Count of posts per skill tag and keyword over time | Analysts, course creators and bootcamps |
| Rate benchmarking | Hourly ranges and fixed budgets by skill and experience level | Agencies and freelance marketplaces |
| Market sizing for a service | Total jobs per search and the share of hourly versus fixed work | Founders and product teams |
| Lead lists for dev shops | Projects that ask for the stack you sell, such as n8n, Supabase or Shopify | Sales teams at agencies and software vendors |
| AI agents that triage work | Post titles and briefs as JSON or Markdown, ranked against a profile | AI and automation teams |

## Tips for scraping Upwork

- **Sort by recency for monitoring.** `&sort=recency` puts the newest posts first, so a short run every few hours sees every new job without walking deep pages.
- **Encode spaces in the keyword.** Upwork search URLs use `%20` between words, as in `q=web%20scraping`. Copy the URL from your browser or encode the query before you build it.
- **Page with `page`.** Add `&page=2`, `&page=3` and so on, and stop when a page returns no posts you have not seen.
- **Deduplicate on title and description.** A post can show up under several keywords. Hash the title and the first part of the brief to drop repeats across searches.
- **Split the budget field.** `budget` holds "Fixed price" or "Hourly" with the range, as in "Hourly: $90.00 - $120.00". Parse the two numbers out of hourly posts for rate analysis.
- **Read the whole brief.** Clients often write their full requirements in `description_snippet`, including stack, deliverables and timeline. It is the field to send to an LLM for classification.

## Frequently asked questions

### Is it legal to scrape Upwork?

Collecting public job posts, such as titles, budgets and skills shown to every visitor, is common practice for market research. Posts and profiles can include personal data, which laws such as the GDPR and the CCPA protect, so collect only what you have a legitimate reason to use and leave out names and contact details. Check Upwork's terms for your use case and ask your lawyer if you are unsure.

### Is there an official Upwork API?

Yes. Upwork offers a GraphQL API that needs an API key request, which Upwork reviews, and OAuth 2.0 tied to an Upwork account. It is built for apps that serve Upwork users, and its terms limit how the data is used. For public job search data at volume without an approved app, teams use a scraper.

### What Upwork pages can I scrape?

Public job search pages at upwork.com/nx/search/jobs/ , with any keyword, sort and page in the URL. Pages behind the Upwork login, such as messages, proposals and contracts, are out of scope.

### How do I scrape Upwork with Python?

Send a GET request to https://api.zenrows.com/v1/ with your API key, the Upwork search URL, Adaptive Stealth Mode and Extract turned on, as in the Python example above. The response's parsed key holds the job posts as a list of dictionaries. The zenrows package on PyPI wraps the same request.

### How do I get alerts for new Upwork jobs?

Run your searches sorted by recency on a schedule, from a cron job, n8n, Make or Zapier, and compare each run with the last one. Anything new goes to Slack, email or a sheet. Batch runs all your keywords in one job.

### Can I scrape Upwork freelancer profiles?

The scraper is built for job search pages. Freelancer profiles hold personal data, so if you collect any, limit it to public professional information you have a legitimate reason to use, and never contact details.

### Can AI agents scrape Upwork with Zenrows?

Yes. Connect the Zenrows MCP server to Claude, Cursor, Codex or any MCP client, or give your agent the Zenrows CLI. Ask for the searches you want in plain language and the agent calls Fetch and Extract for you.

### Can I scrape Upwork on a schedule?

Yes. Create a scheduled Batch job with your Upwork URLs and the same settings as a single request, and Zenrows runs it on its side: every few hours, once a day, at set times on chosen days, or once at a future date. Each run keeps its own results, a webhook can notify your app when a run finishes, and the schedule can be paused or resumed at any time.

### Which integrations work with the Upwork scraper?

Zenrows works with n8n, Make, Zapier, Clay, Dify, LangChain, LlamaIndex, CrewAI and Mastra, plus the Python, Node.js and Go SDKs and any HTTP client. A scheduled workflow can post each new job to Slack or a CRM. See the integrations page for the full list. For salaried roles, see the Indeed scraper .

### What format does the data come in?

JSON by default, with the structured record in parsed and the original page in html . You can also ask for the page as Markdown or plain text, which is what most LLM and RAG pipelines want.

All scrapers: https://www.zenrows.com/scrape. Start free: https://app.zenrows.com/register. Docs: https://docs.zenrows.com
