# Upwork API for agents

> API for Agents · https://apiforagents.dev/platforms/upwork · HTML twin: https://apiforagents.dev/platforms/upwork

Job search from Upwork with its own filters.

Endpoint: `POST https://apiforagents.dev/v1/upwork`. One op per call. Quote first with `"estimateOnly": true`.

## Prices (USD per record, pay as you go)

| Record | Per record |
|---|---|
| jobs | $0.0027 |

## Operations

### search

Search Upwork jobs with the marketplace's own filters. Every hit a full posting with budget, client profile and proposal counts.

| Field | Required | Type | Meaning |
|---|---|---|---|
| query | yes | string (≤300 chars) |  |
| sort | no | enum |  Values: relevance, newest. |
| includeHourly | no | boolean | Keep hourly-rate jobs. Set both this and includeFixed false and nothing matches. |
| hourlyMin | no | integer (0-) | USD per hour. |
| hourlyMax | no | integer (0-) | USD per hour, the upper bound; only hourly jobs are affected. |
| includeFixed | no | boolean | Keep fixed-price jobs. |
| budgetMin | no | integer (0-) | USD, the fixed-price budget. |
| budgetMax | no | integer (0-) | USD, the fixed-price budget's upper bound; only fixed jobs are affected. |
| experienceLevel | no | list of enum | The level the client asked for. Any listed level matches; empty = all. Values: entry, intermediate, expert. |
| clientHistory | no | list of enum | How many jobs the client has hired for before: none, 1-9, or 10+. Empty = any client. Values: 0, 1-9, 10-. |
| projectLength | no | list of enum | Expected duration: under a week, under a month, 1-6 months, or ongoing (6+). Empty = any length. Values: week, month, semester, ongoing. |
| hoursPerWeek | no | list of enum | Weekly commitment: part-time (under 30 h) or full-time (30 h+). Empty = either. Values: part_time, full_time. |
| clientLocation | no | list of string (each ≤60 chars) | Client countries, as Upwork spells them ("United States"). |
| limit | no | integer (5-100) |  |

Response fields (each row of data; every field present, null when the source had nothing):

| Field | Type |
|---|---|
| jobId | string |
| jobUrl | string |
| jobTitle | string |
| jobDescription | string |
| posted | string |
| deadline | string |
| jobType | string |
| projectType | string |
| projectLength | string |
| weeklyWorkload | string |
| hourlyRateMin | number |
| hourlyRateMax | number |
| fixedBudget | number |
| hourlyBudgetSource | string |
| experienceLevel | string |
| englishLevel | string |
| skills | any |
| skillGroups | list of string |
| category | string |
| categoryGroup | string |
| occupation | string |
| applicants | integer |
| interviewing | integer |
| invitesSent | integer |
| unansweredInvites | integer |
| hired | integer |
| positionsToHire | integer |
| jobStatus | string |
| jobAccess | string |
| contractToHire | boolean |
| featuredJob | boolean |
| localMarketJob | boolean |
| locationCheckRequired | boolean |
| risingTalentOk | boolean |
| minJobSuccessScore | number |
| freelancerType | string |
| requiredCountries | string |
| requiredRegions | string |
| requiredTimezones | string |
| requiredLanguages | string |
| client | any |
| clientOtherOpenPostings | any |
| similarJobs | any |
| resultRank | integer |
| details | object |

`details` is always the last field: everything else the source sent for that row, under our own field names. Its keys depend on the source that answered, so read them defensively; it is `{}` when there is nothing more.

Example request:

```bash
curl -X POST https://apiforagents.dev/v1/upwork \
  -H "Authorization: Bearer $AFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"op":"search","query":"shopify developer","sort":"newest","experienceLevel":["expert"],"clientLocation":["United States"],"limit":10}'
```

Example response rows (made-up names, real field names; a live row carries every field above):

```json
[
  {
    "jobTitle": "Build a Shopify product feed sync",
    "jobType": "fixed",
    "fixedBudget": 800,
    "experienceLevel": "intermediate",
    "projectLength": "1 to 3 months",
    "skills": [
      "Shopify",
      "Node.js",
      "APIs"
    ],
    "applicants": "10 to 15",
    "posted": "2026-10-15",
    "hourlyRateMin": null,
    "hourlyRateMax": null,
    "jobId": "2355543097",
    "jobUrl": "https://www.upwork.com/build-a-shopify-product-feed-sync-1000",
    "details": {}
  },
  {
    "jobTitle": "Shopify theme speed fixes",
    "jobType": "hourly",
    "fixedBudget": null,
    "experienceLevel": "expert",
    "projectLength": "Less than 1 month",
    "skills": [
      "Shopify",
      "Liquid",
      "Web performance"
    ],
    "applicants": "5 to 10",
    "posted": "2026-10-14",
    "hourlyRateMin": 30,
    "hourlyRateMax": 60,
    "jobId": "2614115498",
    "jobUrl": "https://www.upwork.com/shopify-theme-speed-fixes-1001",
    "details": {}
  }
]
```

Try it in the playground: https://apiforagents.dev/dashboard/run?platform=upwork&op=search (sign in first).

### history (free)

Stored proposal-count series of postings already held. Free.

| Field | Required | Type | Meaning |
|---|---|---|---|
| subjects | yes | list of string (1-10 items) |  |
| metric | no | enum |  Values: applicants, interviewing, invites_sent, hired. |
| days | no | integer (1-730) |  |

Request shape:

```json
{
  "op": "history",
  "subjects": [
    "example"
  ]
}
```

Try it in the playground: https://apiforagents.dev/dashboard/run?platform=upwork&op=history (sign in first).

## Notes for the agent

Measured Upwork job data, one operation per call. ops: search (a query with Upwork's own filters. Sort, hourly rate and fixed budget bounds, experience level, client hiring history, project length, hours per week, client country; every hit a full posting with budget, skills, the client's profile and spend, and live proposal counts: applicants, interviewing, invites, hired), history (stored proposal-count series of postings already held, by job id or URL, free). Fresh fetches are charged per record; your own answer is free while fresh.
