Upwork Freelance Jobs and Budgets Scraper avatar

Upwork Freelance Jobs and Budgets Scraper

Pricing

from $2.25 / 1,000 results

Go to Apify Store
Upwork Freelance Jobs and Budgets Scraper

Upwork Freelance Jobs and Budgets Scraper

Scrape Upwork job posts by keyword, newest first, through the same search API the site itself calls. Each row has the title, the full post description, a fixed budget or hourly range as numbers, the currency, the experience level the client asked for, Upwork's skill tags, posted time and job URL.

Pricing

from $2.25 / 1,000 results

Rating

0.0

(0)

Developer

String

String

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

1

Monthly active users

13 days ago

Last modified

Categories

Share

This Actor collects freelance job posts from Upwork search. Hand it keywords and it returns the newest matching posts: the description a client wrote, whether the work is fixed-price or hourly, the money as numbers, the experience level asked for, Upwork's own skill tags and when the post went live.

What does the Upwork Jobs Scraper do?

Upwork's search page is a Nuxt bundle whose state blob is a minified function with its values hoisted into positional arguments, so nothing on the page reads back as JSON. The Actor skips the page and calls the GraphQL search the app itself calls, which answers with typed JSON: budgets as numbers, skills as labels, an explicit job type and a real publish timestamp.

That call needs Upwork's visitor token, which the search page issues as a Set-Cookie header and never puts in the page body. Each run mints one token up front and reuses it for every keyword. No Upwork account, no login, no Freelancer Plus subscription.

  • Results are sorted by recency, so a scheduled run gives you the new posts rather than a shuffled page of old ones.
  • budget and the hourly bounds are gated on the job type Upwork assigned, so a fixed-price post never shows an invented hourly rate.
  • Money arrives from Upwork as decimal strings and is parsed to numbers before it reaches the dataset.
  • skills are Upwork's own ontology labels, which makes them safe to group on.
  • The query selection stays inside what an anonymous visitor may read. Asking for a scope the visitor token lacks aborts the entire response rather than nulling one field, so client history is deliberately left out.

What data does it extract?

FieldTypeDescription
jobIdstringUpwork's job ID. Stable, so use it as the de-duplication key
titlestringThe post's title
descriptionstringThe description text as Upwork's search returns it
jobTypestring"FIXED" or "HOURLY"
budgetnumberFixed-price total. Populated only when jobType is "FIXED"
hourlyMinnumberBottom of the hourly range, only when jobType is "HOURLY"
hourlyMaxnumberTop of the hourly range, only when jobType is "HOURLY"
currencystringISO code of the fixed-price budget, e.g. "USD"
experienceLevelstringThe tier the client asked for, e.g. "IntermediateLevel", "ExpertLevel"
skillsstring[]Upwork's skill labels, e.g. ["Data Scraping", "Python"]
postedAtstringISO 8601 timestamp of publication
jobUrlstringDirect link to the post
clientCountrystringAlways null. See the note below
clientTotalSpentnumberAlways null. See the note below
querystringThe keyword that produced the row
sourceUrlstringThe Upwork search URL for that keyword
collectedAtstringISO 8601 timestamp of the run

clientCountry and clientTotalSpent are part of the row shape but are never filled. Client identity and spend sit behind a scope the anonymous visitor token does not hold, and reading them would take one extra fetch per job. They stay as explicit nulls rather than being quietly dropped from the schema.

Why scrape Upwork?

  • Rate benchmarking. What clients actually offer for a skill, split by fixed-price and hourly, with experienceLevel to separate junior work from senior.
  • Demand tracking. Run the same keywords weekly and count posts per skill. skills and postedAt are enough to plot a trend line for a stack.
  • Lead lists for an agency or freelancer. Newest posts first, filtered on budget floor, with the apply link attached.
  • Sizing a service market. Sum budgets by skill over a month to see what a niche is worth before you enter it.
  • Feeding a job-alert product. Join runs on jobId and alert on what is new since the last run.

How to use it

  1. Open the Actor on Apify Store and click Try for free.
  2. Put your keywords in Search terms, one per line, up to 50. "react developer", "shopify", "technical writer".
  3. Click Start. The run gets a visitor token first, then searches each keyword in parallel.
  4. Open the Dataset tab and export as JSON, CSV or Excel, or read it through the API.
  5. To track a market rather than sample it, save the input as a task and schedule it hourly or daily. Recency sort means each run's new jobId values are the new posts.

There is no location or budget filter. queries is the only filter Upwork's visitor search takes, so narrow with the keyword and filter the returned rows on budget, hourlyMin, experienceLevel or skills.

Input

FieldTypeDefaultDescription
queriesstring[]requiredKeywords to search for, 1 to 50. Duplicates collapse to one fetch
maxItemsinteger1000Cap on dataset rows, 1 to 50000. Free plans stop earlier, see below
concurrencyinteger3Keywords fetched in parallel, 1 to 5
{
"queries": ["react developer", "shopify", "technical writer"],
"maxItems": 500,
"concurrency": 3
}

