Upwork Jobs Scraper avatar

Upwork Jobs Scraper

Pricing

from $0.27 / 1,000 job posts

Go to Apify Store
Upwork Jobs Scraper

Upwork Jobs Scraper

Scrape Upwork job posts by keyword or search URL: title, full description, skills, budget, hourly rate, experience level, duration and publication time. Export to JSON, CSV or Excel.

Pricing

from $0.27 / 1,000 job posts

Rating

0.0

(0)

Developer

Nice Dev

Nice Dev

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 hours ago

Last modified

Share

💼 What is Upwork Jobs Scraper?

Upwork Jobs Scraper turns any Upwork job search into a clean dataset. Give it a keyword — or paste a search URL straight from Upwork — and it returns every matching job post with its full description, its skills, its budget or hourly range, the experience level, the project duration and the exact publication time, ready for JSON, CSV or Excel.

It reads the same job posts an anonymous visitor sees on Upwork's search pages. No Upwork account needed, and it is quick: 250 job posts in about 20 seconds, 1,000 in under a minute, which is why it is one of the cheapest Upwork scrapers on the Store.

Try it: type python in Search keyword, hit Start, and look at the Output tab.

📋 What data can you extract from Upwork?

One row per job post, 23 fields:

CategoryWhat you get
🏷️ Job posttitle, link to the job page, Upwork's own id — Fix Broken Python Scraper
📝 Description and skillsthe full text as the client wrote it, line breaks kept; every skill asked for — Python, Scrapy
💰 Budgetfixed price, or hourly range, or weekly retainer, in USD — $20 / $40-60 an hour
🎓 Experience levelentry level, intermediate or expert
⏳ Duration and hourshow long the project lasts, how many hours a week — 1 to 3 months
🕒 Dateswhen it was posted, to the millisecond — what the date filters read
🔎 Where it came fromwhich of your keywords found it, the search page, when it was read

Every field, with an example, is listed in the Output section below.

✅ Why use Upwork Jobs Scraper?

  • 🔔 Monitoring that costs next to nothing when nothing is new. Turn Only new job posts on and a repeat run returns only what appeared since: a run that finds nothing saves 0 rows and is charged only the run-start fee (plus the pages your filters kept nothing of, if you set any).
  • 🎯 Every filter Upwork itself offers, in plain words: job type, experience level, project duration, hours per week, contract-to-hire, budget, hourly rate, category, skills, client hires, client country, client time zone.
  • 🧹 Filters Upwork does not have, run on our side: publication date, words a job post must contain, words it must not.
  • 🗂️ Several keywords in one run — one search each, results deduplicated.
  • 🚀 Fast and cheap. A run of 250 job posts takes about 20 seconds, 1,000 under a minute — you pay for job posts, not for waiting.
  • 🚨 It tells you when it is wrong. If Upwork changes its pages, the run stops and says which field vanished, instead of quietly charging you for empty columns.

🚀 How to scrape Upwork

  1. Click Try for free.
  2. Type a keyword in Search keyword (python, web scraping, logo design) — or paste an Upwork search URL into Start URLs.
  3. Set Max job posts to the number you want.
  4. Optionally open Job filters and Client filters and tick what you need.
  5. Hit Start and watch the Output tab fill.
  6. Download the dataset as JSON, CSV or Excel, or fetch it from the API.

To watch a search instead of scraping it once: turn on Only new job posts, give it a Monitoring key of its own, and schedule the Actor every hour.

💰 How much does it cost to scrape Upwork?

This Actor uses pay per event pricing: $0.30 per 1,000 job posts saved ($0.29 on the Bronze plan, $0.28 on Silver, $0.27 on Gold and above), plus $0.001 per run start (10 cents per 100 runs). Platform usage (compute, proxy) is included in the price. Example: 10,000 job posts ≈ $3; an hourly monitor that finds 20 new job posts per run ≈ $5 a month.

