Search job postings
Search public Upwork job postings by keyword or by an Upwork search page address, with the platform's own filters — job type, experience level, budget band, client history, location and posting age. Every field here is optional on its own, but the request carries a cross-field rule: **give either `query` or `searchUrl`**. An empty body is a 400 in about 80ms — `"Provide either a query or a searchUrl."` — not an unfiltered search of everything. **Coverage caveat, and the first thing to build around.** A search always returns the listing-level fields Upwork puts on its results page. The fuller record behind each posting — the exact posting time, the proposal count, the client's hire history — is **not always available**, and what is available varies by hour. When it is not, this route still answers **200**: the postings carry listing-level fields only, `enrichedJobs` comes back **lower than `jobs.length`** (0 when nothing expanded), every job is marked `enriched: false`, and `notes[]` says so in words — *"Full detail was not available for this request, so these postings carry listing-level fields only (see \"enriched\" on each job). Exact posting times, proposal counts and client hire history are omitted. Retrying often succeeds."* **Detect it with one comparison: `enrichedJobs` against `jobs.length`.** A thin row is a posting whose detail was not published to this request, not a posting with no proposals — reading it the second way silently corrupts any analysis built on it. Retrying often succeeds. Billing: 1 credit per results page fetched, reported as `pagesFetched`. A page costs the same whether it returns 3 postings or 50, and a posting that came back without its fuller record costs nothing extra.
/search-jobs Jobs https://api.fetchlayer.dev/upwork/search-jobs Headers
Authorization string required Bearer token — your FetchLayer API key.
Parameters
query string optional Keywords to search postings for, e.g. "python developer". Either this or `searchUrl` must be given — an empty body is a 400.
searchUrl string optional An Upwork job-search page address to read the filters off, e.g. https://www.upwork.com/nx/search/jobs/?q=react. The easiest way to reproduce a search built in the Upwork UI. Either this or `query` must be given.
page string optional Which results page to start from. (default: 1)
pages integer optional How many results pages to walk, up to 20. Each page is 1 credit. (default: 1)
perPage integer optional Postings per page requested from Upwork, up to 50.
limit integer optional Maximum postings returned in total, up to 500.
sort string optional Result order: relevance or newest. (default: relevance)
experienceLevel string optional Narrow to one level: entry, intermediate, expert.
jobType string optional Narrow to hourly or fixed postings.
paymentVerified boolean optional Only postings from clients whose payment method Upwork has verified.
fixedPriceRange string optional Fixed-price budget band, in the spelling Upwork uses on its own filter.
hourlyRateRange string optional Hourly rate band, in the spelling Upwork uses on its own filter.
clientHistory string optional Narrow by how much the client has hired before, in Upwork's own vocabulary.
location string optional Narrow to postings open to freelancers in a location.
maxJobAge string optional Only postings newer than this age, in the spelling Upwork uses on its own filter.
filters object optional Any further Upwork filter parameters, passed through as the search page's own query fields.
includeDetails boolean optional Ask for every posting to come back with its fuller record. Best-effort: when it is not available, the response is still a 200 with listing-level fields, `enrichedJobs` below `jobs.length` and a note. Verified against prod — setting it true does not guarantee it.
format string optional Envelope format: json or markdown. (default: json)
Response Fields
search-jobs
Search job postings
transport string The source. "upwork-jobs".
notes string[] Caveats you can act on. The one that matters here says per-posting detail was not available and the postings carry listing-level fields only.
pagesFetched number Pages read, and what the request is billed at: 1 credit per page. A search of one results page is 1 whether it returns 3 postings or 50.
query string The keywords that were searched, or null when the request was made with `searchUrl` alone.
searchUrl string The Upwork results page the search read.
pages number[] Which results pages were walked.
totalJobs number How many postings came back.
enrichedJobs number **How many of them came back with their fuller record.** Compare this to `jobs.length`: lower means the rest carry listing-level fields only. This is the field to branch on, not `notes`.
jobs[].id string The posting's Upwork id, in its `~02…` form. Pass it to /job-detail as `jobId`.
jobs[].uid string The same id without its prefix.
jobs[].title string The posting title as published.
jobs[].description string The posting body. On a listing this is the text Upwork shows on the results page.
jobs[].url string The posting's full address. Pass it to /job-detail as `jobUrl`.
jobs[].jobType string "hourly" or "fixed".
jobs[].budget object Parsed rather than passed through as text: {type, amount, min, max, currency, raw}. A fixed-price posting fills `amount` ({type:"fixed", amount:210, currency:"USD", raw:"Est. budget:$210.00"}). An hourly posting with a published range fills `min` and `max` ({type:"hourly", min:15, max:100, currency:"USD", raw:"Hourly: $15.00 - $100.00"}). An hourly posting with no published range fills neither and keeps `raw: "Hourly"` — so a null `min` means Upwork published no rate, not that the rate is zero. `raw` is always the string Upwork showed, so any parse can be checked against it.
jobs[].experienceLevel string The level the client asked for: entry, intermediate, expert (published capitalised, e.g. "Intermediate").
jobs[].relativeDate string The posting age exactly as Upwork wrote it, e.g. "Posted 1 hour ago".
jobs[].postedAt string An ISO timestamp. On an unenriched posting this is derived from `relativeDate`, so it is accurate to the bucket Upwork published rather than to the minute.
jobs[].postedAtIsApproximate boolean True when `postedAt` came from the relative label rather than from an exact published time — which is the normal case on a listing. Check it before treating `postedAt` as a precise ordering key.
jobs[].skills string[] The skill tags on the posting, from Upwork's own annotations.
jobs[].proposals number The exact proposal count. Published only on an expanded posting; null on a listing-level one.
jobs[].proposalsRange object {min, max, label} — the bucket Upwork shows on the results page, e.g. {min:50, max:null, label:"50+"}. This is what you get when `proposals` is null, and it is usually present even on an unenriched posting.
jobs[].duration string Expected project length as published, e.g. "1 to 3 months".
jobs[].workload string Expected time commitment as published, e.g. "Less than 30 hrs/week".
jobs[].freelancersToHire number How many people the client intends to hire, when published.
jobs[].clientLocation string The client's country as Upwork shows it.
jobs[].clientPaymentVerified boolean Whether Upwork has verified the client's payment method.
jobs[].clientRating number The client's average feedback score. 0 on a client with no history — check `clientFeedbackCount` before reading a 0 as a bad rating.
jobs[].clientFeedbackCount number How many reviews that rating is based on.
jobs[].clientTotalSpent number Total the client has spent on Upwork.
jobs[].clientHireRatePercent number Share of the client's postings that ended in a hire. Published on an expanded posting.
jobs[].clientAvgHourlyRate number Average hourly rate the client has paid. Published on an expanded posting.
jobs[].clientName string The client name, where one could be established.
jobs[].clientNameConfidence string How firmly `clientName` was established, so a guess is never mistaken for a published name.
jobs[].clientHasHired boolean Whether the client has hired before. Published on an expanded posting.
jobs[].allowedApplicantCountries string[] Countries the client restricted applicants to, when they did.
jobs[].screeningQuestions string[] Questions applicants must answer, when the posting has any.
jobs[].category string Upwork's category for the posting.
jobs[].categoryGroup string Upwork's broader category group.
jobs[].enriched boolean Whether this posting came back with its fuller record. **False is the field to branch on**: it means the fields above marked "published on an expanded posting" are absent for this row, and it is not an error.
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
{
"query": "python developer",
"searchUrl": "https://www.upwork.com/nx/search/jobs/?q=python+developer",
"pages": [
1
],
"totalJobs": 5,
"enrichedJobs": 0,
"jobs": [
{
"id": "~022100333565184435123",
"uid": "2100333565184435123",
"title": "Full Stack Developer for Web App",
"description": "We need a full stack developer to build a web app for a small business. …",
"url": "https://www.upwork.com/jobs/Full-Stack-Developer-for-Web-App_~022100333565184435123/",
"jobType": "hourly",
"budget": {
"type": "hourly",
"amount": null,
"min": 15,
"max": 100,
"currency": "USD",
"raw": "Hourly: $15.00 - $100.00"
},
"experienceLevel": "Intermediate",
"relativeDate": "Posted 1 hour ago",
"postedAt": "2026-09-16T21:52:00.000Z",
"postedAtIsApproximate": true,
"skills": [
"Python",
"JavaScript",
"HTML5",
"PHP",
"AngularJS",
"TypeScript"
],
"proposals": null,
"proposalsRange": {
"min": 50,
"max": null,
"label": "50+"
},
"duration": "1 to 3 months",
"workload": "Less than 30 hrs/week",
"clientLocation": "United States",
"clientPaymentVerified": true,
"clientRating": 0,
"clientFeedbackCount": 0,
"clientTotalSpent": 0,
"enriched": false
},
{
"id": "~022100291793583666591",
"uid": "2100291793583666591",
"title": "Digital Skills Teacher",
"description": "…",
"url": "https://www.upwork.com/jobs/Digital-Skills-Teacher_~022100291793583666591/",
"jobType": "fixed",
"budget": {
"type": "fixed",
"amount": 210,
"min": null,
"max": null,
"currency": "USD",
"raw": "Est. budget:$210.00"
},
"experienceLevel": "Intermediate",
"relativeDate": "Posted 2 hours ago",
"postedAt": "2026-09-16T20:52:00.000Z",
"postedAtIsApproximate": true,
"skills": [
"Teaching",
"Curriculum Development"
],
"proposals": null,
"proposalsRange": {
"min": 10,
"max": 15,
"label": "10 to 15"
},
"clientLocation": "United Kingdom",
"clientPaymentVerified": true,
"enriched": false
}
],
"notes": [
"Full detail was not available for this request, so these postings carry listing-level fields only (see \"enriched\" on each job). Exact posting times, proposal counts and client hire history are omitted. Retrying often succeeds."
],
"transport": "upwork-jobs",
"pagesFetched": 1
}