Upwork Jobs Scraper: New Job Alerts & Filters avatar

Upwork Jobs Scraper: New Job Alerts & Filters

Pricing

from $0.42 / 1,000 job posts

Go to Apify Store
Upwork Jobs Scraper: New Job Alerts & Filters

Upwork Jobs Scraper: New Job Alerts & Filters

Scrape Upwork job posts by keyword or search URL, or schedule it as a job alert that returns only new posts. Title, full description, skills, budget, hourly rate, experience level, duration and posting time, with 19 filters. Export JSON, CSV or Excel.

Pricing

from $0.42 / 1,000 job posts

Rating

0.0

(0)

Developer

Nice Dev

Nice Dev

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

2

Monthly active users

4 days ago

Last modified

Share

💼 What is Upwork Jobs Scraper?

Upwork Jobs Scraper turns any Upwork job search into a clean dataset — or into a job alert that returns only the new jobs. 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, with 19 filters to keep only the jobs you want, ready for JSON, CSV or Excel. It works as an Upwork API alternative: run it on a schedule as an Upwork job alert, call it from your code, or plug it into Make, Zapier or n8n.

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, 48 fields — 23 always, 25 more with the Client details option:

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
👤 Client details (option)money spent, hires, hours, reviews and rating, payment verified, country and city, member since, company; the job's proposals, interviews and invites — $26,694 spent · 53 hires · 5.0 · Saudi Arabia

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).
  • 👤 The client behind each job post, as an option: how much they have spent, how many freelancers they hired, their rating, whether their payment is verified, and how many proposals the job already has.
  • 🎯 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 (see the job alerts section below).

🔔 Upwork job alerts: monitor new jobs

Turn any Upwork search into a job alert: you get the new job posts and nothing else.

  1. Set your search: a keyword or a search URL, plus the filters that match the jobs you would apply to (skills, budget or hourly rate, experience level, client country, publication date).
  2. Turn on Only new job posts and give the search a Monitoring key of its own.
  3. Create a Schedule for the Actor in Apify (every hour, for instance).

The first run returns every job post that matches. Each later run returns only the job posts it has never delivered under that key, so a run that finds nothing new saves nothing. Send the dataset to Slack, Google Sheets, Make, Zapier or n8n with the Apify integrations, or read it from the API. Watch several searches in one run with More search keywords, and give each alert its own key so that two alerts never hide each other's job posts.

💰 How much does it cost to scrape Upwork?

This Actor uses pay per event pricing: $0.45 per 1,000 job posts saved ($0.44 on the Bronze plan, $0.43 on Silver, $0.42 on Gold and above), plus $0.0045 per run start (45 cents per 100 runs). Platform usage (compute, proxy) is included in the price. Example: 10,000 job posts ≈ $4.50; an hourly monitor that finds 20 new job posts per run ≈ $9.72 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.00023 ($0.23 per 1,000 such pages, $0.215 on Gold and above): 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.

With Include client details ticked, each job post delivered WITH its client's details is also charged the Client details event: $16.23 per 1,000 job posts with their client ($16.22 on the Bronze plan, $16.21 on Silver, $16.20 on Gold and above), on top of the job post. A job post whose page gives no client (a private or closed job) goes out without them and is not charged for it. The option is slower — about 3 seconds per job post — and the run uses more memory, set automatically when the box is ticked.

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
includeClientDetailsClient details option: each job post with its client (25 more columns), charged per job post that has them
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",
"clientDetailsRead": true,
"clientPaymentVerified": true,
"clientCountry": "Saudi Arabia",
"clientCity": "Saihat",
"clientTimezone": "Asia/Baghdad (UTC+03:00)",
"clientTotalSpent": 26694.47,
"clientHires": 53,
"clientActiveHires": 6,
"clientJobsWithHires": 51,
"clientTotalHours": 100.17,
"clientReviews": 40,
"clientRating": 5,
"clientMemberSince": "2022-09-02T00:00:00.000Z",
"clientIndustry": "Finance & Accounting",
"clientCompanySize": 1,
"clientJobsPosted": null,
"clientOpenJobs": null,
"clientAvgHourlyRate": null,
"clientIsEnterprise": false,
"applicantsCount": 9,
"interviewingCount": 6,
"invitesSent": 28,
"unansweredInvites": 22,
"hiredCount": 0,
"clientLastActivityAt": "2026-09-28T13:09:08.031Z",
"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 48 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
clientDetailsReadClient details option: true when the job's page gave its client; false = the fields below are null
clientTotalSpent, clientHires, clientActiveHires, clientJobsWithHires, clientTotalHours, clientAvgHourlyRateClient's history: money spent (USD), freelancers hired, contracts still active, job posts that ended in a hire, hours billed, average hourly rate paid
clientReviews, clientRating, clientPaymentVerifiedReviews received, average rating out of 5, payment method verified
clientCountry, clientCity, clientTimezone, clientMemberSinceWhere the client is, and since when on Upwork
clientIndustry, clientCompanySize, clientIsEnterprise, clientJobsPosted, clientOpenJobsThe client's company and job posts
applicantsCount, interviewingCount, invitesSent, unansweredInvites, hiredCount, clientLastActivityAtThis job's activity: proposals, interviews, invites, hires, and when the client last looked at it

💡 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.00023) 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. With filters on, a job post your filters drop is remembered too: the next runs with the same filters skip it, and a results page of nothing but such job posts is not charged. Change a filter, or reset the memory, to check them again. A job post that does not pass a time-dependent filter yet (a relative "posted before" date) is checked again on the next run. Sorted by relevance or client_total_charge, a new job post can sit on any page: a search then ends after 20 results pages in a row made only of job posts already delivered (a first run, with nothing delivered yet, never ends that way).

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.45 per 1,000 job posts plus $0.0045 per run start ($0.42 per 1,000 on the Gold plan); a results page on which the filters kept nothing costs $0.00023; with includeClientDetails, each job post delivered with its client costs $16.23 per 1,000 more ($16.20 on Gold), 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, up to 10 times on top of the retries (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?

Yes, with the Client details option. Tick Include client details: each job post then comes with its client's money spent, hires, hours, reviews and rating, payment verification, country, city and company, and with the job's proposals, interviews and invites. Each job post delivered with them is charged the Client details event on top of the job post: $16.23 per 1,000 ($16.20 on the Gold plan). A private or closed job gives no client: it goes out without, uncharged for it.

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.
  • Client details come with the option only, about 3 seconds per job post; a private or closed job has none.
  • 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 that reaches its timeout stops itself about 45 seconds before it: no new page is asked, what it read is saved and, with onlyNew, remembered, and the run ends Succeeded with "Stopped before the run's timeout". Resurrect it to go on from there, or give the next run a longer timeout (Run options).

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

  • 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 a few seconds after the job posts are written: 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 the last few seconds 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.