Upwork API & Scraper
The Upwork API returns the open Upwork job board as clean JSON in three actions, with no Upwork account and no login of yours anywhere in the path.
🤖 Using an AI assistant? Copy this link into ChatGPT / Claude / Cursor — it reads every endpoint and parameter instantly and tells you if this API fits your use case.
search takes a keyword, or no keyword at all to browse the whole open board newest-first, and returns up to 200 postings per call. Every row carries the job id and cipher id, the listing URL, the title, the full brief, the contract type, the fixed amount or the hourly range with its currency, the experience level asked for, the project length label, the required skills with the numeric skill uid next to each one, the posting timestamp as both an ISO string and an epoch, the number of freelancers wanted, the premium and enterprise flags, and Upwork's own total match count for the query. Those eleven core fields were present on 50 of 50 rows of a live page. job opens one posting in full and adds what the card does not carry: the category and occupation, the project length in weeks, skills weighted mandatory or nice to have, the applicant requirements, how many people have applied, been interviewed and been hired, Upwork's own list of similar jobs, and the client's history, which is the country, city, timezone, total spend, hires, hours billed, review count, rating and member-since date. filters returns Upwork's own filter vocabulary read straight off the live search page, 351 rows in all including 263 client locations and 72 timezones, and it costs nothing. Filtering is Upwork's own, and each filter offered was checked against Upwork's own match total rather than assumed: on one keyword whose unfiltered total was 1,925, a week-long project length cut it to 929, ongoing to 1,055, full-time workload to 1,112, as-needed to 1,431, Germany to 20, the United Kingdom to 88, Europe to 388, clients who have never hired to 776, clients with ten or more hires to 677 and a single skill uid to 1,323. A posted-within-hours window is ours rather than Upwork's, because a logged-out visitor gets no date filter, and it reports how many rows it removed. One ReefAPI key, the standard { ok, data, meta, error } envelope, and job postings only: no freelancer profiles and no contact data.
Six Upwork values that are not what they look like
The search card and the job page publish different things, and three of Upwork's own words mean something narrower than they read. These were measured on 2026-09-24 against 50 live search rows and 13 live job pages.
| Field | Looks like | Actually is | What comes back |
|---|---|---|---|
| client.total_spent on a search row | a parser miss | Upwork does not publish client history on the search card at all, measured 0 of 50 rows | null, with the key kept rather than dropped, so the absence is visible. The job action carries the real value |
| client.total_spent on a job page | a gap | null when that client has never paid anyone, which is a real fact about the client | the amount on 4 of 13 job pages, null on the rest. client.country, rating, reviews, total_hires, hours_billed and member_since were 13 of 13 |
| duration "semester" | a school term | Upwork's own word for a 3 to 6 month project | semester in, and the readable Upwork label out in duration_label. The spelled-out forms such as 3_to_6_months are accepted too |
| client_hires "10-" | a typo, or up to ten | Upwork's band for ten or more hires | the band as Upwork names it. 10+ is accepted as an alias for the same band |
| skill_uid | something you look up in a directory | a numeric Upwork skill id, and Upwork publishes no skill directory to a logged-out visitor | every job row carries skills_detail[].uid, so you read the uid out of a first search and filter with it on the next |
| a job call returning NOT_FOUND | a bad id | Upwork declining to render that posting to anyone who is not signed in, which it does for a minority of listed jobs | NOT_FOUND rather than a block, because it is an answer about the job and not about us. search still carries that job's full brief |
Measured 2026-09-24 across 24 live searches on 12 unrelated queries plus every offered filter, and 13 job pages. workload is on 32 of 50 search rows because Upwork leaves it unset on fixed-price briefs, and renewed_at on 1 of 50. client.company_name and client.total_jobs_posted were 0 of 13 on the job page: Upwork does not publish them to a logged-out visitor, so the keys are kept and always null.
Real request and response JSON
Captured from the indexed primary action, search, on .
{
"method": "POST",
"url": "https://api.reefapi.com/upwork/v1/search",
"headers": {
"x-api-key": "$REEF_KEY",
"content-type": "application/json"
},
"body": {
"query": "python",
"limit": 10
}
}{
"ok": true,
"meta": {
"api": "upwork",
"endpoint": "search",
"mode": "live",
"latency_ms": 14793,
"record_count": 10,
"bytes": 1300775,
"cache_hit": false,
"stop_reason": "limit_reached",
"charged_credits": 1,
"version": "1.0.0"
},
"data": {
"jobs": [
{
"id": "2103587861243036781",
"cipher_id": "~022103587861243036781",
"url": "https://www.upwork.com/jobs/~022103587861243036781",
"title": "Python Scraper",
"description": "Hi, \nI have a scraper which got some error which need to fix and it also needs to upgrade. Let me know if you can do it on urgent basis. I need it asap. in response mention \"URGENT\" to make sure you read job description carefully.",
"job_type": "fixed",
"budget": {
"type": "fixed",
"amount": 10,
"currency": "USD"
},
"experience_level": "intermediate",
"duration_label": "Less than 1 month",
"workload": null,
"skills": [
"Python",
"Data Scraping",
"Selenium"
],
"skills_detail": [
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
},
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
},
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
}
],
"posted_at": "2026-09-25T20:50:03.971Z",
"posted_at_epoch": 1790369403,
"published_at": "2026-09-25T20:55:04.300Z",
"published_at_epoch": 1790369704,
"renewed_at": null,
"freelancers_to_hire": null,
"is_premium": false,
"is_enterprise": false,
"hiring_now": null,
"client": {
"payment_verified": false,
"country": null,
"total_spent": null,
"total_reviews": null,
"rating": null
}
},
{
"id": "2093710949001962859",
"cipher_id": "~022093710949001962859",
"url": "https://www.upwork.com/jobs/~022093710949001962859",
"title": "Python developer",
"description": "# Python & AI Automation Developer Needed\n\nWe’re looking for an experienced **Python Developer with AI Automation expertise** to upgrade and optimize our website’s existing automation system.\n\n### Responsibilities:\n\n* Develop and improve **Python-based AI automation workflows**.\n* Integrate AI models, APIs, CRM, and website backend.\n* Automate **lead generation, lead qualification, follow-ups, and meeting scheduling**.\n* Build and manage **API, webhook, and database integrations**.\n* Work with **n8n, OpenAI/Claude APIs, and other AI tools**.\n* Debug, optimize, and improve the reliability of ex",
"job_type": "hourly",
"budget": {
"type": "hourly",
"min": 20,
"max": 42,
"currency": "USD"
},
"experience_level": "intermediate",
"duration_label": "1 to 3 months",
"workload": "part_time",
"skills": [
"AI Builder",
"AI Development",
"AI Model Integration"
],
"skills_detail": [
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
},
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
},
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
}
],
"posted_at": "2026-08-29T14:42:44.547Z",
"posted_at_epoch": 1788014564,
"published_at": "2026-08-29T14:44:00.391Z",
"published_at_epoch": 1788014640,
"renewed_at": null,
"freelancers_to_hire": null,
"is_premium": false,
"is_enterprise": false,
"hiring_now": null,
"client": {
"payment_verified": false,
"country": null,
"total_spent": null,
"total_reviews": null,
"rating": null
}
},
{
"id": "2097833332735551069",
"cipher_id": "~022097833332735551069",
"url": "https://www.upwork.com/jobs/~022097833332735551069",
"title": "Python Automation Engineer",
"description": "Need a Python Engineer to complete the following task:\n'1) read two parameters from a csv file\n2) update a text file used by syslog-ng\n3) save the updated file as text file on windows OS.",
"job_type": "hourly",
"budget": {
"type": "hourly",
"min": 30,
"max": 60,
"currency": "USD"
},
"experience_level": "expert",
"duration_label": "More than 6 months",
"workload": "full_time",
"skills": [
"Python",
"Python Script",
"Automation"
],
"skills_detail": [
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
},
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
},
{
"name": "[trimmed-depth]",
"uid": "[trimmed-depth]",
"matched_query": "[trimmed-depth]"
}
],
"posted_at": "2026-09-09T23:43:37.405Z",
"posted_at_epoch": 1788997417,
"published_at": "2026-09-09T23:45:02.193Z",
"published_at_epoch": 1788997502,
"renewed_at": null,
"freelancers_to_hire": null,
"is_premium": false,
"is_enterprise": false,
"hiring_now": null,
"client": {
"payment_verified": false,
"country": null,
"total_spent": null,
"total_reviews": null,
"rating": null
}
}
],
"total_count": 1920,
"returned": 10,
"offset": 0,
"filtered_out": null,
"source": "upwork"
}
}What the Upwork API does
| Action | Description | Concrete use case | Key params |
|---|---|---|---|
| search | Search live Upwork jobs by keyword and Upwork's own filters — contract type, experience level, project length, weekly workload, client location, how many people the client has hired, client timezone — plus a posted-within-N-hours cut. Returns title, full description, budget, skills, posting timestamp and coarse client signals for each job, with the total match count. | Recruiting teams call search to search live Upwork jobs by keyword and Upwork's own filters. | query, limit, offset, job_type, experience_level, ... |
| job | Everything Upwork publishes about one job: the full brief, budget (fixed amount or hourly range), category and occupation, skills with mandatory/nice-to-have weighting, experience level, project length in weeks, freelancer requirements (countries, languages, minimum job-success score and hours), how many people have applied, been interviewed and been hired, and the client's history — country, city, timezone, total spend, jobs posted, hires, hours billed, review count, rating and member-since date. Plus Upwork's own list of similar jobs. Note that Upwork withholds a minority of listed jobs from anyone who is not signed in; those return NOT_FOUND. `search` already carries the full brief, budget, skills and posting time for every job, so this endpoint is for the ones you shortlist. | Labor-market analysts call job to get everything Upwork publishes about one job. | id |
| filters | The exact filter vocabulary Upwork's own job search ships — every accepted client location (263 countries, regions and sub-regions), timezone (72), contract type, experience level, project length, workload and client-hire band. Read straight off the live search page, so it cannot drift from what `search` accepts. | Job boards call filters to get the exact filter vocabulary Upwork's own job search ships. | none |
Call search from your stack
curl -X POST https://api.reefapi.com/upwork/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"query":"python","limit":10}'import requests
r = requests.post(
"https://api.reefapi.com/upwork/v1/search",
headers={"x-api-key": REEF_KEY},
json={
"query": "python",
"limit": 10
},
)
print(r.json()["data"])const res = await fetch("https://api.reefapi.com/upwork/v1/search", {
method: "POST",
headers: {
"x-api-key": process.env.REEF_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
"query": "python",
"limit": 10
}),
});
const { ok, data, meta, error } = await res.json();Ask your MCP-connected assistant: call reefapi.upwork.search with {"query":"python","limit":10}.Who uses this API and why
- Run a freshness feed for an agency: browse the board newest-first with a posted-within-hours window and alert on new postings matching your skill uids.
- Price a service line by reading fixed amounts and hourly ranges across hundreds of live postings in one contract type and experience level.
- Score a lead before writing a proposal: open the shortlisted job and read the applicant count, the hires so far and the client's spend, rating and hours billed.
- Map demand by market: hold the keyword steady and move client_location across countries, regions and timezones to see where the work is being posted.
- Track which skills are actually being bought: pull skills_detail uids off a week of postings and rank them by how many jobs each uid returns.
Questions developers ask before integrating
Do I need an Upwork account, and can my account get banned?
No account, and nothing of yours is exposed. You send a ReefAPI key. No Upwork login of yours is anywhere in the path, so there is no account of yours to rate-limit, to flag or to suspend. Only the public job board is read, which is what Upwork serves to any visitor who is not signed in.
How many Upwork jobs can one search return, and how deep can I page?
Up to 200 jobs in a single call. Upwork serves 50 per page, so a limit above 50 costs extra page fetches and 200 is four of them. offset runs from 0 to 5,000, which puts roughly 5,200 postings within reach for any one query. total_count always reports how many jobs Upwork says match, whether or not you page through them, so narrowing by skill uid, client location or contract type is how you reach the rest. Leaving query out browses the whole open board, which was 109,000 plus jobs when it was measured.
Why are the client's spend and rating null on my search results?
Because Upwork does not publish them there. Client history is not on the search card: total_spent, country, total_reviews and rating were null on 50 of 50 live rows, and only payment_verified is present. The keys are returned as null rather than dropped, so it reads as "Upwork does not publish this here" and not as a parser miss. The job action opens a single posting and returns the client's country, timezone, rating, review count, hires, hours billed and member-since date, which were present on 13 of 13 job pages measured.
Why did a job id come back NOT_FOUND?
Upwork withholds a minority of listed jobs from anyone who is not signed in, and it does so for that job specifically. That was checked rather than guessed: the same ids were refused while control ids succeeded in the same seconds, so it is Upwork declining to render the posting rather than anything on our side, and NOT_FOUND is the honest code for it. There is no predictor on the search row, measured across a dozen row fields. In practice it matters little, because search already carries the full brief, budget, skills and posting time for every job. Use job for a shortlist, when you want the client's history and the proposal count.
Can I filter Upwork jobs by budget, hourly rate or proposal count?
No, and rather than accept those parameters and quietly ignore them, they are not offered. Upwork serves its budget, hourly-rate, payment-verified and proposal-count filters only to signed-in members. Two more, contract-to-hire and occupation, were measured as no-ops: the match total and the returned ids did not move. What is offered was proven to narrow against Upwork's own total: contract type, experience level, project length, weekly workload, client location, client hire band, client timezone, skill uid and sort order. The budget itself is on every returned row, so filtering on it in your own code costs nothing extra.
How do I get only the jobs posted in the last few hours?
Use posted_within_hours, and know that it is ours rather than Upwork's. A logged-out visitor gets no posted-date filter at all, so the search is forced to newest-first and paging stops at the first job older than your window. The response reports filtered_out, which is how many rows the cut removed, so the window is visible rather than silent. Every row also carries posted_at and posted_at_epoch if you would rather cut it yourself.
How do I filter by skill when Upwork publishes no skill list?
You read the uid out of your own results. Upwork publishes no skill directory to a logged-out visitor, so skill_uid takes the numeric uid that every job row already carries in skills_detail[].uid. Run one search on a keyword, take the uids that keep appearing, then filter with them. It is more precise than a keyword: one Python uid returned 1,323 jobs of a 1,925 keyword total. Category is deliberately not offered for the same reason in reverse, because a caller could not discover a legal category value.
What does Upwork leave out, and what is never returned?
Client history is absent from the search card, as above. On the job page, client.company_name and client.total_jobs_posted were 0 of 13 because Upwork does not publish them to a visitor, so those keys are kept and always null. client.city was on 6 of 13 and the client's industry and company size on 2 of 13, only when the buyer filled in a company profile. workload is unset on many fixed-price briefs, 32 of 50 rows. This is a job-board API: it returns postings and the buying business behind them. No freelancer profiles, no applicant identities, and no e-mail, phone or street address for anyone.
What is the Upwork API?
Upwork API is a ReefAPI endpoint group for live freelance job postings with the full brief, fixed or hourly budget, required skills and experience level. It returns live JSON through POST requests under /upwork/v1.
Is the Upwork API free to try?
Yes. ReefAPI starts with 1,000 free credits, no card required. Upwork calls use the same shared credit balance as every other ReefAPI engine.
Do I need an Upwork login or account?
No login to Upwork is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.
How fresh is the Upwork data?
The page example is captured from a live search call, and production requests fetch live data through ReefAPI rather than a static sample.
How many credits does the Upwork API use?
Upwork actions currently cost 1 credit per successful call. Failed or blocked calls are free. All APIs draw from one credit pool.
Can I call Upwork from an AI assistant or MCP client?
Yes. Connect ReefAPI once through MCP and your assistant can call upwork actions with the same key, credit pool and JSON envelope used by normal REST requests.