A filtered-out job post is never charged, and neither is one a previous monitoring run already delivered. A results page on which your filters kept nothing is charged $0.00015 ($0.15 per 1,000 such pages): a filter that matches almost nothing stops being free to run, never expensive. Pages of job posts already delivered are not charged: a monitoring run without filters that finds nothing new costs only the run-start fee. To cap any run, set Maximum cost per run in the run options: the run stops before it would go over.

Upwork itself never returns more than 5 050 job posts for one search, whatever you set. To go past that, split your run: one search per category, per job type or per budget band.

⚙️ Input

You can set everything in the Console form, or pass this JSON to the API. A simple run:

{
"query": "web scraping",
"maxItems": 100,
"sortBy": "recency"
}

Hourly work for experienced freelancers, paid at least $40, posted in the last two days:

{
"query": "python",
"jobType": "hourly",
"experienceLevel": ["Intermediate", "Expert"],
"minHourlyRate": 40,
"postedAfter": "2 days",
"excludeKeywords": ["wordpress", "unpaid"],
"maxItems": 200
}

Watching three searches every hour, only the new ones:

{
"searchQueries": ["web scraping", "data extraction", "crawler"],
"onlyNew": true,
"stateKey": "scraping-watch",
"maxItems": 500,
"maxItemsPerQuery": 200
}
FieldNotes
startUrlsUpwork search URLs copied from the site. Max 100 URLs. The keyword, sort and page are read from the URL; the filters below still apply
query, searchQueriesKeyword, and more keywords (one search each), up to 100 per run
maxItems, maxItemsPerQueryMax job posts (100 by default, 0 = no limit), and the cap of each search
sortByrecency (newest first, the default), relevance or client_total_charge
jobTypehourly or fixed; empty = both
experienceLevelEntry level, Intermediate, Expert
projectDurationLess than one month, 1 to 3 months, 3 to 6 months, More than 6 months — checked on every job post, fixed-price ones included
workloadLess than 30 hrs/week, 30+ hrs/week, As needed — hourly job posts only, unless a fixed budget is set too
contractToHireOnly job posts flagged as leading to a long-term contract
minFixedBudget, maxFixedBudgetFixed price in USD — fixed-price job posts only, unless an hourly filter is set too: then each kind matches its own filter
minHourlyRate, maxHourlyRateHourly rate in USD — hourly job posts only, unless a fixed budget is set too
categories, subcategoriesUpwork category names, e.g. Web, Mobile & Software Dev; a name Upwork does not know stops the run at once and lists the valid ones
skillsSkills the job post asks for, e.g. Python, Web Scraping; several = the job post must ask for all of them
clientHiresNo hires, 1 to 9 hires, 10+ hires
clientLocationsCountry names as Upwork writes them, e.g. United States
clientTimezonesThe client's time zone as Upwork names it, e.g. Europe/Paris, America/New_York; several = any of them
postedAfter, postedBeforeA date (2026-09-01, a UTC day, the whole day included) or a period before now (7 days, 1 month; via the API also 2 hours)
includeKeywords, excludeKeywordsWords a job post must, or must not, contain
maxDescriptionLengthCut each description; 0 = the whole text
onlyNew, stateKey, resetStateMonitoring mode and its memory
proxyConfiguration, minRequestIntervalMs, maxRequestRetries, debugLogAdvanced

📦 Output

One item per job post:

{
"id": "2101592032775799065",
"ciphertext": "~022101592032775799065",
"url": "https://www.upwork.com/jobs/_~022101592032775799065/",
"title": "Fix Broken Python Scraper",
"description": "Need a quick fix for a broken Python scraper used on an e-commerce site.",
"skills": ["Python", "Scrapy", "Data Scraping"],
"skillsDetailed": [{ "uid": "996364628025274386", "name": "Python", "freeText": false }],
"jobType": "FIXED",
"budgetType": "FIXED",
"fixedBudget": 20,
"hourlyBudgetMin": null,
"hourlyBudgetMax": null,
"weeklyRetainerBudget": null,
"experienceLevel": "Intermediate",
"contractorTier": "IntermediateLevel",
"durationLabel": "1 to 3 months",
"durationWeeks": 9,
"hourlyEngagementType": null,
"publishedAt": "2026-09-20T08:40:24.219Z",
"createdAt": "2026-09-20T08:39:21.353Z",
"searchQuery": "python",
"searchUrl": "https://www.upwork.com/nx/search/jobs/?q=python&sort=recency&page=1",
"scrapedAt": "2026-09-20T09:49:31.185Z"
}

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

