# Upwork Jobs Feed: Job Scraper and Alerts (`cloudastra-technologies/upwork-feed`) Actor

Upwork job scraper for new job posts with the client details that matter: payment verified, country, total spent, rating and exact applicant count. Filter, schedule, get alerts.

- **URL**: https://apify.com/cloudastra-technologies/upwork-feed.md
- **Developed by:** [Cloudastra Technologies](https://apify.com/cloudastra-technologies) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.95 / 1,000 job delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 Jobs Feed

An Upwork job scraper built for job alerts. New Upwork job posts, minutes after
they go live, with the client details you use to decide whether a job is worth a
proposal: payment verified, country,
total spent, rating, number of reviews and the exact number of applicants.
Filter on any of them, run it on a schedule, and get only the jobs you have not
seen before, in your dataset or straight to Telegram, Slack, Discord, WhatsApp
or a webhook.

### What you get

- **One row per new job**, newest first: title, full description, link, job
  type, budget or hourly range, experience level, skills, project length,
  workload, and when it was published.
- **The client panel** for every job: country, total spent on Upwork, payment
  verification, rating, number of reviews, and whether the client hides their
  spend.
- **The applicant count** as a number, not a bucket like "10 to 15".
- **Filters** on keywords, client location, client spend, rating and reviews,
  payment verification, job type, experience level, budget, hourly rate,
  workload, project length, number of proposals and publish date, plus your own
  rules on any output field.
- **Up to 10 searches in one run**, each job delivered once.
- **Incremental mode**: schedule it and receive only jobs that are new or
  changed since your last run.
- **Notifications** to your own Telegram, Slack, Discord, WhatsApp or webhook.

### Who it is for

- **Freelancers and agencies** who want to see good jobs early, while the
  applicant count is still low, without refreshing Upwork all day.
- **Lead and sales teams** who track which kinds of clients are hiring for
  which skills.
- **Analysts** who watch demand, rates and budgets for a skill or a market over
  time.
- **Builders of job alert bots and dashboards** who need a clean, filterable
  feed in JSON, CSV or Excel, or through n8n, Make or Zapier.

### Limitations, in plain language

Read this before you buy. It is here so you do not need a refund.

**It depends on Upwork's public job search, and it can stop working if Upwork
changes it.** This actor reads the same public job listings anyone can browse on
Upwork. It is not an official Upwork product and does not use an official Upwork
API. If Upwork changes or restricts its public search, runs can fail until we
adapt, and we cannot promise how long that takes. When that happens a run fails
with a message saying so, rather than returning an empty list that looks like a
quiet hour.

**New public job posts only.** You get jobs that are open and publicly listed.
Invite-only and private jobs are shown by Upwork to invited freelancers only, so
no tool outside Upwork sees them. Closed jobs, past contracts, freelancer
profiles and messages are not included.

**A field Upwork does not report is `null`.** Nothing is guessed or filled in.
For example, a fixed price that Upwork lists without a currency has a `null`
currency, not "USD", and a client who hides their spend has a `null` total
spent. Filters treat these cases the way each filter's description says,
so read the description before relying on one.

**Your use of the data is your responsibility.** Upwork's Terms of Use restrict
automated access to its site. Check that the way you use these jobs fits those
terms and the law where you work.

**No client names, no contact details, no job attachments.** Upwork does not show
a client's name or contact details in its public job listings, and this actor
does not log in to Upwork.

**Search results are capped by Upwork.** Upwork serves about 5,000 results per
search, so a very broad search over a long time window will not reach every job
ever posted. The actor is built for recent jobs: the last minutes, hours or
days.

**Upwork's order is only roughly newest first.** A reposted job is sometimes
listed at its original date, below the newest jobs. Set Minimum pages on an
occasional run to read deeper and catch those.

**There is an hourly page limit per account** (see below). A run that reaches it
delivers what it read and finishes successfully, so check the status message if
a run returns fewer jobs than you expected.

**Now and then a run cannot reach Upwork's search.** It then fails with a
message telling you to retry in a few minutes. On a schedule, keep
`maxAgeMinutes` longer than the time between runs, so the next run also covers
what a failed one missed. With several searches in one run, a run where some
searches worked and others could not be completed finishes successfully with the
jobs it found, and its status message says how many searches were completed.

**A run may start a second run of this actor in your account.** Some searches
need more memory than a normal run has. The run then starts one more run of this
actor in your account, with 4 GB of memory, waits for it and delivers its jobs
in the run you started. You will see that second run in your list of runs, and
it is billed to you like any other run, within the maximum charge you set for the
run you started. If the run you started stops, that second run stops within about a
minute.

**When the service is busy, a run may be turned away.** It then fails at once
with "The service is busy, try again in a few minutes.", before reading or
delivering anything.

**Run it with 256 MB of memory, the default.** A run started with more memory
fails at once with "Run this actor with 256 MB of memory.", before reading or
delivering anything, so you are not billed for memory it cannot use.

**Lists in the input have limits.** Keyword, location and custom filter lists take
up to 100 entries of up to 500 characters each; a search query takes up to 1,000
characters.

**Your maximum charge per run is respected.** A run stops delivering once the next
job would go over the maximum you set for it, finishes successfully and says so in
its status message. Jobs it did not deliver are offered again by the next run in
incremental mode.

**Notifications are best effort.** Each channel gets one attempt per run. A
channel that fails (a wrong token, a deleted webhook) is reported in the log and
never fails the run; the jobs are still in your dataset.

### Quick start

Search for new Python jobs from the last hour and keep at most 50:

```json
{
    "query": "python",
    "maxAgeMinutes": 60,
    "maxResults": 50
}
```

A scheduled alert for well-funded clients outside a few countries, delivered to
Telegram, new jobs only:

```json
{
    "query": "react OR next.js",
    "maxAgeMinutes": 30,
    "verifiedPaymentOnly": true,
    "minClientTotalSpent": 1000,
    "excludeLocations": ["IN", "PK", "BD"],
    "jobType": "hourly",
    "hourlyRate": "40-",
    "incrementalMode": true,
    "stateKey": "react-alerts",
    "telegramToken": "123456:ABC-your-bot-token",
    "telegramChatId": "-1001234567890"
}
```

Schedule the second one every 15 or 30 minutes in Apify and you have a job alert.

### Input

Every field is optional. An empty search returns every new job on Upwork.

#### Search

| Field | Default | What it does |
|---|---|---|
| `query` | empty | Upwork search text. Upwork matches ALL the words; join alternatives with OR (`"landing page" OR shopify`). Commas mean AND. Empty means every new job. |
| `queries` | none | Instead of `query`: up to 10 searches in one run, each a text or `{"query": "...", "stateKey": "..."}`. See "Several searches in one run". |
| `sort` | `recency` | `recency` or `newest` (newest first), `relevance`, or `oldest` (the same jobs, delivered oldest first). |
| `maxAgeMinutes` | 0 | Only jobs published in the last N minutes. 0 means no limit. |
| `maxResults` | 100 | Most rows to deliver per search, newest first, up to 5,050. 0 means no cap. A job several searches match counts in each but is delivered once. |

#### Filters

Filters run on what Upwork returned. A job that fails a filter is never written
to your dataset.

| Field | What it keeps |
|---|---|
| `excludeKeywords` | Drops jobs containing any of the keywords: `{"keywords": ["wordpress"], "matchTitle": true, "matchDescription": false, "matchSkills": true}`. Case-insensitive. A match flag you leave out counts as true. |
| `includeKeywords` | Keeps jobs containing at least one keyword. Same shape. |
| `excludeLocations` | Drops clients in these countries or regions: a country name, a 2 or 3 letter code (`"US"`, `"USA"`), or a region (`"europe"`, `"south_america"`, `"english_speaking"`). Regions follow the UN M49 list. Jobs with no reported country are kept. |
| `location` | Keeps only clients in these countries or regions. Same format. Jobs with no reported country are dropped. |
| `verifiedPaymentOnly` | Only clients whose payment method Upwork reports as verified. |
| `minClientTotalSpent` | Only clients who have spent at least this much (USD). A client who hides their spend, or has none reported, is kept. |
| `minClientRating` | Only clients rated at least this (0 to 5). Unrated clients are kept. |
| `minClientReviewCount` | Only clients with at least this many reviews. |
| `jobType` | `hourly` or `fixed`. |
| `experienceLevel` | `EntryLevel`, `IntermediateLevel` or `ExpertLevel`. |
| `workload` | `as_needed`, `part_time` or `full_time`. Hourly jobs only. |
| `duration` | `week` (less than 1 month), `month` (1 to 3 months), `semester` (3 to 6 months) or `ongoing` (more than 6 months). |
| `budget` | Fixed-price range: `0-99`, `100-499`, `500-999`, `1000-4999` or `5000-`. Hourly jobs are dropped. |
| `hourlyRate` | Hourly jobs whose posted range overlaps yours: `"20-60"`, or `"50-"` for 50 and up. Jobs without a range are dropped. |
| `proposals` | Proposals already sent: `0-4`, `5-9`, `10-14`, `15-19` or `20-49`. Jobs that do not report it are kept. |
| `fromDate`, `toDate` | Published on or after, on or before. A date (`2026-10-01`) or an ISO-8601 time. A bare `toDate` covers the whole day. |
| `customFilters` | Your own rules on any output field: `[{"field": "clientCountry", "op": "equals", "value": "United States"}]`. `op` is `includes`, `notIncludes`, `equals`, `notEquals`, `gt`, `gte`, `lt` or `lte`. Text matching ignores case. An unknown field or op stops the run with an error before anything is read. |

#### Incremental mode

| Field | Default | What it does |
|---|---|---|
| `incrementalMode` | off | Deliver only jobs that are NEW or UPDATED since the last run with the same state key. |
| `stateKey` | derived | Names the remembered search. Empty means it is derived from your search and filters, so changing a filter starts a fresh memory. |
| `emitUnchanged` | off | Also deliver jobs seen before whose content has not changed. |
| `retentionDays` | 7 | How long a job is remembered, 1 to 30 days. |

#### Output

| Field | Default | What it does |
|---|---|---|
| `descriptionFormat` | `text` | `description` is always plain text. `html` adds `descriptionHtml`, `markdown` adds `descriptionMarkdown`, `all` adds both. |
| `descriptionMaxLength` | 0 | Cut descriptions to this many characters. 0 keeps the full text. Filters always see the full text. |
| `compact` | off | Only the fields needed to decide on a job: title, link, budget, client, skills, applicants. |
| `excludeEmptyFields` | off | Leave out fields that are `null`, empty text or an empty list. |

#### Paging

| Field | Default | What it does |
|---|---|---|
| `maxPages` | 20 | Most pages read per search, 50 jobs each. Pages read count toward the hourly limit. |
| `minPages` | 0 | Read at least this many pages before the time window may stop paging. Use it on an occasional run to catch reposted jobs listed at their original date. |
| `orderToleranceMinutes` | 15 | Upwork's order is only roughly newest first, so paging stops only once a whole page is this much older than your time window. |

#### Notifications

| Field | What it does |
|---|---|
| `telegramToken`, `telegramChatId` | Your bot token from @BotFather and a chat ID or channel ID (channel IDs start with -100). Add the bot to the channel as an admin that can post. |
| `slackWebhookUrl` | A Slack incoming webhook. |
| `discordWebhookUrl` | A Discord channel webhook. |
| `whatsappPhoneNumberId`, `whatsappAccessToken`, `whatsappTo` | Meta's WhatsApp Cloud API. Meta delivers only within 24 hours of the recipient's last message to your business number. |
| `webhookUrl`, `webhookHeaders` | One JSON POST per run, `{"metadata": ..., "items": [...]}`, with every delivered job. For n8n, Make, Zapier or your own backend. One attempt, 15 second timeout. |
| `notificationLimit` | Most jobs per Telegram, Slack, Discord or WhatsApp message, 1 to 20 (default 5). The webhook always gets all of them. |
| `notifyOnlyChanges` | In Incremental mode, leave jobs marked UNCHANGED out of notifications. |
| `includeRunMetadata` | Start each message with the search and the number of jobs (on by default). |

Tokens, webhook URLs, webhook headers and the WhatsApp recipient are secret
inputs, so Apify stores them encrypted. The actor never writes them to your dataset or log.

### Output

One row per job. Example (invented values):

```json
{
    "jobId": "1846201937455128576",
    "title": "Python developer for a data pipeline in FastAPI",
    "description": "We have a small FastAPI service that loads CSV exports into Postgres...",
    "url": "https://www.upwork.com/jobs/~021846201937455128576",
    "jobType": "HOURLY",
    "experienceLevel": "IntermediateLevel",
    "budgetAmount": null,
    "budgetCurrency": null,
    "hourlyBudgetMin": 35,
    "hourlyBudgetMax": 60,
    "engagementType": "PART_TIME",
    "engagementDuration": "1 to 3 months",
    "engagementDurationWeeks": 9,
    "skills": ["Python", "FastAPI", "PostgreSQL"],
    "publishTime": "2026-10-16T09:12:44.000Z",
    "totalApplicants": 3,
    "clientCountry": "Germany",
    "clientCountryCode": "DE",
    "clientTotalSpent": 12450,
    "clientPaymentVerified": true,
    "clientRating": 4.86,
    "clientReviewCount": 21,
    "clientHasFinancialPrivacy": false,
    "searchIndex": 0,
    "searchIndexes": [0],
    "scrapedAt": "2026-10-16T09:14:02.511Z",
    "source": "upwork.com"
}
```

All fields:

| Field | Meaning |
|---|---|
| `jobId` | Upwork's id for the job. |
| `title`, `description` | As posted. `description` is plain text. |
| `descriptionHtml`, `descriptionMarkdown` | Only when you ask for them in `descriptionFormat`. Made from the plain text. |
| `url`, `portalUrl` | The job's page on Upwork (the same link twice, for tools that expect either name). |
| `jobType` | `HOURLY` or `FIXED`. |
| `experienceLevel` | `EntryLevel`, `IntermediateLevel` or `ExpertLevel`. |
| `budgetAmount`, `budgetCurrency` | Fixed price and its currency. |
| `hourlyBudgetMin`, `hourlyBudgetMax` | Posted hourly range. |
| `weeklyRetainerBudget` | Weekly retainer, when the client set one. |
| `engagementType` | Workload on hourly jobs: `AS_NEEDED`, `PART_TIME` or `FULL_TIME`. |
| `engagementDuration`, `engagementDurationWeeks` | Project length as Upwork labels it, and in weeks. |
| `skills`, `skillsDetailed` | Skill names, and the same skills with Upwork's ids and which were highlighted. |
| `publishTime`, `createTime`, `sourcingTimestamp` | When the job was published and created, and Upwork's own sourcing time for it. |
| `totalApplicants` | Exact number of proposals so far. |
| `personsToHire` | How many freelancers the client wants. |
| `enterpriseJob`, `premium` | Upwork's enterprise and premium flags. |
| `clientCountry`, `clientCountryCode` | Client's country name and ISO code. |
| `clientTotalSpent`, `clientSpentCurrency` | What the client has spent on Upwork. `null` when the client hides it. |
| `clientPaymentVerified` | `true` when Upwork reports the payment method as verified, `false` for any other reported status, `null` when not reported. |
| `clientRating`, `clientReviewCount` | Client's rating (0 to 5) and number of reviews. |
| `clientHasFinancialPrivacy` | `true` when the client hides their spend. |
| `salaryType`, `salaryMin`, `salaryMax`, `salaryCurrency` | The budget in one shape for both job types: a fixed price is both ends, an hourly job its range. |
| `contentHash` | Changes when the job's title, description, type, experience level, budget, hourly range, length or skills change. Applicant count and client spend do not count as a change. |
| `searchIndex` | In a run with several searches, the search (its position in `queries`, from 0) that found this job. |
| `searchIndexes` | Every search index that matched this job, in a run with several searches; each job is delivered once. |
| `changeType` | In Incremental mode: `NEW`, `UPDATED` or `UNCHANGED`. |
| `scrapedAt` | When the run read the job. |
| `source` | Always `upwork.com`. |

Export the dataset as JSON, CSV, Excel or XML from Apify, or read it through
Apify's API and integrations.

### Several searches in one run

Put up to 10 searches in `queries` instead of `query`. They share one run, one
set of filters and one dataset. A job that more than one search finds is
delivered once, and `searchIndexes` lists every search that matched it. Give a
search its own `stateKey` to keep its Incremental mode memory separate:

```json
{
    "queries": [
        "python",
        {"query": "react OR vue", "stateKey": "frontend"},
        "\"data engineer\""
    ],
    "maxAgeMinutes": 60
}
```

One run with three searches is cheaper and quicker to schedule than three runs.

### Incremental mode

Turn on `incrementalMode` and schedule the actor. Each run delivers only jobs it
has not delivered before for the same state key (`NEW`), plus jobs whose content
changed since (`UPDATED`). Jobs are remembered for `retentionDays` (default 7,
up to 30). Memory is kept separately for each Apify account, so nobody else's
runs affect yours.

Leave it off if you de-duplicate on your side, or if you want every matching job
in the time window on every run.

### Hourly page limit

Each Apify account can read up to **60 result pages an hour** with this actor.
A page holds up to 50 jobs, so that is up to 3,000 jobs an hour before your
filters. Pages read count toward it, and a page that had to be requested again
can count more than once, so a slow hour can use the limit up sooner.

When a run reaches the limit it stops reading, delivers what it has read,
finishes successfully and says so in its status message. The next run carries
on. To stay under it, run less often, narrow your search, or lower `maxPages`.

There is also an overall limit shared by everyone using this actor. In a busy
hour a run can reach it before your own 60 pages, and the status message reads
the same. There is a limit on how many runs each account can start in an hour
as well; a run over it is turned away as busy (see Limitations).

### Pricing

**$0.95 per 1,000 jobs delivered, plus $0.002 per run.** No monthly fee. Every
filter, incremental mode and every notification channel is included.

- You pay for the jobs that reach your dataset, after your filters. A job your
  filters drop is never charged.
- A job that matches several of your searches in one run is delivered, and
  charged, once.
- A run that finds nothing new costs $0.002.
- The rare 4 GB second run (see Limitations) is charged one run start per GB of
  memory, so $0.008. Its jobs are delivered and charged in the run you started,
  not twice.
- Set a maximum charge per run and the run stays within it.

| Use | About |
|---|---|
| One search, 50 jobs | $0.05 |
| 3 searches in one run every 5 minutes, about 2 new jobs a run | $1.12 a day |
| The same schedule on a day with nothing new | $0.58 a day |

### FAQ

**Is this a replacement for Upwork's job RSS feeds?**
Yes. Upwork retired its job RSS feeds in 2024. Schedule this actor every few
minutes with incremental mode on and a Telegram, Slack, Discord, WhatsApp or
webhook channel, and you get the same stream of new jobs, with client details and
filters RSS never had.

**How fresh are the jobs?**
As fresh as Upwork's own listings at the moment the run reads them. For alerts,
schedule the actor every 5 to 30 minutes with a `maxAgeMinutes` a little longer
than the schedule.

**Why did a run return fewer jobs than I expected?**
Check the status message. The usual reasons are your filters, the time window,
`maxResults`, or the hourly page limit. Jobs with a field Upwork did not report
are kept or dropped as each filter's description says.

**Why is a field `null`?**
Because Upwork did not report it for that job. The actor never fills in a value
it was not given.

**Does it need my Upwork account or an API key?**
No. It does not log in to Upwork, and you do not need any third-party API key.
Notifications use your own Telegram, Slack, Discord, WhatsApp or webhook
details, which are optional.

**Can I see invite-only jobs, client names or freelancer profiles?**
No. See Limitations.

**How many jobs does a run return?**
Up to `maxResults` (default 100, at most 5,050) after your filters. How many are
available depends on the search and the time window.

**Can I use it from n8n, Make, Zapier or my own code?**
Yes. Use the webhook input to receive each run's jobs as one JSON POST, or start
runs and read datasets through Apify's API and integrations.

**What does the status message say?**
How many jobs were delivered and from how many searches, for example "12 job(s)
delivered from 3 searches.", and whether the hourly limit was reached. A failed
run says whether to retry.

**Does the actor keep my data?**
Incremental mode remembers the ids and content hashes of jobs it delivered,
per Apify account and state key, for `retentionDays`. Each run's delivered rows
and its input (with secret fields masked) are also kept on our side for 30 days
so that we can look into a failed run, and are then deleted.

# Actor input Schema

## `query` (type: `string`):

Upwork search text, like python or "landing page". Leave empty for every new job on Upwork. Upwork matches ALL words; join alternatives with OR ("landing page" OR shopify). Commas mean AND.

## `queries` (type: `array`):

Instead of Search query: up to 10 searches in one run, each a text or {"query": ..., "stateKey": ...}. One run reads all of them with the same filters and delivers each job once. searchIndex on a row is the search (its position in this list, from 0) that found it, and searchIndexes lists every search that matched it.

## `sort` (type: `string`):

Most recent and Newest: newest first, which the time window and paging assume. Oldest first: the same jobs, delivered oldest first. Most relevant: Upwork's relevance order.

## `maxAgeMinutes` (type: `integer`):

Only jobs published within the last N minutes. 0 = no limit (then Max results and Max pages bound the run). For a scheduled alert, set it a little longer than the time between runs.

## `maxResults` (type: `integer`):

Most rows to deliver per search, newest first, after filters. 0 = no cap. Upwork serves about 5,000 results per search.

## `excludeKeywords` (type: `object`):

Drop jobs containing any of these, case-insensitive substring. {"keywords": \["wordpress"], "matchTitle": true, "matchDescription": false, "matchSkills": true}. Omitted match flags count as true.

## `includeKeywords` (type: `object`):

Keep only jobs containing at least one of these. Same shape as excludeKeywords.

## `excludeLocations` (type: `array`):

Countries (name, "US" or "USA") or regions ("europe", "south_america", "english_speaking"), or {"type": "COUNTRY"|"REGION", "value": ...}. Regions follow the UN M49 list. Jobs with no reported country pass.

## `location` (type: `array`):

Keep only clients in these countries or regions. Same format as Exclude client locations. Jobs with no reported country are dropped.

## `verifiedPaymentOnly` (type: `boolean`):

Keep only clients Upwork reports as VERIFIED. A client with no reported status is dropped.

## `minClientTotalSpent` (type: `integer`):

Drop clients who have spent less. Hidden (financial privacy) or unreported spend passes.

## `minClientRating` (type: `number`):

Drop clients rated below this (0 to 5). Unrated clients pass.

## `minClientReviewCount` (type: `integer`):

Drop clients with fewer reviews than this.

## `jobType` (type: `string`):

Only hourly or only fixed-price jobs.

## `experienceLevel` (type: `string`):

Required experience.

## `workload` (type: `string`):

Hours per week the client expects. Applies to hourly jobs; fixed-price jobs are dropped.

## `duration` (type: `string`):

Expected project length, as Upwork labels it.

## `budget` (type: `string`):

Fixed-price jobs in this range. Hourly jobs are dropped.

## `hourlyRate` (type: `string`):

Hourly jobs whose posted range overlaps this one: "20-60", or "50-" for 50 and up. Fixed-price jobs and hourly jobs without a range are dropped.

## `proposals` (type: `string`):

Number of proposals already sent. Jobs that do not report it pass.

## `fromDate` (type: `string`):

A date (2026-10-01) or ISO-8601 time. Works with Max job age: the later of the two applies.

## `toDate` (type: `string`):

A date (the whole day counts) or ISO-8601 time.

## `customFilters` (type: `array`):

Rules on any output field: \[{"field": "clientCountry", "op": "equals", "value": "United States"}]. op is includes, notIncludes, equals, notEquals, gt, gte, lt or lte. Text matching ignores case.

## `incrementalMode` (type: `boolean`):

Only push jobs that are NEW or UPDATED since this stateKey last ran. Turn it off if you de-duplicate on your side.

## `stateKey` (type: `string`):

Names the remembered search. Empty = derived from the search inputs.

## `emitUnchanged` (type: `boolean`):

In incremental mode, also push jobs seen before whose content has not changed.

## `retentionDays` (type: `integer`):

How long incremental mode remembers a job id.

## `descriptionFormat` (type: `string`):

description is always plain text. html adds descriptionHtml, markdown adds descriptionMarkdown, all adds both, made from that text.

## `descriptionMaxLength` (type: `integer`):

Cut descriptions to this many characters. 0 = full text. Filters see the full text.

## `compact` (type: `boolean`):

Only the fields needed to decide on a job (title, budget, client, skills, link).

## `excludeEmptyFields` (type: `boolean`):

Drop null, empty text and empty list fields from each row.

## `orderToleranceMinutes` (type: `integer`):

Upwork's newest-first order is only approximate, so paging stops only once a whole page is this much older than the time window. Most runs never need to change it.

## `maxPages` (type: `integer`):

Most pages read per search, 50 jobs each. Pages read count toward your account's limit of 60 pages an hour.

## `minPages` (type: `integer`):

Read at least this many pages before the time window may stop paging. Upwork lists some reposted jobs at their original date, below the newest jobs; an occasional deeper run finds them. Pages read count toward the hourly limit. 0 = normal stop rule.

## `telegramToken` (type: `string`):

From @BotFather. With a chat ID set, each run sends its new jobs to that chat or channel. Pairs well with Incremental mode.

## `telegramChatId` (type: `string`):

Chat ID (from @userinfobot) or channel ID (starts with -100). Add the bot to the channel as an admin that can post.

## `slackWebhookUrl` (type: `string`):

Slack incoming webhook (Slack app, Incoming Webhooks, Add New Webhook).

## `discordWebhookUrl` (type: `string`):

Server Settings, Integrations, Webhooks, New Webhook.

## `whatsappPhoneNumberId` (type: `string`):

Meta Business phone number ID (the numeric ID, not the phone number).

## `whatsappAccessToken` (type: `string`):

Meta Cloud API token with whatsapp_business_messaging. Meta only delivers within 24 hours of the recipient's last message to your business number.

## `whatsappTo` (type: `string`):

Phone number in E.164 format, like +4512345678.

## `webhookUrl` (type: `string`):

For n8n, Make, Zapier or your own backend: one JSON POST per run, {metadata, items}, with every delivered job. One attempt, 15 s timeout.

## `webhookHeaders` (type: `object`):

Extra headers for the webhook, like {"Authorization": "Bearer ..."}.

## `notificationLimit` (type: `integer`):

Most jobs sent to Telegram, Slack, Discord or WhatsApp per run. The webhook gets all of them.

## `notifyOnlyChanges` (type: `boolean`):

In Incremental mode, leave out jobs marked UNCHANGED. No effect otherwise.

## `includeRunMetadata` (type: `boolean`):

Start each notification with the search and the number of jobs.

## Actor input object example

```json
{
  "query": "python",
  "queries": [
    "python",
    {
      "query": "react OR vue",
      "stateKey": "frontend"
    }
  ],
  "sort": "recency",
  "maxAgeMinutes": 1440,
  "maxResults": 50,
  "excludeKeywords": {
    "keywords": [
      "wordpress",
      "unpaid"
    ],
    "matchTitle": true,
    "matchDescription": false,
    "matchSkills": true
  },
  "includeKeywords": {
    "keywords": [
      "fastapi",
      "django"
    ]
  },
  "excludeLocations": [
    "IN",
    "PK",
    "south_america"
  ],
  "location": [
    "US",
    "CA",
    "europe"
  ],
  "verifiedPaymentOnly": false,
  "minClientTotalSpent": 0,
  "minClientRating": 0,
  "minClientReviewCount": 0,
  "hourlyRate": "30-80",
  "fromDate": "2026-10-01",
  "toDate": "2026-10-15",
  "customFilters": [
    {
      "field": "totalApplicants",
      "op": "lte",
      "value": 10
    }
  ],
  "incrementalMode": false,
  "stateKey": "python-alerts",
  "emitUnchanged": false,
  "retentionDays": 7,
  "descriptionFormat": "text",
  "descriptionMaxLength": 0,
  "compact": false,
  "excludeEmptyFields": false,
  "orderToleranceMinutes": 15,
  "maxPages": 20,
  "minPages": 0,
  "telegramChatId": "-1001234567890",
  "notificationLimit": 5,
  "notifyOnlyChanges": false,
  "includeRunMetadata": true
}
```

# Actor output Schema

## `jobs` (type: `string`):

One row per job: title, description, link, budget or hourly range, skills, publish time, applicant count and the client panel (country, total spent, payment verified, rating, reviews). Each job is delivered and billed once per 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 = {
    "query": "python",
    "maxAgeMinutes": 1440,
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("cloudastra-technologies/upwork-feed").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 = {
    "query": "python",
    "maxAgeMinutes": 1440,
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("cloudastra-technologies/upwork-feed").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 '{
  "query": "python",
  "maxAgeMinutes": 1440,
  "maxResults": 50
}' |
apify call cloudastra-technologies/upwork-feed --silent --output-dataset

```

## MCP server setup

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

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/lSeuDvcfFRbpT3azQ/builds/ebqTgzn3o9nLf5etK/openapi.json
