The open Upwork job board as JSON, with Upwork's own filters
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.
3 active endpoints, on 0 and 1 credit tiers.
- POST/upwork/v1/search
- POST/upwork/v1/job
- POST/upwork/v1/filters
What Upwork endpoints does ReefAPI ship?
3 live read endpoints. Read-only data API: no writes, no account actions, no dashboard access on the target site.
Upwork API
3 of 3 endpoints, ready to run
Live Upwork postings by keyword and Upwork's own filters: title, full brief, fixed amount or hourly range, required skills with their uids, experience level, project length, posting timestamp and the total match count. Up to 200 a call.
// Press "Try it" and this pane shows exactly what the // live site returned this second — including an empty // result, if that is the truth. No key, no account.
How the Upwork API works
Upwork is a normal ReefAPI surface — the same four rules that hold for every other engine on the key.
No OAuth app, no request signing, no per-site account. One key covers all 294 engines.
Every route is a POST with a JSON body. Parameters are validated against the published schema before anything is charged.
Credits, not seats. Failed and blocked calls are never charged, and cache hits cost nothing.
One envelope everywhere. meta carries latency_ms, record_count and the endpoint that answered.
Read the vocabulary once, page the board, open only the shortlist
Two of Upwork's filters take values you cannot guess, and one of them costs nothing to fetch. The other, the skill uid, falls out of your first search. Getting both right is the difference between a keyword feed and a targeted one.
{}Zero credits. 351 vocabulary rows: the 263 client locations Upwork accepts, its 72 timezones, and the exact spellings for contract type, experience level, project length, workload and client-hire band. Fetch it once and cache it.
{"query": "python scraping", "limit": 50, "sort": "newest"}1 credit for up to 200 rows. Read total_count to see how big the market is before you page. Every row already carries the full brief, the budget, the skills and the posting time, so most work is done here. Note the skills_detail[].uid values that keep appearing.
{"skill_uid": "<a uid from above>", "client_hires": "10-", "posted_within_hours": 24}1 credit. A skill uid is stricter than a keyword, the hire band keeps buyers who have actually hired before, and posted_within_hours reports filtered_out so you can see what the window removed.
{"id": "<a row's cipher_id>"}1 credit. This is where the client's history lives, because it is not on the search card: spend, hires, hours billed, rating and member-since, plus the applicant and hire counts. Upwork withholds a minority of listed jobs from a logged-out visitor and those return NOT_FOUND, so shortlist first.
A feed scoped to real Upwork skill ids and real buyer bands rather than a keyword, with the competition and the buyer's track record read only on the jobs you actually care about.
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}'{
"ok": true,
"data": { … },
"meta": {
"api": "upwork",
"endpoint": "search",
"mode": "live",
"latency_ms": …,
"record_count": …
},
"error": null
}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.
What the board gives a visitor, how deep it goes, and what it holds back
Measured 2026-09-24 on 24 live searches across 12 unrelated queries plus every offered filter, 50 rows of one page field by field, and 13 job pages. Three of these lines go against us.
You send a ReefAPI key and nothing else. No Upwork login of yours is involved at any point, so there is no account of yours that can be rate-limited, flagged or suspended. What is read is the public board Upwork serves to any visitor who is not signed in. Search ran 24 of 24 across 12 unrelated queries, and the five negative cases returned INVALID_PARAM, MISSING_PARAM, NOT_FOUND and UNKNOWN_ACTION rather than a 200 with an empty body.
limit runs to 200 in one call. Upwork serves 50 jobs a page, so 200 is four page fetches and anything over 50 costs extra. offset runs to 5,000, which puts about 5,200 postings within reach for any single query. total_count reports Upwork's own match total whether or not you page, and browsing with no keyword at all showed 109,000 plus open jobs, so narrowing by skill uid, client location or contract type is how you reach past the ceiling.
A logged-out visitor gets no posted-date filter from Upwork at all. posted_within_hours forces newest-first, stops paging at the first job older than the window and reports filtered_out, which is how many rows the cut removed. It is documented as our filter rather than dressed up as one of Upwork's. Every row also carries posted_at and posted_at_epoch if you prefer to cut it yourself.
total_spent, country, total_reviews and rating were null on 50 of 50 live search rows. Only payment_verified is there. The keys are kept and returned as null so the absence is visible rather than looking like a parser miss. The job action is where client history lives: country, timezone, rating, reviews, total hires, hours billed and member-since were present on 13 of 13 job pages, total spend on 4 of 13 because it is genuinely null when a client has never paid, and city on 6 of 13.
Some listed postings are not rendered to anyone who is not signed in, and those return NOT_FOUND. Measured 12 of 14 and 16 of 18 readable on the newest board pages, and 9 of 14 on older ids harvested from relevance-sorted searches, where more postings have since closed. There is no predictor on the search row, checked across a dozen row fields. It costs little in practice: search already carries the full brief, budget, skills and posting time for every job, so job is for the shortlist.
Checked against Upwork's own match total rather than assumed. On one keyword whose unfiltered total was 1,925: a week-long project cut it to 929 and ongoing to 1,055, full-time workload to 1,112 and as-needed to 1,431, Germany to 20, the United Kingdom to 88 and Europe to 388, clients who have never hired to 776 and clients with ten or more hires to 677, and a single skill uid to 1,323. Sorting by newest against relevance returned no overlapping ids on the first page.
Upwork serves its budget, hourly-rate, payment-verified and proposal-count filters only to signed-in members. Contract-to-hire and occupation were measured as no-ops: the match total and the returned ids did not move. None of the six is offered, because accepting a budget filter and silently ignoring it is worse than not having one. Category is left out for the opposite reason: it works, but Upwork publishes no category directory to a visitor, so no caller could discover a legal value.
id, cipher_id, url, title, the full description, contract type, experience level, project length label, budget, skills and both posting timestamps were present on 50 of 50 rows. workload was on 32 of 50, because Upwork leaves it unset on many fixed-price briefs, and renewed_at on 1 of 50. On the job page, client.company_name and client.total_jobs_posted were 0 of 13: Upwork does not publish them logged out, so the keys stay and are always null.
This is a job-board API. It returns the posting and the buying business behind it, which are business facts Upwork publishes itself. No freelancer profiles, no applicant identities, and no e-mail address, phone number or street address for anyone. The record is built from an explicit allow-list, so the buyer's own raw internal fields on the page cannot leak into the response.
search is 1 credit and returns up to 200 jobs, so a 200-row page and a single job cost the same: ask for the page. job is 1 credit. filters is free, because it is Upwork's own vocabulary and you only need it once.
What people build with Upwork
The jobs this data is most often used for.
endpoints
credits per call
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.
What Upwork data costs
The cheapest call here is 0 credits, so $15/mo (Pro) buys 10,000 of them — $1.50 per 1,000 credits. Credits roll over and never expire, and failed or blocked calls are not charged.
Full pricing →- 1,000 free credits on signup, no card
- One key, all 294 APIs, one credit pool
- Failed and blocked calls are never charged
- Credits roll over and never expire
Call it in two lines
Sign up, get 1,000 credits and one key that works on every engine. Then this is the whole protocol.
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"])Have a question? We got answers.
The questions people actually ask before wiring up Upwork.
Get a free key →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.
16 Jobs & Hiring APIs on the same key
One key, one credit pool, one response envelope. If you are pulling Upwork, you are one call away from the rest of the category — no second contract, no second integration.
Need something this API does not do?
Name the endpoint, the field, or a source we do not carry yet. We ship new APIs every week and you would be first to get the key. Real people read every message and reply the same day.
Try it on your own data before you pay anything
The call above is the real endpoint, not a recording. A free key gives you 1,000 credits, the other 293 APIs, and the same envelope everywhere.
Endpoints, parameters and credit costs on this page are read from the live catalog and cannot drift from what the API accepts. Field notes were captured on 2026-09-24.