Upwork API — Getting Started — FetchLayer Docs
Documentation menu
FetchLayer FetchLayer API

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.

bash
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

Every posting always carries the listing-level fields. The fuller record behind a posting — the exact posting time, the exact proposal count, the client’s hire rate — is not always available, and how much of it is varies by hour. 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.

bash
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

Sending {} 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.

bash
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

This route returns only the fuller record, so when that record is not available there is nothing thinner to hand back and you get a 503: “The upstream source is temporarily refusing automated requests. Please retry.” Four consecutive calls returned it during the window measured on 17 September 2026, while /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.

bash
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.

Next steps