Getting Started
Public Upwork job postings as structured JSON. No Upwork account — just your FetchLayer key.
1. Get your API key
Create an account and generate a FetchLayer API key from your dashboard. The same key works on every platform.
Create free account →2. Search postings
Give a query, or an Upwork search page address as searchUrl. One of the two is required.
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}'Check enrichedJobs against jobs.length before you use the data
enrichedJobs tells you how many came back complete, and each row carries enriched so you can filter per posting rather than per response. A real response measured on 17 September 2026 returned 5 postings with enrichedJobs: 0 — a 200, with a note saying exactly that. Retrying often returns more. null is not zero
proposals: null on a listing-level posting means the exact count was not published to this request — and proposalsRange is still there with {"label":"50+"}. A posting nobody has applied to would be a real 0. Branch on enriched before you aggregate anything. 3. Reuse a search you already built
Build the search on Upwork with its own filter UI, then paste the address. The filters come across as-is, so there is no re-expressing a saved search as a dozen parameters.
curl -X POST https://api.fetchlayer.dev/upwork/search-jobs \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"searchUrl":"https://www.upwork.com/nx/search/jobs/?q=react","limit":3}'An empty body is a 400, on purpose
{} returns “Provide either a query or a searchUrl.” in about 80ms. Every individual field is optional, but the request has to name a subject — an unfiltered sweep of every posting on Upwork is not something anyone means to ask for. The body is also a strict object, so an unknown field is a 400 naming it rather than a field quietly ignored. 4. Read one posting in full
Pass a posting’s id or URL from a search result for the exact posting time, the exact proposal count, the screening questions and the client’s hire history.
curl -X POST https://api.fetchlayer.dev/upwork/job-detail \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"jobUrl":"https://www.upwork.com/jobs/Digital-Skills-Teacher_~022100291793583666591/"}'This route is best-effort, and says so with a 503
/search-jobs kept answering 200 throughout. Retry it, and use the search route when you need breadth rather than depth. 5. Sort a pile of pasted links first
Free, fetches nothing, and tells you which route each address belongs to — plus the search terms read straight off a results page.
curl -X POST https://api.fetchlayer.dev/upwork/resolve-url \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.upwork.com/nx/search/jobs/?q=python"}'That returns {"kind":"search","jobId":null,"searchUrl":"…","query":"python","pagesFetched":0} in about a tenth of a second — hand searchUrl or query straight to /search-jobs.
Speed and billing
One page of results measured 24–28 seconds against production, and costs 1 credit whether it returns 3 postings or 50. Several pages in one request take proportionally longer, so page through rather than batching if something is waiting on the response. /resolve-url is free.
Errors and retries
A 400 names what is wrong with the request — an empty body, or an unrecognised field. A 401 means the key is missing or invalid. A 503 on /job-detail means the fuller record was not available; retry it, and fall back to /search-jobs in the meantime.