All 23 fields

FieldsWhat you get
title, descriptionJob post: the text as the client wrote it, line breaks kept
skills, skillsDetailedSkill names picked from Upwork's list, and the same with Upwork's own ids so you can re-filter. A skill the client typed in by hand is not in Upwork's search, so not here
jobType, budgetTypeFIXED, HOURLY or WEEKLY_RETAINER
fixedBudget, hourlyBudgetMin, hourlyBudgetMax, weeklyRetainerBudgetBudget: what the client is offering, in USD
experienceLevel, contractorTierEntry level, Intermediate or Expert, in words and in Upwork's code
durationLabel, durationWeeks, hourlyEngagementTypeDuration and hours: how long, and how many hours a week
publishedAt, createdAtISO 8601 UTC, to the millisecond — you can sort a monitoring feed by the second. publishedAt = when the post went live (what the date filters read); createdAt = what the job page counts "Posted … ago" from, hours earlier for about 1 post in 4
url, id, ciphertextThe public job page and Upwork's own references
searchQuery, searchUrl, scrapedAtWhich of your searches found it, and when

💡 Tips

How to get more results

Set maxItems to 0 and use a broad keyword, or several keywords in More search keywords. Upwork stops every search at 5 050 job posts: to go further, split it (see below).

How to reduce costs

The price is per job post, so the levers are maxItems, maxItemsPerQuery, the filters (a filtered-out job post is free; a whole page of them costs $0.00015) and onlyNew for recurring runs (you never pay twice for the same job post). With Sort by: Newest first and postedAfter, a search stops after 5 hours of older posts.

Several searches in one run

Put your keywords in More search keywords: each one becomes its own search. A job post found by two of them is saved once. Max job posts per search stops the first keyword from eating the whole budget. You can also paste several search URLs into startUrls: each one is a search of its own, with the same cap.

Monitoring: only the new job posts

Only new job posts remembers the ids it has delivered under your Monitoring key, in a store that never expires (upwork-jobs-scraper-seen, up to 150,000 listings per key). Give each watched search its own key. Reset monitoring memory forgets everything under that key and returns every match again.

Filter by publication date

postedAfter and postedBefore take a date (2026-09-01 is a UTC day, and the whole day is included) or a period before now (7 days, 2 weeks, 1 month; via the API also 2 hours or a full ISO date-time). The filter reads publishedAt. Filtered-out job posts are never charged and do not count in maxItems; the run summary tells how many were filtered. With Sort by: Newest first, a search stops after 5 hours of older posts (Upwork ranks a few posts up to 3 hours before it publishes them), so a short window costs very little. Upwork shows only the 5 050 newest posts of a search, about the last 31 hours on all of Upwork: postedBefore reaches older posts in a narrower search (a keyword, a category).

Getting more than 5 050 job posts

That is Upwork's own ceiling for one search, not ours. Split the run: the same keyword with jobType hourly then fixed, or one run per entry of categories, or by budget band.

🔌 Integrations and API

Call the Actor via the Apify API, the JavaScript or Python clients, or connect it with integrations and webhooks (Make, Zapier, n8n, Google Sheets, Slack, Airtable…). The dataset can be fetched as JSON or CSV from any tool.

🤖 Use with AI agents (MCP)

AI agents (Claude, ChatGPT, Cursor…) can find and run this Actor through the Apify MCP server, billed to their Apify account like any run. It returns one item per Upwork job post. Actor id: nice_dev/upwork-jobs-scraper; MCP server with this Actor only: https://mcp.apify.com/?tools=fetch-actor-details,nice_dev/upwork-jobs-scraper.

Smallest input, for a cheap first call:

{
"query": "python",
"maxItems": 10
}

