Upwork data.
Without an Upwork account.
Job postings with parsed budgets, skills, durations, client spend and proposal ranges, as clean JSON via REST or MCP. Search by keyword, or paste an Upwork search URL.
Trusted by developers shipping real products.
246,915 served today
Request
$ curl -X POST \ https://api.fetchlayer.dev/upwork/search-jobs \ -H "Authorization: Bearer ss-your-key" \ -H "Content-Type: application/json" \ -d '{"query":"python developer","limit":5}'
Response
{
"totalJobs": 5, "enrichedJobs": 0,
"jobs": [{
"title": "Full Stack Developer for Web App",
"jobType": "hourly",
"budget": { "type": "hourly",
"min": 15, "max": 100, "currency": "USD" },
"proposals": null,
"proposalsRange": { "label": "50+" },
"clientPaymentVerified": true,
"enriched": false
}], "pagesFetched": 1
} Built for feedback workflows
Fetch the public signal. Use it wherever your team works.
What the client is paying, ready to sort
Every posting’s budget arrives as numbers you can rank on: the fixed price, or the hourly range with its currency, alongside the exact string Upwork displayed. Sort a day of postings by what the work is worth without writing a parser for "$15.00-$100.00/hr".
Your Upwork search, as an API call
Paste the search page address you already built on Upwork as searchUrl, and its filters come across as-is. No re-expressing a saved search as a dozen parameters, and no guessing at the spelling of a budget band.
The client, not just the posting
Location, whether their payment method is verified, their feedback score and how many reviews it rests on, and what they have spent on Upwork to date — so you can rank demand by who is actually paying rather than by who is posting.
The whole posting, not the preview
Title and full body, the skills asked for, category, experience level and expected duration — and on a posting you open in detail, its screening questions and the client’s hire history. Enough to decide whether the work is worth bidding on without opening Upwork.
Posting times you can order by
Every posting carries an ISO timestamp, so a day’s new work sorts by when it landed rather than by "Posted 1 hour ago". Where Upwork published only that relative label, the response says the timestamp was derived from it — so you know which ones to trust to the second.
How much competition a posting has
The exact proposal count where Upwork publishes one, and Upwork’s own band — "50+", "10 to 15" — where it does not. They are separate fields on purpose, so a posting nobody has applied to never looks the same as one whose count was simply not published.
Ask what the freelance market is paying—then export it.
Prefer not to write code? Describe the work in FetchLayer Research Chat and get a table back, with the postings whose detail was not available marked as such rather than quietly averaged in.
- ✓Ask questions in plain English
- ✓Analyze themes, sentiment, and pain points
- ✓Download the structured results as CSV or JSON
No code or setup required · Uses your FetchLayer credits
Find Upwork postings for "python developer" from payment-verified clients, and give me the hourly and fixed budgets, the skills asked for and how many proposals each has.
Twenty-eight postings. Hourly rates cluster at $20–45/hr, with two outliers to $100. Eleven are fixed-price, median $600. Nine came back with an exact proposal count; the rest show a range like "50+", and those are marked so you do not read them as zero.
upwork-postings.csv
28 postings · 9 columns · ready to download
Solutions
Build the workflow around the feedback.
Then find who to contact
Turn a company or a profile into the contact address it actually publishes, with the page it came from.
Explore →What companies are spending on
The B2B ads a company is running, with the copy and creative behind them.
Explore →What it is like to work there
Employer reviews, salary reports and interview experiences, with ratings and free text.
Explore →From raw feedback to action
One request is the start of the workflow.
Store the structured response, run it on a schedule, pass it to an LLM, or feed it into the system your team already uses. FetchLayer handles retrieval so you can focus on the outcome.
- 01
Search, or paste a search
Send a keyword with filters, or the address of an Upwork search page you already built. One of the two is required, so a request never turns into an unfiltered sweep of everything by accident.
- 02
Read enrichedJobs first
Compare it to the number of postings you got back. Equal, and every row is the full record. Lower, and the rest are listing-level — filter on each row's enriched flag before you aggregate anything.
- 03
Go deeper on the ones that matter
Send a posting's id or URL to job-detail for its exact time, exact proposal count, screening questions and the client's hire history. It is best-effort and says so: a 503 means retry, not that the posting is gone.
MCP & agents
Your AI already knows
how to use this.
Use the Upwork endpoint directly from your MCP-compatible agent. Ask it to retrieve public feedback, identify patterns, and turn the result into a useful brief without writing integration glue.
Pick your editor and we hand you the config with your key already in it — no JSON to hand-edit, no wrapper code.
Get your free API keyFree tier · no credit card · any MCP host
What are clients paying for Python work on Upwork right now?
Five postings on the first page. Hourly budgets run $15–100/hr; fixed-price ones from $10 to $2,760. Four clients are payment-verified. enrichedJobs was 0 of 5, so exact proposal counts were not available on this pass — the ranges are there ("50+", "10 to 15") and I have not treated any of them as zero.
Works with every MCP-compatible tool
Use cases
What teams build on public freelance demand.
Postings, budgets and the clients behind them, as rows you can sum. Here is what that is for.
Freelance rate benchmarking
Budgets arrive as numbers, not display strings, so a week of postings for a skill sums into a real hourly and fixed-price distribution instead of a pile of text to regex.
Demand tracking by skill
Run the same search on a schedule and watch which skills, durations and experience levels clients are actually posting for — and which have gone quiet.
Agency lead sourcing
Filter to payment-verified clients with real spend history, and work the postings where the budget and the track record both stand up.
Competitive bid intelligence
Proposal counts and ranges tell you how contested a posting is before you write anything. A "50+" and a null are different answers, and both are labelled.
Job board and newsletter feeds
Reuse a search you already built on Upwork as the query, and turn it into a curated feed with parsed budgets and skill tags on every row.
Talent market reports
Client location, spend and hire history alongside every posting, so a market report is about who is paying rather than only about who is posting.
Compare approaches
Spend time on the insight, not the collection layer.
| What you need | FetchLayer | Official API | Build it yourself |
|---|---|---|---|
| Budgets returned as numbers | ✓ | n/a | You parse the string |
| Incomplete rows labelled per posting | ✓ | n/a | You would not know |
| Reuse an Upwork search URL | ✓ | n/a | You re-implement filters |
| No Upwork account required | ✓ | n/a | You keep one warm |
| Ready for AI and MCP workflows | ✓ | n/a | Your integration |
FetchLayer provides access to publicly available data. It is not affiliated with, endorsed by, or sponsored by the platform.
Pricing
Start free. Scale when ready.
Pay per request with no commitment, or lock in a flat monthly rate with built-in savings.
Free plan — 30 requests
Verify responses, inspect the API, no credit card required.
Pay as you go
$1.99
per 1,000 requests
$0.00199 per request · credits never expire
How credits work — no multipliers
One credit = one API call, on every endpoint and platform. No matter how many results come back, you pay for the call, not the output size. Most scraping APIs charge multipliers of 5x, 25x, even 75x per call depending on the target — here it's always 1.
- No multipliers — 1 request = 1 credit
- No monthly commitment
- Credits never expire
- Same API & MCP access as subscribers
- ∞ req/min, ∞ API keys
∞
Expiry
Credits stay until you use them. No monthly reset, no pressure to hit a quota.
0
Commitments
No subscription, no contract, no minimums. Start, pause, or scale whenever.
100%
Access
Every endpoint and MCP included. Not a limited tier — same data as any subscriber.
API reference
Every parameter, documented.
All requests are POST with a JSON body and Bearer auth header.
POST /upwork/search-jobs Public job postings by keyword or by an Upwork search URL, with the platform's own filters. Every response reports enrichedJobs against the number of postings returned, so a thin result set is visible rather than assumed complete.
query string Keywords, e.g. "python developer". Give this or searchUrl. searchUrl string An Upwork search page address — its filters come across as-is. jobType / experienceLevel string 'hourly' | 'fixed'; 'entry' | 'intermediate' | 'expert' hourlyRateRange / fixedPriceRange string Budget bands, in Upwork's own spelling. paymentVerified / clientHistory string Narrow to verified clients, or by how much they have hired before. location / maxJobAge / sort string Where applicants may be, how new, and 'relevance' or 'newest'. limit / pages / perPage number Up to 500 postings across up to 20 pages, 1 credit per page. POST /upwork/job-detail One posting by id or URL, as its fuller record — the exact posting time, the exact proposal count, screening questions and the client's hire history. Best-effort: when that record is not available, this route answers 503 rather than handing back something thinner than you asked for.
jobId string The posting's id, from a search result. Give this or jobUrl. jobUrl string The posting's full address, from a search result. POST /upwork/resolve-url Sort a pile of pasted Upwork links into postings and searches, and read the search terms straight off a results page. Free — it reports pagesFetched: 0.
url* string Any Upwork job or search address. Fields marked * are required. Full API reference →
FAQ
Common questions.
What is the Upwork Jobs API?
A REST endpoint and MCP tool that returns public Upwork job postings as structured JSON. You send a keyword or an Upwork search URL with a Bearer token; you get back the postings with their titles, bodies, parsed budgets, skills, durations, client location and client spend. No Upwork account, no HTML to parse.
Do I need an Upwork account?
No. Everything returned is what Upwork publishes publicly on its job search — the same postings anyone can read without signing in. There is no login, no OAuth, and nothing to keep warm.
Can I reuse a search I built on Upwork?
Yes, and it is the fastest way to start. Paste the Upwork search page address as searchUrl and the filters on it come across as-is — job type, experience level, budget band, client history, location, posting age. Or pass query and set the filters as fields. One of the two is required: a request with neither is rejected immediately rather than running an unfiltered search of everything.
Are the budgets usable as numbers?
Yes. budget comes back parsed, not as a display string: {type, amount, min, max, currency, raw}. A fixed-price posting fills amount. An hourly posting with a published range fills min and max. An hourly posting where Upwork published no rate fills neither and keeps raw: "Hourly" — so a null min means no rate was published, not that the rate is zero. raw is always the exact string Upwork showed, so you can check any parse by eye.
Is every field filled in on every posting?
No, and this is the thing to know before you build on it. Each posting comes back with the listing-level fields Upwork puts on its results page, always. The fuller record behind a posting — the exact posting time, the exact proposal count, the client's hire rate and average paid rate — is not always available, and how much of it is varies by hour. Nothing is silently invented to fill the gap.
How do I tell which postings have the fuller record?
One comparison: enrichedJobs against jobs.length. Equal means every posting came back complete. Lower means the rest carry listing-level fields only, and each of those is marked enriched: false so you can filter on the row rather than on the total. notes[] says it in words too. Retrying often returns more.
What happens if I read a thin posting as a complete one?
You get a wrong answer that looks right — a posting with proposals: null read as a posting nobody has applied to. That is why enriched is on every row and why proposalsRange is still there when proposals is not: "50+" is a real answer, and null is not the same as zero. Branch on enriched before you aggregate.
How fast is it?
A page of results measured 24 to 28 seconds against production. Reading several pages in one request takes proportionally longer. If you are building something interactive, fetch one page per request and page through it rather than asking for many at once.
How is it billed?
One credit per results page, reported on every response as pagesFetched. A page costs the same whether it returns three postings or fifty, and a posting that came back without its fuller record costs nothing extra. Parsing an Upwork URL is free — it reports pagesFetched: 0.
Can my AI agent use it?
Yes. All three routes are MCP tools on the same FetchLayer MCP server: upwork_search_jobs, upwork_job_detail and upwork_resolve_url. Tool calls use the same key and are billed exactly like REST, and the agent sees enrichedJobs alongside the postings, so it can tell you when a result set is thin instead of presenting it as complete.
Start pulling Public job postings today.
Get a free API key, make your first request, and put public feedback to work in your own product or research workflow.
Used by developers at
Individual developers or teams — not official partnerships