Concurrency is capped at 5 for this Actor, lower than the 10 most of our others allow.

Output

One row per job post. Illustrative values, not a captured run.

{
"jobId": "1826104573920145408",
"title": "React developer to rebuild a customer dashboard",
"description": "We have an existing dashboard in Vue that we want rewritten in React...",
"jobType": "HOURLY",
"budget": null,
"hourlyMin": 25,
"hourlyMax": 45,
"currency": null,
"experienceLevel": "IntermediateLevel",
"skills": ["React", "TypeScript", "REST API"],
"postedAt": "2026-08-20T07:41:12.000Z",
"jobUrl": "https://www.upwork.com/jobs/~021826104573920145408",
"clientCountry": null,
"clientTotalSpent": null,
"query": "react developer",
"sourceUrl": "https://www.upwork.com/nx/search/jobs/?q=react+developer&sort=recency",
"collectedAt": "2026-08-20T09:31:02.118Z"
}

A fixed-price post is the mirror image: budget and currency set, both hourly bounds null.

Reliability

The rows are Upwork's own JSON response, field for field. Nothing is parsed out of rendered markup and no model sits in the path, so a number is the number Upwork holds.

A keyword whose search cannot be read is recorded in the run's SUMMARY under failures, together with Upwork's own explanation when it gave one. Upwork answers a rejected call with HTTP 200 and a message rather than an error status, so that message is carried into the failure record instead of being lost. A run in which every keyword failed exits with an error.

Limitations

One request per keyword, asking for Upwork's 50 newest matches, which is the most the endpoint returns for a visitor. There is no pagination input, so a run samples the top of the market rather than crawling it; the measured average is around 37 posts per keyword. Ten keywords come back with roughly 370 posts.

Client country, client total spend, hire rate, proposal and applicant counts, and freelancer profiles are all out of scope. So is filtering by location or budget at the source.

How much does it cost?

Pay per event: you are charged per result row written to the dataset, plus Apify's platform usage for the run. The current per-result price is on this Actor's Store page under Pricing.

Minting the visitor token at the start of a run writes no rows and costs you nothing, and neither does a keyword that fails.

Runs started from an Apify free plan stop at 250 requests and 250 results and report that in the run's status message. Any paid plan runs the full input and whatever maxItems you set. The limit exists because this Actor fetches through our own anti-bot infrastructure, which Apify does not cover for free-plan runs. It binds on requests as well as rows so that a long input list cannot spend those fetches on rows the run will never return.

Using it with the Apify API

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('usestring/upwork-jobs').call({
queries: ['react developer', 'shopify'],
maxItems: 500
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const hourly = items.filter((job) => job.jobType === 'HOURLY' && job.hourlyMax);
const topOfRange = hourly.map((job) => job.hourlyMax).sort((a, b) => a - b);
console.log(`${hourly.length} hourly posts, median top rate`, topOfRange[Math.floor(topOfRange.length / 2)]);

The Python client and the REST API take the same input. The run's SUMMARY record, with the item count and any failed keywords, is in the default key-value store.

The Actor searches Upwork as an anonymous visitor, using the same public endpoint the site's own search page uses and the visitor token that page hands out to anyone who loads it. No account is created, no credentials are supplied and nothing behind a login is read.

Job posts are advertisements clients published so freelancers can find and bid on them. The output holds no freelancer profiles, no client identities and no contact details.

Public data collection is broadly lawful in the US and the EU; how you use the data is governed by your own jurisdiction and agreements. This is not legal advice.

Fiverr is the useful pairing: Upwork prices the demand side, Fiverr the supply side, for the same skill keyword.

FAQ

Do I need an Upwork account or a paid plan? No. The run searches as an anonymous visitor with the token Upwork's own search page issues.

How many posts does one keyword return? One call per keyword asking for Upwork's 50 newest matches; the measured average is about 37. There is no way to page deeper as a visitor, so use narrower keywords to widen coverage.

Are these really the newest posts? Yes. Every search is sorted by recency, and postedAt carries the publish timestamp if you want to filter further or measure how quickly a market moves.

Do I get the client's country, spend or hire rate? No. Those live behind a scope the anonymous visitor token does not hold. The fields exist in the row shape and are always null, which is honest about what the run could see.

Can I filter by budget or client location? Not at the source. queries is the only input filter, so filter the returned rows on budget, hourlyMin, experienceLevel or skills after the run.

What happens if a keyword fails? It goes into failures in the run's SUMMARY with Upwork's own message where there is one, and the other keywords still return their rows. If every keyword fails, the run errors out.

Feedback

Found a post that came back wrong, or need a field that is not in the table? Open an issue from this Actor's Store page with the keyword you searched and roughly when you ran it.