Key output fields: url, title, description, skills, jobType, fixedBudget, hourlyBudgetMin, publishedAt.

Cost: $0.30 per 1,000 job posts plus $0.001 per run start ($0.27 per 1,000 on the Gold plan); a results page on which the filters kept nothing costs $0.00015, see the pricing section above. Cap each call with maxItems and, through the API, with the run option maxTotalChargeUsd.

❓ FAQ

The Actor only reads what Upwork shows publicly to any anonymous visitor. It logs in to nothing. Results describe job posts written by clients and can contain personal data, which is protected by GDPR: do not store it without a legitimate reason. You are responsible for using the data in compliance with Upwork's Terms of Use and applicable law. This Actor is not affiliated with Upwork.

Does it need a login or a proxy?

No login. The proxy is included in the price: leave the default setting (the residential proxy is not available). A request the site turns away is retried at once on a new proxy session (without a proxy, after a pause of 5 seconds, doubled at each retry up to 150 seconds).

Does it return the client's spending, rating or number of proposals?

No. Those figures are not part of what Upwork shows an anonymous visitor on its search pages, so this Actor does not return them. If you need the client's spending history or the number of proposals, this is not the Actor for you — and we would rather say so here than let you find out after a paid run.

Is the data safe to open in Excel or to show on a web page?

Titles and descriptions are the clients' own words, copied as they are. A text can begin with -, +, = or @ (-20% rate for a long contract, =SUM of my needs): Excel and Google Sheets may read such a cell of a CSV file as a formula or as a number. The Actor leaves the text as it is, so that the JSON and the API give the real value: when you open a CSV, import these columns as text. Every URL field holds an https://www.upwork.com/… address built from Upwork's own reference, never a link copied from a payload. On a web page, escape every field like any text written by a stranger.

Known limitations

  • 5 050 job posts per search, Upwork's own ceiling (a search URL pasted past page 101 returns nothing). Split the run to go further.
  • Only the keyword, the sort and the page are read from a pasted search URL: set the filters in the form, they apply to pasted URLs too.
  • No client information, no proposal count, no "payment verified" flag — see above.
  • There is no filter for the number of proposals or for payment verification, because Upwork's search offers none.
  • onlyNew remembers job ids, not their content: a job post the client edited is not returned again.
  • Two runs sharing the same stateKey at the same time may both return the same new job post.

A run the platform stops without warning (out of memory, run timeout)

  • Resurrect it: it goes on from where it stood at most a minute before the stop. What it had read since is read again, and the job posts already saved are skipped: none is delivered or charged twice, and maxItems still counts them.
  • With onlyNew, the memory is saved once a minute: resurrect the stopped run and the job posts it had saved meanwhile join the memory; leave it stopped for good, and the next run may return up to a minute of them once more.

Something doesn't work?

The last line of the log counts the job posts saved, filtered out and already delivered, and the requests that failed after every retry. Those requests are listed, with the reason, in the FAILED_REQUESTS record of the run's key-value store. A run that saved nothing and had failed requests fails, and its last message gives the cause (a search URL that does not exist says so, instead of "run it again"). A run that saved job posts can still fail: when at least as many requests failed for good as were read (page 1 read, the next pages blocked), it keeps what it saved and fails with "Most requests failed", so that a blocked run never looks like a complete one.

If Upwork changes its pages, you are told instead of paying for blank rows. A results page that counts job posts but gives none the Actor can read is an error (listed in FAILED_REQUESTS), never a quiet "No listings found". If the first 20 listings read all lack a column every job post fills (title, description, publishedAt, createdAt, jobType, experienceLevel, durationLabel, durationWeeks, skills, ciphertext, and fixedBudget or hourlyEngagementType on the posts of that kind), the run saves nothing more, stops and fails, and its last message names the missing field: at most those first listings are charged. A job post that postedAfter / postedBefore drops because it has no date at all counts among those 20.

🛟 Support

Open an issue in the Issues tab with a link to your run: the run log and the FAILED_REQUESTS record of the key-value store show exactly which URLs failed and why.