# Upwork Job Scraper: Scheduled Job Alerts, Qualified Clients (`hyperbach/upwork-scraper-ai`) Actor

New Upwork jobs in seconds, served from a live-updated index — no login, no waiting on a scrape. Filter by client spend, payment-verified, hire rate and budget. Empty runs free. Start from any time; never miss a job. $2 per 1,000, no monthly fee.

- **URL**: https://apify.com/hyperbach/upwork-scraper-ai.md
- **Developed by:** [Hyperbach](https://apify.com/hyperbach) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Features:** Limited permissions
- **Stats:** 622 total users, 262 monthly users, 100.0% runs succeeded, 3 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Upwork Job Scraper: Scheduled Job Alerts, Qualified Clients

> **Scheduled job alerts from Upwork: run this Actor every few minutes and get the new postings from payment-verified clients with real spend — into Slack, email or a webhook through Apify's own integrations.** It answers from an index that is already current, so a call takes seconds and an empty check costs nothing. Qualify by client spend, hire rate and budget; start the feed from any moment you choose and never miss a job. **$2 per 1,000 results — no monthly fee, no login.**

![An Upwork job search page in a browser with a Python job posted 12 minutes ago. Beside it, a part of the row this Actor returns for the job: date_posted and created_at are highlighted, the timestamps a scheduled run uses to return only the jobs new since the run before; then the client's payment check, total spend, rating and location, and the hourly range.](https://api.apify.com/v2/key-value-stores/dcLLNGq99686o2A4v/records/hero-upwork-scraper-ai.webp)

**Built for freelancers, agencies, and job-board builders** who need to see a posting before everyone else has replied to it. Most scrapers go to Upwork at request time and hand you raw rows. This one is already watching.

### What is measured, not claimed

- **Freshness.** 99.8% of postings are available within 10 minutes of going live, median 2.9 minutes — measured on the live feed over the 7 days to 2026-09-17, not estimated.
- **A start point you own.** `created_at` is a value you pass, not state we keep. Among the published input schemas of the largest Upwork scrapers on the Store (checked 2026-09-06), none exposes one: their time filters are posting time, and their "new jobs only" modes are since-last-run state held on their side. Yours can be replayed, shared between machines, or resumed after downtime — and a job that Upwork lists late is never skipped.
- **The filters buyers actually use come first.** Payment-verified, total spent, client and job scores, hire rate, open jobs, invites — with comparison and range operators: `">=1000"`, `"500-2000"`. 61 filters in total; the ones nobody uses sit at the bottom of the form.
- **Screening questions captured per job**, ready for your proposal generator. AI-extracted skills and urgency for those who want them.
- **$2 per 1,000 results, less on paid plans, no monthly fee.** Zero-result runs cost nothing, so a tight polling schedule costs no more than a lazy one.

### 🧭 Three ways to use it

#### 1. Alerts — new jobs, minutes after they post

Set `notifications_only: true` and schedule the Actor every few minutes. Each run returns only the postings that arrived since your previous run, and can send them straight to Slack, Telegram or a webhook if you paste one URL — see **Job alerts in five minutes** below. Median time from posting to availability: 2.9 minutes.

```json
{ "notifications_only": true, "buyer_payment_verified": true, "total_spent": ">=1000", "limit": 50 }
```

#### 2. Research — search the last 90 days, open and closed

Leave `notifications_only` off (the default) and every filter runs against the whole index for the last **90 days**. Jobs stay in the index after they are filled or expire, so a search returns closed postings as well as open ones — what clients asked for and offered to pay across three months, not only what is still live. Built for researchers, analysts and anyone pricing a niche.

```json
{ "skills": "React", "date_posted": "90d", "price": ">=1000", "limit": 1000 }
```

The posting's text is as it read in its first minutes; its proposal and invite counts and its budget carry the newest reading we have, and `refreshed_at` says when (see **What is captured once, and what is kept current**). Whether a job has since closed is not a column — add `include_history: true` for its status over time, or pass its id to **Refresh mode** to read it now. For anything older than 90 days, [Upwork Jobs History](https://apify.com/hyperbach/upwork-jobs-history) has every posting since November 2024 — the last 12 months or all of it, by category and skill.

#### 3. Job history — add `include_history: true`, free

Set `include_history: true` in either mode and every row carries the job's recorded history, at no extra charge:

```json
{ "skills": "React", "date_posted": "7d", "include_history": true, "limit": 200 }
```

- **`history.readings`** — the job's state at each moment it changed: status, applicants, interviewing, invites, hires, the client's last activity and the budget. Identical readings are collapsed, so the list is the job's story, not a log of every look.
- **`history.edits`** — each change the client made to the posting itself, and what changed: the title before and after, skills added and removed, the description rewritten.

The history is served from our own database of recorded versions, not re-read from Upwork when you ask. That is why it can reach back to the first time we saw a job — which a scraper that only reads Upwork at run time cannot do.

We re-read every job in the 45-day window on a measured schedule: every 6 hours while it is under two days old, every 2 days until day 7, then once more before day 45. The schedule started on 2026-09-24, and jobs posted since then are re-read from their first hours. Older jobs are being worked through newest first; until the schedule reaches one, it comes back with empty lists. Readings for the jobs we already tracked go back to 2026-08-22.

### 📈 Who it's for

- **Freelancers** — poll every few minutes and see matching jobs while they are still fresh, with screening questions ready to drop into your proposal.
- **Agencies** — filter straight to high-budget, agency-friendly clients with real spend and hire history.
- **Job boards & SaaS** — power a niche Upwork job board or alerting product on a feed that stays current without you running scrapers.
- **Market researchers** — track pricing, demand, and skill trends across millions of postings.

### 🚀 Quick Start

#### 1. Basic Usage

```javascript
{
  "limit": 10
}
```

#### 2. With Smart Filtering

```javascript
{
  "limit": 20,
  "skills": "React",
  "price_min": ">=50",
  "buyer_score": ">=4.0",
  "ai_urgency": "Urgent"
}
```

#### 3. Notification Mode (Only New Jobs)

```javascript
{
  "notifications_only": true,
  "limit": 50
}
```

#### 4. Start the feed from a moment you choose

```javascript
{
  "created_at": ">=2026-09-01T00:00:00Z",
  "buyer_payment_verified": true,
  "total_spent": ">=1000",
  "limit": 100
}
```

`created_at` is when a job entered this index, not when Upwork posted it. Pass `>` the newest value you received and call again: you get exactly the jobs added since, including the ones Upwork listed late. A bound reads the whole millisecond it names, so `>` leaves out the job you copied the value from and `<=` keeps it. `notifications_only: true` (the run keeps the position) and `page_after` (below) also walk with no repeats and no gaps. The start point is yours to keep, replay, or share between machines — nothing on our side decides your position — the value you pass does.

#### 5. Paste a search from Upwork

```javascript
{
  "search_url": "https://www.upwork.com/nx/search/jobs/?q=n8n%20OR%20zapier&t=0&hourly_rate=40-&payment_verified=1&sort=recency",
  "notifications_only": true
}
```

Run the search on upwork.com, copy the address bar, paste it. Its keywords and filters become this Actor's inputs, and a field you set yourself wins over the URL. A filter the URL carries that this Actor cannot apply (proposal counts, time zones, two of three experience levels) is named in the status message and in each row's `input_warning`, never dropped in silence. A link to one job reads that job live, as `refresh_job_ids` does.

**Coming from another Upwork Actor?** Run your saved input as it is. The usual names are read as this Actor's own — `query`, `searchUrl`, `startUrls`, `maxResults`, `incrementalMode`, `jobType`, `experienceLevel`, `verifiedPaymentOnly`, `minClientTotalSpent`, `minClientRating`, `hourlyRate`, `budget`, `location`, `category`, `includeKeywords`, `excludeKeywords`, `maxAgeMinutes`, `telegramToken`, `slackWebhookUrl`, `webhookUrl` and more — and the status message says which name it read as which field.

### 📏 Check a search before you schedule it — `estimate: true`

Add `estimate: true` to any search and the run measures it instead of running it. It returns **one row**, billed as one result, and moves no feed position:

```json
{ "keywords": "\"brand film\" OR \"product commercial\"", "notifications_only": true, "estimate": true, "estimate_poll_minutes": 15 }
```

- **Volume** — how many jobs this exact search would have delivered over the last 28 days: per day, per week and per hour, plus the quietest and the busiest day. The count uses the same filter code a real run uses, so the estimate and the feed cannot disagree.
- **Band** — `too_narrow` (under one job a week), `healthy`, or `too_broad` (over 30 a day).
- **Cost per month** at each Apify plan's price, for how often you plan to run it (`estimate_poll_minutes`) and in the mode you chose. With `notifications_only: false`, a time window polled more often than it is wide re-sends the same jobs, and the estimate says how many times.
- **Which filter cuts the most** — for every filter, how many jobs a day the search would find without it.
- **5 recent matches** — title, budget and client spend, so you can see whether the volume is the right jobs.

The run's status message has the summary in one line, and the OUTPUT record has the same row. A relative date filter (`date_posted: ">=1h"`) is measured the way a scheduled feed reads it; an absolute `created_at` or `date_posted` is a start point, not part of the flow, and the row says it left it out. With keywords matched by meaning the count is approximate, and the row says so.

### 🔔 Job alerts in five minutes

Paste one URL into the form, schedule the task, and every run that finds new jobs sends **one message with those jobs** to your Slack, Telegram, or any webhook. A run that finds nothing sends nothing. Nothing to host, nothing to code, no account with us.

**1. The task.** Create a Task from this Actor with your filters (`buyer_payment_verified: true`, `total_spent: ">=1000"`, `keywords: "react frontend developer"`, …) and set **`notifications_only: true`** — each run then returns only the postings that arrived since the previous run, so nothing is sent twice.

**2. Where the alert goes.** Fill in one or more of these in the same form:

- **Slack** — in Slack: *Apps → Manage → Build (api.slack.com/apps) → Create New App → From scratch → Incoming Webhooks → Activate → Add New Webhook to Workspace → pick the channel*. Copy the URL that starts with `https://hooks.slack.com/services/…` into **`slack_webhook_url`**. The message is a Slack block per job: title as a link, rate or budget, client country, spend, hire rate.
- **Telegram** — message **@BotFather**, `/newbot`, copy the token into **`telegram_bot_token`**. Add the bot to your chat or channel, then get the chat id: message **@userinfobot** for your own id, or for a channel post once and read the id from `https://api.telegram.org/bot<token>/getUpdates` (channel ids start with `-100`). Put it in **`telegram_chat_id`**.
- **Anything else** — put your URL in **`webhook_url`** (n8n, Make, Zapier catch hooks, your own backend). It receives a JSON `POST`: `{ "count": 3, "total": 3, "run_url": "…", "run_id": "…", "dataset_id": "…", "items": [ …up to 100 compact rows: id, title, url, dates, price, category, skills, client spend, hire rate, scores… ] }` — the full rows with descriptions are in the dataset. Extra headers, e.g. `{"Authorization": "Bearer …"}`, go in **`webhook_headers`**.

Your webhook URL and bot token are yours: the run sends to them directly from inside the Actor and they are never stored or seen on our side (the form masks them; our run records keep only "set").

**3. The schedule.** Task → Schedule. The index behind this Actor takes in new postings about every 3 minutes (a posting is in it a median 3 minutes after Upwork shows it, 5 minutes at the 90th percentile — measured 2026-09-24), so a schedule tighter than that sees nothing new and only costs start events. Every 5–15 minutes is the normal setting: an empty run costs nothing and sends nothing. Keep one schedule per `clientId` and filter set — two schedules on the same feed share one cursor.

**What you get per run:** up to 10 jobs in the message (the rest counted, all of them in the dataset), a link to the run. Set **`notify_when_empty: true`** if you want a message on empty runs too. A delivery that fails (a wrong URL, a bot not in the chat) is written into the run's status line and never fails the run — the rows are in the dataset either way.

Ready-made: the published task **[Slack alerts for new Upwork jobs from qualified clients](https://apify.com/hyperbach/upwork-scraper-ai/examples/slack-alerts-for-new-qualified-upwork-jobs)** carries the filters — copy it, paste your Slack URL, schedule it.

Apify's own Slack and Gmail integrations (Task → Integrations) still work; they fire on every run, including empty ones, and send a link rather than the jobs.

Why the alerts are complete: `notifications_only` keeps a cursor on our side per `clientId` and filter set, at the last row you received. A run returns the postings after it, oldest first; when more match than `limit`, the run says so (`has_more`) and the next run continues from the last row, so nothing is skipped and nothing is sent twice. A job that reaches Upwork's search late still arrives on your next run, because the feed is ordered by when a job entered our index, not by when Upwork stamped it.

**The first run of a feed.** Pass `created_at: ">=<a moment>"` with `notifications_only: true` and the first run streams from that moment exactly like every later one: oldest first, `has_more`, the next run continuing. Without `created_at`, the first run is one page of the newest `limit` matches and the older ones are not queued — the run's status line says so. The cursor is keyed on `clientId` plus every filter and its value; `limit` and `fields` are not part of the key, so you can change them freely, and any change to a filter starts a new feed. A `clientId` you do not set derives from your Apify account, so a manual test run with the same filters advances your production cursor — give production its own `clientId`.

### 📤 Output Format

Each dataset item is a flat job record. Real example from a fresh posting (`job_score: 94`):

```json
{
  "id": "022053503680858460814",
  "title": "Senior React Native / Cloud Backend Engineer for iOS Video App",
  "url": "https://www.upwork.com/jobs/~022053503680858460814",
  "description": "We are looking for a senior engineer to join a small, fast-moving team building a live iOS app in React Native with heavy cloud backend connectivity and cloud-based video processing. This is not a web app project. We need someone experienced with real iOS apps on real devices, App Store/TestFlight workflows, mobile-to-cloud video uploads, client/server sync, and production-safe feature deployment...",
  "date_posted": "2026-05-10T15:53:24Z",
  "created_at": "2026-05-10T15:59:01Z",
  "price_type": "Hourly",
  "price_min": 70,
  "price_max": 100,
  "skills": "React Native, iOS, Mobile App Development, Google Cloud Platform, Python, AI Development",
  "category_name": "Web, Mobile & Software Dev",
  "subcategory_name": "Web Development",
  "experience_level": "Expert",
  "engagement_label": "3 to 6 months",
  "qual_pref_english": "ANY",
  "client_location": "United States",
  "buyer_city": "Miami",
  "buyer_score": 4.93,
  "buyer_feedback_count": 44,
  "buyer_payment_verified": true,
  "jobs_posted": 57,
  "hires": 55,
  "hire_rate": 100,
  "total_spent": 54226.41,
  "avg_hourly_rate": 55.35,
  "total_hours": 473,
  "open_jobs": 1,
  "ai_clients_technical_understanding": "High",
  "ai_technical_skills": "React Native, iOS app development, App Store/TestFlight workflows, mobile video/photo upload, camera roll/photo library permissions, cloud backend (GCP preferred, AWS acceptable), async jobs/queues/workers, client/server state sync, production logging/debugging, AI-assisted development",
  "ai_inferred_technical_skills": "Slack, Markdown documentation, real device testing",
  "ai_explicit_mention_of_agency": "No Mention",
  "job_score": 94,
  "job_score_breakdown": {
    "categories": {
      "price":             { "raw_score": 18, "normalized_score": 18, "weight": 20 },
      "client_reputation": { "raw_score": 42, "normalized_score": 42, "weight": 42 },
      "client_spending":   { "raw_score": 24, "normalized_score": 24, "weight": 40 },
      "location":          { "raw_score": 10, "normalized_score": 10, "weight": 10 }
    },
    "top_contributors": [
      { "attribute": "price_min,price_max",    "value": "$70-$100/hr",       "score": 18, "category": "price" },
      { "attribute": "hire_rate",              "value": "100% (57 jobs)",    "score": 15, "category": "client_reputation" },
      { "attribute": "buyer_score",            "value": "4.93 (44 reviews)", "score": 15, "category": "client_reputation" },
      { "attribute": "avg_hourly_rate",        "value": "$55.35/hr",         "score": 15, "category": "client_spending" },
      { "attribute": "buyer_payment_verified", "value": true,                "score": 12, "category": "client_reputation" },
      { "attribute": "client_location",        "value": "United States",     "score": 10, "category": "location" },
      { "attribute": "total_spent",            "value": "$54,226.41",        "score": 6,  "category": "client_spending" },
      { "attribute": "total_hours",            "value": "473 hours",         "score": 3,  "category": "client_spending" }
    ],
    "penalties": []
  },
  "questions": [
    "Describe a React Native iOS app you worked on that was live in TestFlight or the App Store. What parts of the client/server flow did you personally build or maintain, especially around photo/video uploads, auth, sync, or backend job status?",
    "Do you personally have an iPhone with a large real photo/video library that you can use for testing? What model and how many photos/videos in your camera roll?",
    "How are you currently using AI as a developer?",
    "How do you prevent regression?"
  ]
}
```

`job_score` is the rule-based 0–100 rank. Category weights: price (20) + client_reputation (42) + client_spending (40) + premium (5) + location (10). Each category is capped at its weight, then summed and bounded to 0–100.

`job_score_breakdown` is always included and explains which fields contributed how many points. Use it to build UI that highlights why a job ranked high, or to debug your score-threshold filters. `top_contributors` is sorted descending; `penalties` lists negative contributions (e.g., unverified payment, low hire rate on a client with many jobs posted).

`questions` is the array of Upwork screening questions captured for the job — empty `[]` for ~83% of postings, 1–6 strings when the client included them. Feed it into your auto-proposal generator.

Empty/missing AI fields are returned as `null` (not every job goes through full AI enrichment).

For the full list of fields with types and operator support, see [Complete Field Reference](#-complete-field-reference) above.

### 📸 What is captured once, and what is kept current

Everything a client writes into a posting is recorded when the job first enters the index — typically within minutes of going live (median under 4) — and kept exactly as it read then: title, description, skills, screening questions. The client's history (total spent, hire rate, feedback score, jobs posted) is what the job page showed at posting time, the same numbers you would have weighed before bidding.

The fields that move after posting carry the **newest reading we have**: `client_total_applicants`, `client_total_invited` (interviewing), `client_invites_sent`, `client_unanswered_invites`, `client_last_activity`, and the budget — `price`, `price_min`, `price_max`. **`refreshed_at`** says when that reading was taken. When it is `null` we have not re-read the job yet, and those fields are still the values from its first minutes, when proposals are usually around 1.

We re-read every job in the 45-day window on a measured schedule: every 6 hours while it is under two days old, which is when proposals pile up; every 2 days until day 7; once more before day 45. The schedule started on 2026-09-24 and works newest first, so recent jobs are current and older ones are being reached.

`client_invites_sent` is worth reading two ways: its value in the job's first minutes tells you whether the posting started invite-first (`>=1`) or open to everyone (`0`), and `include_history` shows how it grew after.

For every reading we have recorded of a job, add `include_history: true`. To read a job live from Upwork right now, use **Refresh mode** below.

### 🔄 Refresh mode — live "get job by id"

The feed serves an index by design; refresh mode is the deliberate exception. Pass `refresh_job_ids` (up to 50 — job URLs, `~02…` ciphers, or numeric ids) and the run fetches those jobs **live from Upwork, right now**, bypassing the index entirely:

```json
{ "refresh_job_ids": ["https://www.upwork.com/jobs/~021234567890123456789", "021987654321098765432"] }
```

Each returned item is the job's current state: title, description and budget as they read now, and the **live activity counters** — applicants, interviewing, invites sent, unanswered invites, hires, last client activity. This is the data that changes after posting — exactly what the snapshot index deliberately does not carry.

**Two output shapes.** By default (`refresh_shape: "raw"`) each job comes back as its live record, nested — the shape refresh mode has always returned. Set **`refresh_shape: "flat"`** and each job comes back as the **same flat row the feed returns** — the same field names and values (`title`, `price_min`, `total_spent`, `hire_rate`, `skills`, `questions`, …) — plus a `live` block:

```json
"live": { "status": "ACTIVE", "applicants": 19, "interviewing": 8, "invites_sent": 60,
          "unanswered_invites": 51, "hired": 0, "last_client_activity": "2026-09-19T10:00:00Z",
          "fetched_at": "2026-09-19T14:02:11Z" }
```

Use `flat` when you run both modes on the same jobs and want one shape in your pipeline.

Rules: refresh runs cannot be combined with search filters; each refreshed job bills at the normal result rate. A job Upwork no longer shows comes back as a row with its status — `CLOSED`, `FILLED`, `PRIVATE` or `HIDDEN` — from our last reading of it, marked `status_from: "last_reading"` with that reading's time in `refreshed_at` (in the flat shape: `live.status_from` and `live.fetched_at`); it bills like any row. A job Upwork shows to logged-in users only comes back from our search sighting of it, marked `access: "requires_account"` and `status_from: "search_row"`, with that sighting's time in `refreshed_at`; it bills like any row. Ids no source has cost nothing and are listed in the run `OUTPUT` under `refreshSummary.not_found`. When Upwork itself does not answer, we retry for up to 80 seconds; ids still unread after that cost nothing, are listed under `refreshSummary.failed` with the reason, and the run ends `PARTIAL` (or failed, when nothing was read) so you can run them again. A run of 50 ids takes roughly a minute.

**Timeouts.** A refresh reads Upwork live, ten ids per call. One call usually takes 4–15 seconds, and up to about 40 seconds when many refreshes arrive at the same moment. Give a refresh run a timeout of **at least 120 seconds for up to 10 ids**, and about 300 seconds for 50; the default (one hour) is always enough. When a run gets close to its timeout, it stops early and keeps the jobs it already returned — you pay for those only — and its status message says to raise the timeout to get the rest. A run needs about 3 seconds just to start, so under roughly 15 seconds there is no time to fetch anything. Feed checks are different: they answer from the index in a second or two, so a short timeout such as 60 seconds is fine for them.

### 💰 Pricing

**$2.00 per 1,000 results.** No monthly subscription. No platform usage fees. Runs that return zero results cost nothing.

Paid Apify plans pay less, automatically: **$1.70** per 1,000 on Starter, **$1.40** on Scale, **$1.00** on Business.

**`limit` goes up to 10,000 and the default is 50.** A full 10,000-row run is $20 at list price. For a one-time backfill that is the point — the published task [Backfill 90 days of Upwork jobs once, then poll](https://apify.com/hyperbach/upwork-scraper-ai/examples/backfill-90-days-of-upwork-jobs-then-poll) does exactly that. For a schedule it is not: set `notifications_only: true` and each new posting is billed once.

### 📜 Terms of use and data provenance

**What a row is.** One public Upwork job posting as Upwork shows it to a visitor: the posting's own fields (title, description, budget or rate, skills, category, posting time) and the client-level figures Upwork prints on it (payment verified, total spent, hires, location, reviews). No freelancer profiles, proposals, messages or account data. Descriptions are the clients' own text.

**How it is collected.** Every posting entered the index as a public listing, visible on Upwork without an account. We collect without an Upwork account and never with anyone's credentials: the Actor does not ask for yours and never acts on Upwork for you. Upwork does not license its listings to third parties and we do not claim an Upwork licence. We do not give legal advice and do not warrant that a given use is lawful where you are.

**Your licence to the output.** Under the [Apify Actor Terms](https://docs.apify.com/legal/actor-terms-and-conditions) you keep the intellectual-property rights in the output (§3.2) and it is your Customer Data (§4.3). As the creator (§2.2) we grant you a non-exclusive, perpetual licence to use the rows for your business: alerts, matching, analysis, enrichment, and products built on them, including a job board that shows postings with a link to Upwork. You may not resell or redistribute the rows themselves as a dataset or a feed, and you may not use them to contact people at scale. Compliance with the laws that apply to your use is yours.

Questions about these terms: apify@hyperbach.com.

***

### 📖 Reference — Fields, Filters & Syntax

Everything below is the complete manual. Skim it when you're ready to build precise queries.

### ⚙️ Input Configuration

*All parameters are optional.*

| Parameter | Type | Description |
|-----------|------|-------------|
| `limit` | number | Number of jobs to return (1-10000, default: 50) |
| `notifications_only` | boolean | If true, only returns NEW jobs since last call |
| `refresh_shape` | `raw` / `flat` | Refresh mode only: the live record as it comes (default), or the feed's flat row plus a `live` block — see *Refresh mode* |
| `slack_webhook_url`, `telegram_bot_token` + `telegram_chat_id`, `webhook_url` (+ `webhook_headers`) | string | Where to send one message per run with the new jobs — see *Job alerts*. `notify_when_empty` (boolean) also sends when nothing was found |

**One run returns up to 10,000 rows.** `limit` sets how many, from 1 to 10,000,
and defaults to 50. Rows come back in the order they entered our index, newest
first — not by Upwork's own posting time, which is why `date_posted` on
consecutive rows does not decrease in step.

**The searchable window is the last 90 days.** A filtered search reads the most
recent 90 days of the index and stops there. Ask for 10,000 and you get 10,000
only if that many postings match inside the window; otherwise you get every
match there is. Nothing is held back — the window simply ends. Older history is
served by the published dataset, not by this live feed.

**On a schedule, set `notifications_only: true`.** The run remembers the newest posting it delivered, per filter set, and the next run starts there, so every posting is billed once and no code holds a cursor. A time-window filter polled often does not do this: `date_posted: ">=20 minutes ago"` every 4 minutes returns each posting about five times and bills it five times. Use `created_at` when you want to hold the start point yourself — pass `>` the newest `created_at` you received, not a clock.

**How you get more depends on the mode.** With `notifications_only: true` the
run remembers where it stopped, so each run returns only what arrived since the
previous one, oldest first, and a schedule walks forward through time on its own;
`has_more: true` there means the next run continues from the last row, nothing is
skipped. The first run of a feed follows the same rule when you pass a
`created_at` start point; without one it is a single page of the newest `limit`
matches, and older ones are not queued. With `notifications_only: false` each run
returns the newest matches in the window; a `has_more: true` in the output means
more postings match than your `limit` asked for. A repeat run returns the same newest
page; to go further back, pass the run's `nextPage` (in OUTPUT and on the status line)
as `page_after` with the same filters. Each page continues below the last row of the
one before, with no repeats and no gaps, until `nextPage` is absent — the whole 90-day
window, one page at a time.

**Order.** A full page is newest first by `created_at`, then by the job's internal id, so
two jobs never tie. Filters that read a live value — status, applicant and invite counts,
budget — match today's reading, so a job can enter or leave such a filter between two
runs; text matches (`keywords` in exact mode, `title`, `description`) do not change after a
job is indexed. Keywords matched by meaning can; set `keywords_match: "strict"` when you
need a walk you can replay.

**Builds.** Every build of this Actor asks the same live backend, so pinning an old build
does not freeze what a query returns; it only keeps the old inputs and OUTPUT. `page_after`
and `nextPage` are in builds from 2026-09-26 on (the current one has them). Run the `latest` tag unless you have a reason
not to. In OUTPUT, `is_notification_mode` is the mode the run used: `true` for
`notifications_only: true`, `false` for a full page. Before 2026-10-02 it was always `true`.

**A run never asks for more rows than your `maxTotalChargeUsd` can pay for.** The
page is sized to the cap before the feed moves its cursor, so a cap can shorten a
run but never lose a row; the status line says when it did, and the next run
continues.

### ⚠️ Filter Quality — Read This Before Filtering

Upwork only **requires** title, description, skills, and price-type from the
client. Almost everything else is optional, and clients fill it in
inconsistently. **If you filter on a field that's empty for most jobs, you
exclude all the matching jobs that simply didn't supply the value** — not
just the non-matching ones.

Field density measured on the last 30 days (~119K postings, 2026-09-17):

- **Always-filled (safe to filter):** `title`, `description`, `skills`,
  `price_type`, `category_name`, `subcategory_name`, `experience_level`,
  `qual_pref_english`, `qual_type`, `client_location`, `hire_rate`,
  `company_size`, `open_jobs`, `ai_explicit_mention_of_agency` (all ≥96%)
- **Mostly filled (mild loss):** `buyer_contract_date` (~82%),
  `buyer_payment_verified` (~80%), `ai_technical_skills` (~79%)
- **Half-filled (use carefully):** `price` / `price_min` / `price_max`
  (fixed-price jobs leave hourly bounds null and vice versa),
  `total_spent` (~60%), `buyer_score` (~57%), `buyer_feedback_count`, `industry`,
  `engagement_label`, `ai_duration` (~41%)
- **Sparse (filter at your own risk):** `ai_urgency` (~18%),
  `ai_deadline` (~16%), `qual_rising_talent` (~5%), `qual_portfolio_required` (~0%)
  — most jobs are *not* tagged

**Recommendation:** filter on dense fields, then use sparse fields for
post-fetch sorting / scoring / display. Combining sparse filters compounds
the loss multiplicatively.

### 📊 Available Filter Fields

You can filter jobs by **61 different fields** across these categories:

#### 📋 Essential Job Information

Core job details like title, description, pricing

**Available fields:** `title`, `description`, `skills`, `price_type`, `price`, `price_min`, `price_max`, `category_name`, `subcategory_name`, `date_posted`, `url`, `keywords`, `exclude_keywords`

#### 📋 Job Requirements

Experience level, qualifications, and constraints

**Available fields:** `experience_level`, `engagement_label`, `engagement_weeks`, `qual_min_hours_week`, `qual_min_success_score`, `qual_pref_english`, `qual_rising_talent`, `qual_portfolio_required`, `qual_type`

#### 📋 Client Information

Details about the client posting the job

**Available fields:** `client_location`, `jobs_posted`, `hire_rate`, `avg_hourly_rate`, `open_jobs`, `total_spent`, `hires`, `active_hires`, `total_hours`, `industry`, `company_size`, `buyer_city`, `buyer_feedback_count`, `buyer_score`, `buyer_contract_date`, `buyer_payment_verified`, `client_total_applicants`, `client_total_invited`, `client_unanswered_invites`, `client_positions_to_hire`, `client_last_activity`, `qual_countries`

#### 📋 AI-Powered Insights

AI-generated analysis of job requirements and urgency

**Available fields:** `ai_urgency`, `ai_duration`, `ai_deadline`, `ai_technical_skills`, `ai_inferred_technical_skills`, `ai_explicit_mention_of_agency`, `ai_clients_technical_understanding`

#### 📋 System Metadata

Internal tracking and system fields

**Available fields:** `id`, `created_at`, `questions`, `job_score`, `job_score_breakdown`

#### 🔥 Most Popular Filters

Measured on the live feed: share of requests that carry each filter.

| Field | Type | Example | Used in | Description |
|-------|------|---------|---------|-------------|
| `buyer_payment_verified` | boolean | true | 78% | Boolean flag indicating whether the client has verified their payment method on Upwork |
| `keywords` | text | "chatbot" | 75% | Search the job's title, description, skills, categories and AI-generated fields |
| `total_spent` | numeric | ">=1000" | 70% | Total amount the client has spent on Upwork across all their projects |
| `created_at` | date | ">=2026-09-01T00:00:00Z" | 59% | When this job entered our index — a start point you own, including jobs Upwork listed late |
| `date_posted` | date | ">=2026-09-01" | 35% | When the job was posted on Upwork |
| `category_name` | select | "Web, Mobile & Software Dev" | 25% | Upwork's top-level category of the job |
| `exclude_keywords` | text | "wordpress shopify" | 19% | Drop jobs containing any of these words |
| `buyer_score` | numeric | ">=3" | 19% | Score of the client's performance on Upwork |
| `job_score` | numeric | ">=40" | 4% | Rule-based 0–100 score that ranks the job by attractiveness |
| `subcategory_name` | select | "AI Apps & Integration" | 2% | Upwork's subcategory, one level under category_name |

> **Keywords match by meaning.** Describe the work in plain words — `website migration shopify webflow` — and a new search returns every job with all those words **plus** the jobs about the same thing in other words, each with a `relevance` score (0-1). A search you already ran keeps matching exactly as before. For an exact word match, write the syntax — `react OR vue`, `"cold email"`, `-wordpress` — or set `keywords_match: "strict"`. The run's status message says which mode ran. `exclude_keywords` drops a job if **any** listed word appears.

> **Words, like Upwork's advanced search:** `all_words`, `any_words`, `none_words`, `exact_phrase`, `title_search`, `skills_search`. Each is a list — one word or phrase per entry — and matches the exact words. They combine with each other and with `keywords`: `{"keywords": "ai voice agent", "any_words": ["vapi", "retell"], "none_words": ["wordpress"]}`.

> **Tip — filter server-side, not after the fact:** every filter below is applied before `limit` picks the page, so narrowing costs you nothing and rows you would have discarded never enter your dataset. Comparison and range syntax works on any numeric field — `"total_spent": ">=10000"`, `"price": "500-2000"` — and `date_posted` is publication time, so `">=10 minutes ago"` means what it says.

### 🎛️ Filter Syntax Guide

Different field types support different filtering options:

#### **📝 Text Fields** (`skills`, `title`, `description`)

- **Contains search**: `"React"` → finds jobs mentioning React
- **Several words are one phrase**: `"product animation"` → that exact phrase, in order. To describe the work in your own words, use `keywords` (matched by meaning); for exact rules — words in any order, alternatives, exclusions — use `all_words`, `any_words`, `none_words`
- **Case-insensitive**: automatically handled
- **Empty check**: `"=null"` → finds jobs with empty/null values
- **Non-empty check**: `"!=null"` → finds jobs with content

#### **🔢 Numeric Fields** (`price`, `price_min`, `buyer_score`)

- **At least**: `"1000"` → 1000 or more (a bare number is a minimum — what a filter means)
- **Exactly**: `"=1000"`
- **Operators**: `">=50"`, `"<=100"`, `">25"`, `"<75"`
- **Ranges**: `"500-2000"` → between 500 and 2000 (inclusive)
- **Upwork's own spellings work too**: `"10+"` = `">=10"`, `"over 1,000"`, `"under 500"`, `"$5,000"`

#### **✅ Boolean Fields** (`buyer_payment_verified`, `qual_rising_talent`, `qual_portfolio_required`)

- **True**: `"true"` or `"1"`
- **False**: `"false"` or `"0"`

#### **📅 Date Fields** (`date_posted`, `created_at`)

- **Relative age (easiest)**: `"7d"` = last 7 days, `"24h"` = last day, `"2w"` = last two weeks. A bare number means days: `"3"` = last 3 days.
- **In words**: `"today"`, `"yesterday"`, `"past 24 hours"`, `"this week"`, `"last 30 days"`, `"3 days ago"` — Upwork's filter labels as written
- **Two-digit dates**: `09/20/2026` is read (20 cannot be a month); `09/10/2026` is refused — month-day or day-month? Write `2026-09-10`
- **Exact date**: `"2025-01-15"`
- **Operators**: `">=2025-01-01"`, `"<=2025-12-31"` — also work on relative ages (`"<=7d"` = older than a week)
- **ISO format**: `"2025-01-15T10:30:00Z"`

#### **🎯 Select Fields** (`experience_level`, `ai_urgency`)

- **Exact match**: `"Intermediate"`, `"Urgent"`
- **Case-sensitive**: use exact values from field options (e.g. `"Urgent"`, not `"urgent"` or `"high"`)

### 🗂️ Categories and subcategories

`category_name`, `subcategory_name` and `industry` take these exact values, and nothing else. The counts are jobs in the feed over the 90 days to 2026-09-25, most first. To get a whole field of work, set `category_name` (for example `Web, Mobile & Software Dev` for software development). It is exact and does not depend on the words in a posting. Use `keywords` for the task inside it.

| `category_name` | Jobs | `subcategory_name` values under it |
|---|---:|---|
| `Design & Creative` | 103,156 | `Video & Animation` (45,102), `Graphic, Editorial & Presentation Design` (30,152), `Branding & Logo Design` (8,004), `Performing Arts` (6,443), `Art & Illustration` (5,652), `Product Design` (3,590), `Audio & Music Production` (2,010), `Photography` (1,886), `NFT, AR/VR & Game Art` (317) |
| `Sales & Marketing` | 79,466 | `Digital Marketing` (46,397), `Lead Generation & Telemarketing` (24,339), `Marketing, PR & Brand Strategy` (8,730) |
| `Web, Mobile & Software Dev` | 66,449 | `Web Development` (22,631), `Web & Mobile Design` (17,718), `Ecommerce Development` (9,037), `Mobile Development` (5,349), `Scripts & Utilities` (3,746), `QA Testing` (3,090), `AI Apps & Integration` (1,639), `Game Design & Development` (979), `Other - Software Development` (903), `Product Management & Scrum` (574), `Desktop Application Development` (513), `Blockchain, NFT & Cryptocurrency` (270) |
| `Admin Support` | 32,117 | `Virtual Assistance` (18,269), `Data Entry & Transcription Services` (5,258), `Market Research & Product Reviews` (5,222), `Project Management` (3,368) |
| `Engineering & Architecture` | 18,912 | `3D Modeling & CAD` (5,723), `Civil & Structural Engineering` (2,727), `Building & Landscape Architecture` (2,623), `Contract Manufacturing` (2,163), `Electrical & Electronic Engineering` (2,029), `Energy & Mechanical Engineering` (1,506), `Interior & Trade Show Design` (1,289), `Physical Sciences` (745), `Chemical Engineering` (107) |
| `Accounting & Consulting` | 17,293 | `Accounting & Bookkeeping` (7,325), `Recruiting & Human Resources` (4,091), `Management Consulting & Analysis` (2,021), `Financial Planning` (1,755), `Other - Accounting & Consulting` (1,493), `Personal & Professional Coaching` (608) |
| `Writing` | 12,276 | `Content Writing` (5,975), `Editing & Proofreading Services` (3,187), `Professional & Business Writing` (1,864), `Sales & Marketing Copywriting` (1,250) |
| `Data Science & Analytics` | 9,190 | `AI & Machine Learning` (4,291), `Data Analysis & Testing` (2,792), `Data Extraction/ETL` (1,266), `Data Mining & Management` (841) |
| `Customer Service` | 7,557 | `Customer Service & Tech Support` (6,746), `Community Management & Tagging` (811) |
| `IT & Networking` | 6,809 | `Network & System Administration` (2,127), `DevOps & Solution Architecture` (2,003), `Information Security & Compliance` (1,650), `ERP/CRM Software` (834), `Database Management & Administration` (195) |
| `Translation` | 6,718 | `Translation & Localization Services` (5,658), `Language Tutoring & Interpretation` (1,060) |
| `Legal` | 6,524 | `Corporate & Contract Law` (5,431), `Public Law` (493), `International & Immigration Law` (382), `Finance & Tax Law` (218) |

**`industry`** (the client's industry, 29 values): `Tech & IT` (19,339), `Sales & Marketing` (13,387), `Media & Entertainment` (7,562), `Education` (5,852), `Health & Fitness` (5,000), `Retail & Consumer Goods` (4,812), `Art & Design` (4,434), `HR & Business Services` (3,984), `Fashion & Beauty` (3,945), `Real Estate` (3,828), `Finance & Accounting` (3,455), `Manufacturing & Construction` (2,682), `Engineering & Architecture` (2,363), `Food & Beverage` (1,882), `Travel & Hospitality` (1,580), `Automotive` (1,316), `Legal` (1,253), `Energy & Utilities` (915), `Sports & Recreation` (911), `Science & Medicine` (718), `Nonprofit` (519), `Transportation & Warehousing` (487), `Supply Chain & Logistics` (387), `Agriculture & Forestry` (373), `Government & Public Sector` (370), `Aerospace` (185), `Aviation` (98), `Mining` (63), `Military & Defense` (43).

#### 📋 Complete Field Reference

##### Essential Job Information

| Field | Type | Operators | Examples | Description |
|-------|------|-----------|----------|-------------|
| `title` | text | contains, =null, !=null | `Build a React Dashboard with Real-time Analytics`, `Virtual Assistant for Email Management`, `Logo Design for Tech Startup` | The job posting title as written by the client. Contains the main description of what work needs to be done. |
| `description` | text | contains, =null, !=null | `We need an experienced React developer to build a ...`, `Looking for a Python expert who can integrate machine learning models ...` | Full job description text as written by the client. Contains detailed requirements, expectations, and project scope. |
| `skills` | text | contains, =null, !=null | `JavaScript, React, Node.js`, `Python, Machine Learning, TensorFlow` | Comma-separated list of required skills and technologies for the job as specified by the client. |
| `price_type` | select | equals | `Fixed-price`, `Hourly` | How the job is priced: Fixed Price (one-time payment) or Hourly (paid per hour worked). |
| `price` | numeric | >=, <=, >, <, =, ranges | `500`, `1200`, `50` | The budget amount for the fixed-price job. |
| `price_min` | numeric | >=, <=, >, <, =, ranges | `25`, `50`, `100` | Lower end of the job's hourly rate band (Upwork jobs advertise a range like $10-35/hr). Hourly jobs only - fixed-price jobs carry their budget in `price` and leave this empty. |
| `price_max` | numeric | >=, <=, >, <, =, ranges | `75`, `150`, `500` | Upper end of the job's hourly rate band (Upwork jobs advertise a range like $10-35/hr). Hourly jobs only - fixed-price jobs carry their budget in `price` and leave this empty. |
| `category_name` | select | equals | `Accounting & Consulting`, `Admin Support`, `Customer Service`, `Data Science & Analytics`, `Design & Creative`, +7 more | Upwork's top-level category of the job. The value must match exactly. One of 12, most jobs first: "Design & Creative", "Sales & Marketing", "Web, Mobile & Software Dev", "Admin Support", "Engineering & Architecture", "Accounting & Consulting", "Writing", "Data Science & Analytics", "Customer Service", "IT & Networking", "Translation", "Legal". |
| `subcategory_name` | select | equals | `3D Modeling & CAD`, `Accounting & Bookkeeping`, `AI & Machine Learning`, `AI Apps & Integration`, `Art & Illustration`, +59 more | Upwork's subcategory, one level under category_name. The value must match exactly. Most common: "Digital Marketing", "Video & Animation", "Graphic, Editorial & Presentation Design", "Lead Generation & Telemarketing", "Web Development", "Virtual Assistance". All 64 values, grouped under their category, are in the README section "Categories and subcategories". |
| `date_posted` | date | >=, <=, >, <, =, equals | `7d`, `24h`, `>=2026-05-01`, `2026-05-17` | When the job was posted on Upwork. Use this to find recent opportunities or analyze posting patterns. |
| `url` | text | contains, =null, !=null | `https://www.upwork.com/jobs/~01234567890abcdef`, `https://www.upwork.com/jobs/~987654321fedcba09` | Direct link to the job posting on Upwork. Use this to view the full job details or apply. |
| `keywords` | text | contains | `python machine learning`, `react javascript typescript`, `design ui ux figma`, `marketing seo content`, `data analysis sql python` | Search the job's title, description, skills, categories and AI-generated fields. A new search matches by meaning: every job with all your words, plus jobs about the same thing in other words (see keywords_match). `a OR b`, "quotes" and -word ask for an exact word match: bare words must all appear, OR separates alternatives, quotes keep a phrase, a leading minus drops a word. For a whole field of work, set category_name instead: it is exact (e.g. "Web, Mobile & Software Dev"). |
| `exclude_keywords` | text | contains | `wordpress php`, `data entry copy paste`, `logo design graphic`, `social media marketing`, `excel spreadsheet manual` | Drop jobs containing any of these words. Quotes keep a phrase together (`"data entry" wordpress` drops both), and you can write OR yourself. Searches the same text fields as keywords. |

##### Job Requirements

| Field | Type | Operators | Examples | Description |
|-------|------|-----------|----------|-------------|
| `experience_level` | select | equals | `Entry_level`, `Expert`, `Intermediate` | Required experience level for the job. Exact values: Entry_level, Intermediate, Expert. |
| `engagement_label` | select | equals | `1 to 3 months`, `3 to 6 months`, `Less than 1 month`, `More than 6 months` | Expected duration or type of engagement (e.g., 1 to 3 months, 3 to 6 months, Less than 1 month, Less than 1 week, More than 6 months). |
| `engagement_weeks` | select | equals | `3`, `9`, `18`, `52` | Project duration in weeks as Upwork encodes it. Values seen: 3, 9, 18, 52. |
| `qual_min_hours_week` | select | equals | `0`, `10`, `30`, `40` | Minimum hours per week the client requires for hourly jobs. Values seen: 0 (none stated), 10, 30, 40. |
| `qual_min_success_score` | select | equals | `0`, `80`, `90` | Minimum Upwork success score required to apply for the job. |
| `qual_pref_english` | select | equals | `ANY`, `CONVERSATIONAL`, `FLUENT`, `NATIVE` | Client's preferred English proficiency level for freelancers. |
| `qual_rising_talent` | boolean | equals | `True`, `False` | Whether the job is open to Upwork Rising Talent (newer freelancers with potential). |
| `qual_portfolio_required` | boolean | equals | `True`, `False` | Whether the client requires a portfolio or work samples to apply. |
| `qual_type` | select | equals | `AGENCY`, `ANY`, `INDEPENDENT` | Type of freelancer the client is looking for: Agency (team/company), Independent (solo freelancer), or Any (no preference). |

##### Client Information

| Field | Type | Operators | Examples | Description |
|-------|------|-----------|----------|-------------|
| `client_location` | text | contains, =null, !=null | `United States`, `United Kingdom`, `Canada` | Geographic location of the client posting the job. |
| `jobs_posted` | numeric | >=, <=, >, <, =, ranges | `1`, `10`, `>50` | How many jobs this client has posted on Upwork over their whole account — a count, not a date. "10" means 10 or more; use date_posted or created_at for a time window. |
| `hire_rate` | numeric | >=, <=, >, <, =, ranges | `75`, `90`, `50` | Upwork's own hire rate for the client: the share of their posted jobs that led to a hire, 0-100. It is not hires divided by jobs posted (a job can hire several people). High values mean a client who hires rather than browses. |
| `avg_hourly_rate` | numeric | >=, <=, >, <, =, ranges | `45.50`, `75.00`, `>=100` | Average hourly rate this client typically pays freelancers. Based on their historical hiring patterns. |
| `open_jobs` | numeric | >=, <=, >, <, =, ranges | `0`, `2`, `<5` | Number of jobs the client currently has open/active. |
| `total_spent` | numeric | >=, <=, >, <, =, ranges | `100`, `500`, `>=1000000` | Total amount the client has spent on Upwork across all their projects. |
| `hires` | numeric | >=, <=, >, <, =, ranges | `1`, `10`, `>50` | Total number of freelancers hired by the client. |
| `active_hires` | numeric | >=, <=, >, <, =, ranges | `1`, `10`, `>50` | Number of freelancers currently hired by the client. |
| `total_hours` | numeric | >=, <=, >, <, =, ranges | `100`, `500`, `>=1000` | Total number of hours the client has worked on Upwork across all their projects. |
| `industry` | select | equals | `Aerospace`, `Agriculture & Forestry`, `Art & Design`, `Automotive`, `Aviation`, +24 more | The client's industry, as the client set it on Upwork. The value must match exactly. Most common: "Tech & IT", "Sales & Marketing", "Media & Entertainment", "Education", "Health & Fitness", "Retail & Consumer Goods". All 29 values in the last 90 days are in the list. |
| `company_size` | select | equals | `0`, `1`, `10`, `100`, `1000`, +4 more | Size of the client's company. Predefined option for company size. Select from available choices. |
| `buyer_city` | text | contains, =null, !=null | `New York`, `London`, `Paris`, `Tokyo`, `Sydney`, `Berlin`, `Rome`, `Madrid`, `Amsterdam`, `Mumbai`, `Beijing`, `Delhi` | City where the client is located. |
| `buyer_feedback_count` | numeric | >=, <=, >, <, =, ranges | `1`, `10`, `>=2` | Number of feedbacks the client has received. |
| `buyer_score` | numeric | >=, <=, >, <, =, ranges | `>=4.7`, `5.0` | Score of the client's performance on Upwork. |
| `buyer_contract_date` | date | >=, <=, >, <, =, equals | `>=2024-01-15`, `2024-01-20T14:30:00Z`, `365d` | Date when the client first registered their account on Upwork. Indicates how long the client has been active on the platform. |
| `buyer_payment_verified` | boolean | equals | `True`, `False` | Boolean flag indicating whether the client has verified their payment method on Upwork. |
| `client_total_applicants` | numeric | (read-only, not a filter input) | - | Proposals the job has received. The newest reading we have — `refreshed_at` says when; while it is null this is still the value from the job's first minutes. |
| `client_total_invited` | numeric | (read-only, not a filter input) | - | Freelancers the client has invited to interview. The newest reading we have — `refreshed_at` says when; while it is null this is still the value from the job's first minutes. |
| `client_unanswered_invites` | numeric | (read-only, not a filter input) | - | Invites the client has sent that are still unanswered. The newest reading we have — `refreshed_at` says when; while it is null this is still the value from the job's first minutes. |
| `client_positions_to_hire` | numeric | (read-only, not a filter input) | - | How many freelancers the client intends to hire for this job (1 for most postings). |
| `client_last_activity` | date | (read-only, not a filter input) | - | When the client last acted on this job (viewed proposals, sent invites). The newest reading we have — `refreshed_at` says when; while it is null this is still the value from the job's first minutes. |
| `qual_countries` | array | (read-only, not a filter input) | - | Countries the client restricted applicants to, as listed on the job; empty when the job is open worldwide (85% of postings). |

##### AI-Powered Insights

| Field | Type | Operators | Examples | Description |
|-------|------|-----------|----------|-------------|
| `ai_urgency` | select | equals | `Immediate`, `Long-Term`, `Moderately Urgent`, `Not Urgent`, `Urgent`, `Very Urgent` | AI-detected urgency level of the job based on language and posting patterns. |
| `ai_duration` | select | equals | `Flexible`, `Flexible Deadline`, `Long-Term`, `Mid-Term`, `Part-Time`, `Short-Term` | AI-detected project duration based on job description analysis. Indicates expected length and type of engagement. |
| `ai_deadline` | select | equals | `Fixed Deadline`, `Flexible Deadline`, `Immediate Deadline`, `No Deadline` | AI-detected deadline type for the job based on urgency indicators and time-sensitive language in the job description. |
| `ai_technical_skills` | text | contains, =null, !=null | `JavaScript, React, Node.js`, `Python, Django, PostgreSQL`, `AWS, Docker, Kubernetes` | Technical skills explicitly mentioned in the job description, extracted using AI. These are skills directly stated by the client as requirements or preferences. |
| `ai_inferred_technical_skills` | text | contains, =null, !=null | `Git, REST APIs, Testing`, `Database Design, Security`, `Responsive Design, SEO` | Technical skills inferred by AI from the job description context, even when not explicitly mentioned. These are skills likely needed based on project requirements and industry patterns. |
| `ai_explicit_mention_of_agency` | select | equals | `Agencies Welcome`, `No Agencies`, `No Mention` | AI-detected explicit mention of agency preferences in the job posting. Indicates whether the client welcomes agencies, prefers individual freelancers, or has no specific preference. |
| `ai_clients_technical_understanding` | select | equals | `Expert`, `High`, `Low`, `Moderate` | AI-detected assessment of the client's technical understanding based on how they describe their project requirements. Helps identify whether the client has deep technical knowledge, moderate understanding, basic knowledge, or expert-level expertise in the domain. |

##### System Metadata

| Field | Type | Operators | Examples | Description |
|-------|------|-----------|----------|-------------|
| `id` | text | contains, =null, !=null | `022053503680858460814`, `021987654321098765432` | Unique identifier for the job posting on Upwork. Use this to track specific jobs or avoid duplicates. |
| `created_at` | date | >=, <=, >, <, =, equals | `2024-01-20T14:30:00Z` | When this job entered our index — a start point you own, including jobs Upwork listed late. Replayable and shareable, unlike a vendor-held 'since last run' state. A bound reads the whole millisecond it names: > the newest created_at you received returns exactly the jobs added since, and <= a row's created_at includes that row. notifications_only: true and page_after also walk without repeats or gaps. |
| `questions` | array | (read-only, not a filter input) | `['Name one long-tail keyword you would target for a UK mortgage and investment app and explain why?', 'Share a specific SEO or ASO result you have driven. Include the numbers?']`, `['Are you willing to undergo a background check, in accordance with local law/regulations?', 'How soon can be available for work?', 'What is your level of proficiency in English?', 'Briefly describe your experience with Oracle Fusion HCM']` | Upwork screening questions captured for the job, as written by the client. Empty array `[]` for ~83% of postings (jobs without screening questions). When present, contains 1-6 question strings. Feed straight into a proposal-answer generator to pre-write answers, or surface as an application checklist. |
| `job_score` | numeric | >=, <=, >, <, =, ranges | `>=50`, `>=70`, `60-90` | Rule-based 0–100 score that ranks the job by attractiveness. Weighted across price (20), client reputation (42), client spending (40), premium status (5), and location (10). Because 82 of those 100 points come from client reputation and spending, a threshold mostly selects for good clients rather than good briefs. |
| `job_score_breakdown` | object | (read-only, not a filter input) | - | Always-included explanation of the `job_score`: per-category raw/normalized/weight, ranked `top_contributors`, and `penalties`. Use to surface why a job ranked high in UI, or to debug threshold filters. See Output Format below. |

### 🎯 Smart Usage Patterns

#### 💼 Freelancer Job Alerts

```javascript
// High-value React jobs from quality clients
{
  "notifications_only": true,
  "skills": "React",
  "price_min": ">=60",
  "buyer_score": ">=4.0",
  "buyer_payment_verified": true,
  "ai_urgency": "Urgent"
}

// Remote-friendly design jobs
{
  "keywords": "design ui ux figma",
  "price_type": "Fixed-price",
  "price": "500-5000"
}
```

#### 🏢 Agency Lead Generation

```javascript
// Jobs explicitly welcoming agencies or teams
{
  "ai_explicit_mention_of_agency": "Agencies Welcome",
  "price_min": ">=100",
  "total_spent": ">=10000"
}

// Enterprise projects with team requirements
{
  "ai_explicit_mention_of_agency": "Agencies Welcome",
  "engagement_label": "More than 6 months",
  "company_size": "1000",
  "ai_clients_technical_understanding": "High"
}

// High-value development projects for agencies
{
  "ai_explicit_mention_of_agency": "Agencies Welcome",
  "keywords": "react node.js typescript",
  "price": ">=25000",
  "ai_duration": "Long-Term",
  "buyer_score": ">=4.5"
}
```

#### 🔍 Market Research & Analysis

```javascript
// Track AI/ML job trends and pricing
{
  "keywords": "machine learning artificial intelligence",
  "date_posted": ">=2025-01-01",
  "limit": 100
}

// Monitor mobile app development market
{
  "skills": "React Native",
  "price_type": "Fixed-price",
  "date_posted": ">=2024-12-01",
  "experience_level": "Intermediate"
}

// Analyze client spending patterns
{
  "total_spent": ">=100000",
  "hire_rate": ">=90",
  "buyer_payment_verified": true,
  "limit": 50
}
```

#### 🎯 Specialized Niches

```javascript
// Blockchain & crypto projects with quality clients
{
  "keywords": "blockchain cryptocurrency solidity ethereum",
  "exclude_keywords": "scam pyramid scheme",
  "ai_clients_technical_understanding": "High",
  "price_min": ">=50"
}

// Technical writing for clients who understand the work
{
  "keywords": "technical writing documentation",
  "ai_clients_technical_understanding": "Moderate",
  "experience_level": "Intermediate",
  "price_min": ">=30",
  "qual_pref_english": "FLUENT"
}

// Urgent fixes for immediate delivery
{
  "ai_urgency": "Immediate",
  "ai_deadline": "Fixed Deadline",
  "keywords": "bug fix maintenance urgent",
  "ai_duration": "Short-Term",
  "price_min": ">=40",
  "date_posted": ">=2025-01-01"
}
```

#### 🚀 Notification & Automation

```javascript
// Daily new job alerts for Python developers
{
  "notifications_only": true,
  "skills": "Python",
  "price_min": ">=45",
  "buyer_score": ">=3.5",
  "limit": 20
}

// Weekend side project hunting
{
  "engagement_label": "Less than 1 month",
  "price_type": "Fixed-price",
  "price": "1000-10000"
}
```

### ⚡ Performance

- **Typical run:** ~8 seconds end to end for 100 results. Most of that is Apify container start-up — the query behind it returns in well under a second.
- **Jobs are served from a pre-indexed store, not scraped live per run.** That's why a 100-result run takes seconds rather than minutes, and why filters run against the whole corpus instead of one page of search results. The posting is indexed once; its moving counts and budget are re-read on a schedule and served from the same store — see "What is captured once, and what is kept current" above.

### Errors

**A run does not fail because of your input** (builds from 2026-10-03 on). When an input cannot be used — a date that is not a date, a malformed `page_after`, two settings that contradict each other — the run ends **Succeeded**, bills no results, and says what to change:

- the status message starts with `INPUT REJECTED`;
- the dataset stays empty: a row would be billed as a result, so none is written;
- the key-value store record `ERROR` holds `{"error": true, "code": "…", "message": "…"}`.

From code, read the `ERROR` record when the dataset comes back empty. Only the start fee ($0.00001) applies. A search that matches nothing is not an error: the dataset is empty and no results are billed. A run that ends **Failed** is a fault on our side, never your input; it bills no results, and we are alerted. An older pinned build still ends a rejected input **Failed**, with the same message.

### 🚨 Common Issues & Solutions

#### Issue: "No jobs returned"

- **Solution**: Check your filters aren't too restrictive
- **Tip**: Start with broader filters and narrow down

#### Issue: "Same jobs appearing"

- **Solution**: Use `notifications_only: true` for new jobs only

- **Tip**: Cursor tracking is automatic per user session

- **Tip**: Use higher `limit` values to reduce request frequency

### 🔗 Integration Examples

#### Webhook Integration

```javascript
// Set up webhook to receive new job notifications
const webhook = await apifyClient.webhooks().create({
  eventTypes: ['ACTOR.RUN.SUCCEEDED'],
  requestUrl: 'https://your-app.com/webhook/new-jobs'
});
```

#### Slack Bot Integration

```javascript
// Post new jobs to Slack channel
const run = await client.actor('hyperbach/upwork-scraper-ai').call({
  notifications_only: true,
  skills: 'React',
  price_min: '>=50'
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();

items.forEach(job => {
  slack.postMessage({
    channel: '#job-alerts',
    text: `New ${job.skills} job: ${job.title} - $${job.price_min}+/hr`
  });
});

jobs.items.forEach(job => {
  slack.postMessage({
    channel: '#job-alerts',
    text: `🚀 New ${job.skills} job: ${job.title} - $${job.price_min}+/hr`
  });
});
```

### 📊 Need historical data?

For research, market sizing or model training, the live scraper isn't the right tool. **[Upwork Jobs History](https://apify.com/hyperbach/upwork-jobs-history)** has every Upwork posting since November 2024 — over 3 million — with the full description, budget, skills, and the client's spend and hires as they stood on the day the job was posted. Take the last 3, 6 or 12 months or all of it, narrowed by category and skill; a free estimate prices any selection before you buy it.

A one-time download of the 2024–2025 dataset in CSV, JSON, Parquet, SQLite and DuckDB is also on [Gumroad](https://hyperbach.gumroad.com/l/upwork).

### 🆘 Support

Found an issue or have a feature request? Open an issue on the [Issues tab](https://apify.com/hyperbach/upwork-scraper-ai/issues). We respond within ~48 hours.

**Missing a field or a result?** Add `feedback` to any run and say what you needed and did not get: a missing field, a wrong or empty result, the Actor you switch from or to, and why. An AI agent can do the same. The run returns the same rows at the same price, and a person reads every note.

**Disagree with a charge?** Write to apify@hyperbach.com with the run ID. We will ask Apify to compensate you, and the charge comes back to your Apify account as platform credit.

Contact: apify@hyperbach.com

# Actor input Schema

## `search_url` (type: `string`):

Paste a search from upwork.com: run the search with the filters you want, then copy the address bar (https://www.upwork.com/nx/search/jobs/?q=…). Its keywords, job type, budget, hourly rate, experience level, payment verified, client hires, project length, hours per week, client location and category are read as the inputs below. A field you set yourself wins over the URL. A filter this Actor cannot apply is named in the run's status message, never dropped in silence. One search per run. A link to one job reads that job live, as refresh_job_ids does.

## `keywords` (type: `string`):

Search the job's title, description, skills, categories and AI-generated fields. A new search matches by meaning: every job with all your words, plus jobs about the same thing in other words (see keywords_match). `a OR b`, "quotes" and -word ask for an exact word match: bare words must all appear, OR separates alternatives, quotes keep a phrase, a leading minus drops a word. For a whole field of work, set category_name instead: it is exact (e.g. "Web, Mobile & Software Dev").

## `keywords_match` (type: `string`):

How `keywords` match. auto (default): a new search matches by meaning — every job with all your words, plus jobs about the same thing in other words, each with a `relevance` score; a search you already ran before this option existed keeps matching exactly as it did; a search written with OR, "quotes" or -word matches the words exactly. strict: the words exactly, always. semantic: by meaning, always. Editing the keywords of a saved task makes it a new search.

## `exclude_keywords` (type: `string`):

Drop jobs containing any of these words. Quotes keep a phrase together (`"data entry" wordpress` drops both), and you can write OR yourself. Searches the same text fields as keywords.

## `category_name` (type: `string`):

Upwork's top-level category of the job. The value must match exactly. One of 12, most jobs first: "Design & Creative", "Sales & Marketing", "Web, Mobile & Software Dev", "Admin Support", "Engineering & Architecture", "Accounting & Consulting", "Writing", "Data Science & Analytics", "Customer Service", "IT & Networking", "Translation", "Legal".

## `subcategory_name` (type: `string`):

Upwork's subcategory, one level under category_name. The value must match exactly. Most common: "Digital Marketing", "Video & Animation", "Graphic, Editorial & Presentation Design", "Lead Generation & Telemarketing", "Web Development", "Virtual Assistance". All 64 values, grouped under their category, are in the README section "Categories and subcategories".

## `date_posted` (type: `string`):

When the job was posted on Upwork. Use this to find recent opportunities or analyze posting patterns. Simplest form is a relative age: 90m = posted in the last 90 minutes, 24h = last day, 7d = last 7 days, 2w = last two weeks. A bare number means days (3 = last 3 days). Or give an ISO date (YYYY-MM-DD or full ISO datetime) with >= for newer than that date, <= for older.

## `created_at` (type: `string`):

When this job entered our index — a start point you own, including jobs Upwork listed late. Replayable and shareable, unlike a vendor-held 'since last run' state. A bound reads the whole millisecond it names: > the newest created_at you received returns exactly the jobs added since, and <= a row's created_at includes that row. notifications_only: true and page_after also walk without repeats or gaps. Use >=2026-09-01T00:00:00Z to start from a moment you choose, then pass > the newest created_at in each batch. This is ingestion time, not the Upwork posting date (that is date_posted).

## `limit` (type: `integer`):

Rows to return in this run (1-10000, default 50), ordered by when each posting entered our index, newest first. The searchable window is the last 90 days. With notifications_only=true each run continues from where the last one stopped; with notifications_only=false a run returns the newest matches; to go further back, pass the run's next_page token as page_after.

## `notifications_only` (type: `boolean`):

Set true on a schedule: each run returns only the postings that arrived since the previous run, per filter set, and every row is billed once. False returns the newest matches every time; a time-window filter polled often re-bills the overlap.

## `page_after` (type: `string`):

Full page only (notifications_only=false): continue below the last row of an earlier run. Paste that run's next_page (in its OUTPUT and status message) with the same filters, and this run returns the next older matches — no repeats, no gaps — until next_page is absent. Leave empty for the newest page.

## `estimate` (type: `boolean`):

Measure the search instead of running it: how many jobs it would have delivered per day, week and hour over the last 28 days, whether that is too narrow, workable or too broad, what a month of it costs on each Apify plan at your schedule, which filter cuts the most jobs, and 5 recent matches. Returns ONE row, billed as one result; moves no feed position. Uses every filter below, plus limit, notifications_only and estimate_poll_minutes for the cost.

## `estimate_poll_minutes` (type: `integer`):

How often you plan to run this search, for the estimate's monthly cost. Default 60.

## `include_history` (type: `boolean`):

Attach each job's recorded history to its row, free — no extra charge. `history.readings` is the job's state at each moment it changed (status, applicants, interviewing, invites, hires, last client activity, budget); `history.edits` is each change the client made to the posting, and what changed. Served from our own database of readings, not re-read from Upwork: a job we have not re-read yet has empty lists. Works in feed mode and in refresh mode, where it is the history recorded before that run.

## `refresh_shape` (type: `string`):

Refresh mode only. "raw" (default) returns each job's live record as it comes, nested. "flat" returns the same flat row the feed returns — the same field names and values — plus a `live` block (status, applicants, interviewing, invites, hires, last client activity), so feed and refresh rows have one shape.

## `slack_webhook_url` (type: `string`):

Post one message per run with the new jobs to a Slack channel. Slack app → Incoming Webhooks → Add New Webhook. Nothing is sent when a run finds nothing (see notify_when_empty). The URL goes to Slack only, never to our servers.

## `telegram_bot_token` (type: `string`):

From @BotFather. With telegram_chat_id, one Telegram message per run with the new jobs.

## `telegram_chat_id` (type: `string`):

Your chat or channel id (from @userinfobot; channels start with -100). The bot must be in the chat.

## `webhook_url` (type: `string`):

POST a JSON body {count, total, run_url, run_id, items\[]} (up to 500 rows) to your URL after each run that found jobs — for n8n, Make, Zapier or your own backend.

## `webhook_headers` (type: `object`):

Extra HTTP headers for the webhook POST, e.g. {"Authorization": "Bearer …"}.

## `notify_when_empty` (type: `boolean`):

Send the notification even when the run found no new jobs. Off by default so a schedule stays quiet.

## `whats_new` (type: `boolean`):

A one-line note about this Actor's newest feature at the end of the run's status message and in the OUTPUT record, until you try the feature or 20 runs have shown it. After regular use it also asks, at most three times and a week apart, for a rating on the Store page. Never added to the dataset rows. Uncheck to hide both.

## `all_words` (type: `array`):

Every entry must appear in the job. One word or phrase per entry; an entry with spaces must appear as written. Exact word match, like Upwork's advanced search.

## `any_words` (type: `array`):

At least one entry must appear in the job. One word or phrase per entry: webflow, framer, "landing page".

## `none_words` (type: `array`):

Drop a job if any entry appears in it. One word or phrase per entry.

## `exact_phrase` (type: `array`):

Each entry must appear with its words in this order, e.g. cold email.

## `title_search` (type: `array`):

Every entry must appear in the job title.

## `skills_search` (type: `array`):

Every entry must appear in the job's skills (the listed skills and the ones our AI read from the description).

## `buyer_payment_verified` (type: `boolean`):

Boolean flag indicating whether the client has verified their payment method on Upwork.

## `total_spent` (type: `string`):

Total amount the client has spent on Upwork across all their projects. Use numeric operators like >=1000000 for values above threshold, or range syntax like 500000-1000000 for values between 500000-1000000.

## `buyer_score` (type: `string`):

Score of the client's performance on Upwork. Use numeric operators like >=5.0 for values above threshold, or range syntax like 4.7-5.0 for values between 4.7-5.0.

## `job_score` (type: `string`):

Rule-based 0–100 score that ranks the job by attractiveness. Weighted across price (20), client reputation (42), client spending (40), premium status (5), and location (10). Because 82 of those 100 points come from client reputation and spending, a threshold mostly selects for good clients rather than good briefs. Half of all jobs score under 37 and only 9% reach 70, so start at >=50 and raise it. Jobs with no client spending history skip that 40-point block and therefore score low — a high threshold filters out thin postings as well as weak ones.

## `hire_rate` (type: `string`):

Upwork's own hire rate for the client: the share of their posted jobs that led to a hire, 0-100. It is not hires divided by jobs posted (a job can hire several people). High values mean a client who hires rather than browses. Use >= for clients who hire frequently (>=80), or <= for selective clients (<=50). Null values indicate new clients.

## `open_jobs` (type: `string`):

Number of jobs the client currently has open/active. Use numeric operators like <=10 for values below threshold, or range syntax like 5-20 for values between 5-20.

## `client_invites_sent` (type: `string`):

Invites the client had sent when the job was captured, minutes after posting. Clients usually send invites while creating the job, so this catches invite-first postings. 0 when the source reported none (NULLs backfilled to 0 on 2026-08-22 — before that, numeric filters silently dropped the unreported rows). "0" finds jobs with no invites yet at capture time (least pre-committed clients); ">=1" finds invite-first postings. Numeric operators and range syntax like 5-20 work as on any numeric field.

## `hires` (type: `string`):

Total number of freelancers hired by the client. Use numeric operators like >=10 for values above threshold, or range syntax like 5-20 for values between 5-20.

## `client_location` (type: `string`):

Geographic location of the client posting the job. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `avg_hourly_rate` (type: `string`):

Average hourly rate this client typically pays freelancers. Based on their historical hiring patterns. Filter by client's typical budget range. Use >= for higher-paying clients (>=60) or <= for budget-conscious clients.

## `buyer_city` (type: `string`):

City where the client is located. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `buyer_contract_date` (type: `string`):

Date when the client first registered their account on Upwork. Indicates how long the client has been active on the platform. Use >= for clients registered after a date, <= for before a date. Format: YYYY-MM-DD or ISO datetime. A relative age also works (365d = registered within the last year). Older registration dates indicate more established clients.

## `jobs_posted` (type: `string`):

How many jobs this client has posted on Upwork over their whole account — a count, not a date. "10" means 10 or more; use date_posted or created_at for a time window. A minimum: "5" or ">=5". Not a time filter — for jobs posted in the last day use date_posted: "24h".

## `price_type` (type: `string`):

How the job is priced: Fixed Price (one-time payment) or Hourly (paid per hour worked).

## `price` (type: `string`):

The budget amount for the fixed-price job. Use numeric operators like >=1000 for jobs above $1000, or 500-2000 for budgets between $500-2000. Only applies to fixed-price jobs.

## `price_min` (type: `string`):

Lower end of the job's hourly rate band (Upwork jobs advertise a range like $10-35/hr). Hourly jobs only - fixed-price jobs carry their budget in `price` and leave this empty. price_min and price_max together describe the rate window you want, and a job matches when its advertised band OVERLAPS that window. price_min=35 (or >=35) means "can pay $35/hr or more" and returns a job advertised at $10-35/hr. The operator decides which end of the window you are setting, so <=35 here means a ceiling and behaves exactly like price_max=35 - a bare number is read as a floor, never as an exact match. Use 25-75 for work paying somewhere in $25-75. Jobs stating no rate are matched on the client's historical hourly rate instead; see include_unspecified_rate.

## `price_max` (type: `string`):

Upper end of the job's hourly rate band (Upwork jobs advertise a range like $10-35/hr). Hourly jobs only - fixed-price jobs carry their budget in `price` and leave this empty. Sets the ceiling of the rate window you want; a job matches when its advertised band overlaps it. price_max=50 (or <=50) means "can be done at $50/hr or under". The operator decides which end you are setting, so >=50 here means a floor and behaves exactly like price_min=50 - a bare number is read as a ceiling, never as an exact match. Combine with price_min for a window, e.g. price_min=35 plus price_max=90. Jobs stating no rate are matched on the client's historical hourly rate instead; see include_unspecified_rate.

## `include_unspecified_rate` (type: `string`):

What a rate filter should do with jobs that state no rate - about a third of hourly jobs, plus every fixed-price job (their budget is in `price`). `likely` (default) also returns those jobs when the CLIENT's own historical hourly rate fits your window, which is the best evidence available when the posting itself is silent. `off` returns only jobs with a stated rate that overlaps. `all` returns every rate-unspecified job regardless of evidence.

## `experience_level` (type: `string`):

Required experience level for the job. Exact values: Entry_level, Intermediate, Expert.

## `title` (type: `string`):

The job posting title as written by the client. Contains the main description of what work needs to be done. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `description` (type: `string`):

Full job description text as written by the client. Contains detailed requirements, expectations, and project scope. Filter: matches the text as written, any case, as whole words: `qualified leads` does not match 'unqualified leads'. `a OR b` matches jobs with either.

## `skills` (type: `string`):

Comma-separated list of required skills and technologies for the job as specified by the client. Filter: matches the text anywhere in the field, any case. A comma list or `a OR b` matches jobs with any of them.

## `job_is_premium` (type: `boolean`):

Boolean flag indicating whether job is premium applies to this job.

## `qual_rising_talent` (type: `boolean`):

Whether the job is open to Upwork Rising Talent (newer freelancers with potential).

## `qual_portfolio_required` (type: `boolean`):

Whether the client requires a portfolio or work samples to apply.

## `qual_min_hours_week` (type: `string`):

Minimum hours per week the client requires for hourly jobs. Values seen: 0 (none stated), 10, 30, 40.

## `ai_urgency` (type: `string`):

AI-detected urgency level of the job based on language and posting patterns.

## `ai_technical_skills` (type: `string`):

Technical skills explicitly mentioned in the job description, extracted using AI. These are skills directly stated by the client as requirements or preferences. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `ai_explicit_mention_of_agency` (type: `string`):

AI-detected explicit mention of agency preferences in the job posting. Indicates whether the client welcomes agencies, prefers individual freelancers, or has no specific preference.

## `clientId` (type: `string`):

Unique identifier for the client. The service automatically tracks what jobs this client has already seen.

## `fields` (type: `array`):

Optional list of field names to keep in the dataset output. If empty, all fields are returned. Saves storage cost on long runs. Example: id, title, url, job_score, questions. Always-implicit field `id` is kept whether listed or not.

## `refresh_job_ids` (type: `array`):

LIVE mode: fetch these jobs fresh from Upwork right now instead of serving the pre-indexed store. Accepts up to 50 entries in any form — job URL, ~02… cipher, or the numeric id. Returns each job's CURRENT state: activity counters (applicants, hires, invites) plus the client's recent contract history with both-way review texts. Cannot be combined with search filters. Billed per returned job at the normal result rate; ids that no longer resolve are free and listed in the run OUTPUT.

## `active_hires` (type: `string`):

Number of freelancers currently hired by the client. Use numeric operators like >=10 for values above threshold, or range syntax like 5-20 for values between 5-20.

## `ai_anti_bot_phrase` (type: `string`):

AI-detected anti-bot phrases and instructions used by clients in job descriptions to filter out automated applications and ensure human applicants read the full posting. These phrases typically ask applicants to include specific words, numbers, emojis, or perform certain actions in their proposals to prove they've read the requirements carefully. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `ai_clients_technical_understanding` (type: `string`):

AI-detected assessment of the client's technical understanding based on how they describe their project requirements. Helps identify whether the client has deep technical knowledge, moderate understanding, basic knowledge, or expert-level expertise in the domain.

## `ai_deadline` (type: `string`):

AI-detected deadline type for the job based on urgency indicators and time-sensitive language in the job description.

## `ai_duration` (type: `string`):

AI-detected project duration based on job description analysis. Indicates expected length and type of engagement.

## `ai_inferred_technical_skills` (type: `string`):

Technical skills inferred by AI from the job description context, even when not explicitly mentioned. These are skills likely needed based on project requirements and industry patterns. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `ai_named_entities` (type: `string`):

AI-extracted named entities from the job description including company names, technologies, frameworks, tools, locations, and other important entities mentioned by the client. This field helps identify specific brands, platforms, or technologies the client is working with. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `ai_specific_requirements_before_applying` (type: `string`):

AI-extracted specific requirements that clients mention applicants must fulfill before applying, such as portfolio submissions, work samples, or specific application instructions. Filter: matches the text anywhere in the field, any case. `a OR b` matches jobs with either.

## `buyer_feedback_count` (type: `string`):

Number of feedbacks the client has received. Use numeric operators like >=10 for values above threshold, or range syntax like 5-20 for values between 5-20.

## `buyer_offset_utc` (type: `string`):

UTC offset of the buyer's timezone in milliseconds. Indicates the time difference between the buyer's local time and UTC. Values are in milliseconds. Common offsets: 0 (UTC/GMT), 3600000 (+1 hour, Europe), -28800000 (-8 hours, US West Coast), 28800000 (+8 hours, Asia). Use >= or <= to find clients in specific timezone ranges. Positive values are east of UTC, negative are west.

## `company_size` (type: `string`):

Size of the client's company. Predefined option for company size. Select from available choices.

## `engagement_label` (type: `string`):

Expected duration or type of engagement (e.g., 1 to 3 months, 3 to 6 months, Less than 1 month, Less than 1 week, More than 6 months).

## `engagement_weeks` (type: `string`):

Project duration in weeks as Upwork encodes it. Values seen: 3, 9, 18, 52.

## `hourly_budget_type` (type: `string`):

Type of hourly budget. Options: AS_NEEDED, DEFAULT, FULL_TIME, MANUAL, NOT_PROVIDED, NOT_SURE, PART_TIME.

## `industry` (type: `string`):

The client's industry, as the client set it on Upwork. The value must match exactly. Most common: "Tech & IT", "Sales & Marketing", "Media & Entertainment", "Education", "Health & Fitness", "Retail & Consumer Goods". All 29 values in the last 90 days are in the list.

## `qual_min_success_score` (type: `string`):

Minimum Upwork success score required to apply for the job.

## `qual_pref_english` (type: `string`):

Client's preferred English proficiency level for freelancers.

## `qual_type` (type: `string`):

Type of freelancer the client is looking for: Agency (team/company), Independent (solo freelancer), or Any (no preference).

## `total_hours` (type: `string`):

Total number of hours the client has worked on Upwork across all their projects. Use numeric operators like >=100 for values above threshold, or range syntax like 50-200 for values between 50-200.

## `feedback` (type: `string`):

A note to the people who maintain this Actor: what you needed and did not get. For example a missing field, a wrong or empty result, or the Actor you switch from or to, and why. The run does not read it: the rows and the charges stay the same. A person reads every note.

## Actor input object example

```json
{
  "keywords_match": "auto",
  "limit": 50,
  "notifications_only": false,
  "estimate": false,
  "estimate_poll_minutes": 60,
  "include_history": false,
  "refresh_shape": "raw",
  "notify_when_empty": false,
  "whats_new": true,
  "clientId": "default"
}
```

# Actor output Schema

## `newJobs` (type: `string`):

Dataset of new AI-enriched Upwork jobs returned by this run. Cursor-tracked per clientId — only jobs the caller has not seen since their last run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("hyperbach/upwork-scraper-ai").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("hyperbach/upwork-scraper-ai").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call hyperbach/upwork-scraper-ai --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hyperbach/upwork-scraper-ai"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/RDMDCh3d2XZKng8Mz/builds/3ilBo8gpDpAbcsNV3/openapi.json
