# Upwork Clients Scraper — Company Behind Any Upwork Job (`moxlade/upwork-buyer-intel`) Actor

Give it Upwork job IDs or URLs, from any Upwork scraper or the Upwork API. Get the company behind each job, the evidence for the name, what the client has paid, and each contract with the freelancer and both reviews. One price per named client. Jobs from the last 30 days.

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

## Pricing

from $50.00 / 1,000 named clients

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 Clients Scraper — Company Behind Any Upwork Job

**The client behind an Upwork job.** Give it Upwork job IDs or URLs, from any Upwork scraper or the Upwork API, and get the company that posted each job: its name and domain, the evidence for the name (the brands, people and clients the postings give away), what the client has paid on Upwork with a split by kind of work, and on request each contract with the freelancer, the money and both reviews. A client is named only at high confidence; every other ID comes back as a free row that says why. This is not Upwork's API and has no link to Upwork: it reads a corpus of public Upwork postings and client histories.

### One job, its client and their contracts

Give it this:

```json
{
  "jobIds": [
    "https://www.upwork.com/jobs/~022112651010648303714"
  ],
  "includeContracts": true,
  "contractsPerBuyer": 3
}
```

Back comes one row per job ID you give (`recordType: job`), and with `includeContracts` one row per contract of each named client (`recordType: contract`) — the fields below are the ones this real row carried; every row has the same 50, and a field the source did not show arrives as null, not as a zero:

```json
{
  "recordType": "job",
  "asked": "https://www.upwork.com/jobs/~022112651010648303714",
  "upworkId": "022112651010648303714",
  "found": true,
  "title": "Video Intro Designer",
  "url": "https://www.upwork.com/jobs/~022112651010648303714",
  "datePosted": "2026-10-20T21:03:51.152000+00:00",
  "priceType": "Hourly",
  "clientLocation": "United States",
  "clientJobsPosted": 114,
  "clientHires": 86,
  "clientPublicSpend": 84893.26,
  "clientPaymentVerified": true,
  "buyerRef": "7cfdb2b6b69ad2760e3a9b47",
  "identityKnown": true,
  "company": "TutorJack",
  "domain": "tutorjack.com",
  "confidence": "high",
  "country": "United States",
  "city": "Colorado Springs",
  "spendOnRecord": 82971.57,
  "logoUrl": "https://www.google.com/s2/favicons?domain=tutorjack.com&sz=128",
  "identity": {"relations": [{"type": "identity_candidate", "entityName": "TutorJack", "entityDomain": "tutorjack.com", "status": "confirmed", "confidence": "high", "evidence": "Named  …,
  "contractsSummary": {"total": 48, "inProgress": 11, "completed": 37, "totalBilledUsd": 30108.41, "avgHourlyRate": 12.07, "maxHourlyRate": 73, "distinctFreelancers": 35, "avgRatingGiven": 5,  …,
  "contractsAsOf": "2026-10-20T21:10:34.612361+00:00"
}
```

The same actor, asked differently:

- **A column of IDs pasted from a spreadsheet, named clients only up to 20** — `{"jobIdsText": "022112651010648303714\n~022112654213757398440\nhttps://www.upwork.com/jobs/~022112636033867914441", "maxItems": 20}`

### What a named row carries

**The company, with the reason we believe it.** `company` and `domain` are the client behind the job, given only at high confidence. `identity` lists the evidence: the brands the client operates, the people who sign the postings, the clients they work for, and the candidates we ruled out, each with its status, its confidence and the words in the postings that gave it away. Measured 2026-10-20 over the 4,933 clients named on postings from the last 30 days: `identity` is filled for 51% of them; it is empty when the name came straight from the record and needed no research.

**What they have paid, before you write a proposal.** `contractsSummary` covers the contracts we hold for the client: how many, how many still running, the total billed, the average and highest hourly rate, how many freelancers, the ratings they gave and the ratings freelancers gave them, and what the running contracts bill now. Filled for 99% of named clients (2026-10-20). Upwork shows about the last 50 contracts on a client's profile, so for a client with hundreds of hires the summary covers the recent ones, and `totalBilledUsd` can be far under `clientPublicSpend`, which is Upwork's lifetime figure.

**Rate X-Ray: where the money went.** `contractsSummary.rateXray` splits what the client paid by kind of work, one of Upwork's 63 subcategories, with the contracts, the spend, the share of spend and the median hourly rate in each. The kind of work is read from the contract title by a classifier, so a contract can land in a neighbouring subcategory; the amounts are exact either way. 99.9% of the contracts of named clients carry a label (2026-10-20), and a new contract gets its label within about an hour.

**The reviews freelancers wrote about them.** With `includeContracts`, each contract is a row: the job title, hourly or fixed, the months it ran, the rate, the total charged, the hours, the freelancer, the kind of work, and both reviews. `ratingToClient` and `reviewToClient` are what the freelancer said about the client, the nearest thing to a reference for a client you are about to work for.

**How fresh the history is.** `contractsAsOf` says when we last read the client's contract history. We read it again each time the client posts a job, and we answer only postings from the last 30 days, so it is rarely older than that: for 95% of named clients the history was read at their latest posting, median age 15 days (2026-10-20). A contract that ended or got a review after that date shows as it was then.

**Every ID gets a row.** An ID we do not hold comes back `found: false`, a job we hold but cannot name comes back `identityKnown: false` with a `note`, and a job posted more than 30 days ago says so. A gap in your list is something you see, not something you work out.

**Be realistic about the hit rate.** We hold 3.4 million postings back to March 2024. About 13% of recent postings resolve to a named company; the rest come back unresolved, free. That is high enough to run across a shortlist and far too low to treat as a directory. An ID that is unresolved today can be answered on a later run.

**The second step of a pipeline, not a job search.** It does not browse or search Upwork. Any Upwork job scraper gives you IDs; this turns them into clients. Paste the IDs as they come: a bare ID, a `~`-prefixed ID, a job URL, a column of a spreadsheet or another Actor's `url` field all work.

### All 50 fields of a row

Every row carries all of them. A field the source did not show arrives as null, never as a zero or an empty string, and the note says when that happens.

#### The job you asked about

| field | type | what it holds |
|---|---|---|
| `recordType` | string | `job` for the row of an ID you gave, `contract` for a contract of a named client (with `includeContracts`). Join the two on `upworkId`. |
| `asked` | string | Exactly what you gave for this ID, so the row matches your own list. |
| `upworkId` | string | The job's Upwork ID, without the `~`. Null on a row of an ID the run's time limit did not reach. |
| `found` | boolean | True when we hold the job. False: not in the corpus, and the row says so in `note`. |
| `title` | string | The job's title as posted. |
| `url` | string | The job on Upwork. |
| `datePosted` | string | When Upwork published the job (UTC). A client is named only for a job from the last 30 days. |
| `priceType` | string | `Hourly` or `Fixed`, as the job states it. |
| `price` | number | The fixed price, when the job gives one figure (USD). |
| `priceMin` | number | Lower end of the hourly range (USD). |
| `priceMax` | number | Upper end of the hourly range (USD). |
| `note` | string | Why a row names nobody: not in the corpus, no high-confidence match, posted more than 30 days ago, or not reached by the run. |

#### The client as Upwork shows them

| field | type | what it holds |
|---|---|---|
| `clientLocation` | string | The client's country as Upwork shows it on the job. |
| `clientJobsPosted` | integer | Jobs the client has posted on Upwork, as the job shows it. |
| `clientHires` | integer | Freelancers the client has hired, as the job shows it. |
| `clientPublicSpend` | number | What Upwork shows as the client's lifetime spend (USD). |
| `clientPaymentVerified` | boolean | Whether Upwork shows the client's payment method as verified. |
| `buyerRef` | string | Our stable reference for the client, the same on every job of theirs and across runs. It is not an Upwork ID. |

#### The company behind the job

Filled only when `identityKnown` is true: a posting from the last 30 days whose client we name at high confidence.

| field | type | what it holds |
|---|---|---|
| `identityKnown` | boolean | True when the row names the company: a job from the last 30 days and a match at high confidence. Only these rows are charged. |
| `confidence` | string | How sure the match is (`high` on every named row). Null for a job older than 30 days. |
| `company` | string | The company behind the job. Null unless `identityKnown`. |
| `domain` | string | The company's web domain, when we hold one. |
| `country` | string | Where the company is, from our research. |
| `city` | string | The company's city, when we hold it. |
| `logoUrl` | string | A logo or favicon of the company that loads in a browser or a sheet. 64% of named clients (2026-10-20). |
| `employees` | string | Company size as the source states it, a count or a range. Text, about 3% of named clients. |
| `revenue` | string | Company revenue as the source states it. Text, about 3% of named clients. |
| `industry` | string | The company's industry, about 3% of named clients. |
| `founded` | integer | The year the company was founded, about 3% of named clients. |
| `linkedinUrl` | string | The company's LinkedIn page, about 3% of named clients. |
| `identity` | object | The evidence for the name: `relations` (each with `type`, `entityName`, `entityDomain`, `status`, `confidence`, `evidence`: the brands, people and clients the postings give away) and `ruledOut` (candidates we rejected, with why). Null when the name needed no research. |

#### What the client has paid

| field | type | what it holds |
|---|---|---|
| `spendOnRecord` | number | What the client has paid, as we hold it (USD). |
| `contractsSummary` | object | What the client has paid over the contracts we hold: `total`, `inProgress`, `completed`, `totalBilledUsd`, `avgHourlyRate`, `maxHourlyRate`, `distinctFreelancers`, ratings given and received, what the running contracts bill (`active…`), `asOf`, and `rateXray` (spend by kind of work with the median rate). |
| `contractsAsOf` | string | When we last read the client's contract history (UTC). It is read again each time the client posts a job. |

#### A contract row

Only with `includeContracts`, on rows with `recordType: contract`, most recent first, joined to the job row by `upworkId` and `buyerRef`.

| field | type | what it holds |
|---|---|---|
| `jobTitle` | string | The contract's job title. |
| `jobType` | string | `hourly` or `fixed`. |
| `status` | string | `in_progress` or `completed`. |
| `startMonth` | string | The month the contract started (`YYYY-MM`). |
| `endMonth` | string | The month it ended (`YYYY-MM`); null while it runs. |
| `hourlyRate` | number | The contract's hourly rate (USD); null on a fixed-price contract. |
| `totalCharge` | number | What the contract billed in all (USD). |
| `totalHours` | number | Hours billed; null on a fixed-price contract. |
| `freelancerName` | string | The freelancer's name as Upwork shows it (first name, last initial). |
| `freelancerUpworkId` | string | The freelancer's Upwork profile ID. |
| `subcategory` | string | One of Upwork's subcategories, read from the contract title by a classifier. |
| `ratingToFreelancer` | number | The rating the client gave the freelancer (1–5). |
| `reviewToFreelancer` | string | What the client wrote about the freelancer. |
| `ratingToClient` | number | The rating the freelancer gave the client (1–5). |
| `reviewToClient` | string | What the freelancer wrote about the client. |
| `contractsHeldForBuyer` | integer | How many contracts we hold for this client in all. |

### Ways to run it

**Paste the IDs you have.** Put job IDs or URLs into `jobIds`, one per line, or paste a column into `jobIdsText` (newlines, commas or spaces). Both lists are joined and duplicates removed. Another Actor's output works too: `jobUrls` and `startUrls` take a list of URLs or of objects with a `url` or `id`.

**Add the contracts.** Turn on `includeContracts`: each named client adds its contracts as rows, most recent first, up to `contractsPerBuyer` (25 by default, 200 at most). A client asked about twice in one run gets its contracts once. Contract rows are free.

**Cap the spend.** `maxItems` caps the named clients, and so the charge. Rows that name nobody and contract rows do not count. Without it, the cap is the number of IDs you gave.

**Run it after a job scraper.** Schedule your Upwork job scraper, then run this on the IDs it returned, by an integration or a task. The job scraper tells you what is posted; this tells you who posted it.

### Everything you can ask for

| input | accepts | default | what it changes |
|---|---|---|---|
| `jobIds` | `array` | — | One per line. A bare id, a ~-prefixed id, or the job URL — all three work. |
| `jobIdsText` | `string` | — | Paste a column straight from a spreadsheet or another Actor's output. Separated by newlines, commas or spaces — all fine. Combined with the list above, duplicates removed. |
| `includeContracts` | `boolean` | `false` | For each client we can name, add their contracts: what the work was, who did it, what it paid, hours billed, and the reviews in both directions. Free. |
| `contractsPerBuyer` | `integer` | `25` | Most recent first. Upwork's own profile shows the last 50; we hold more for a small number of heavy buyers. |
| `maxItems` | `integer` | — | Caps the named clients and therefore spend. A row is charged only once it is in your dataset. Rows that name nobody and contract lines are free and not counted. Defaults to the number of ids you gave. |

### If an input can't be used

The run still ends Succeeded and charges nothing. Its status message begins `INPUT REJECTED` and says what to change. The dataset holds a single row, `{"error": true, "code": …, "message": …}`, with no results, and the key-value record `ERROR` repeats it, so an agent should check `error` on the first row. A search that finds nothing is not an error, only an empty dataset. A run marked Failed is a fault on our side, not your input; it costs nothing and reaches us.

### Before you run it

**Is this Upwork's API?** No. It has no link to Upwork. It answers from a corpus of public Upwork postings and the client histories Upwork shows to anyone, joined to the company behind them. You bring the IDs; it never searches or browses Upwork for you.

**Why only jobs from the last 30 days?** A client is named for a posting from the last 30 days only. An older posting comes back `found: true` with no company and a `note` that says so, free. The window keeps the answer about a client who is hiring now.

**How sure is a name?** A company is returned only at high confidence. Below that the row says `identityKnown: false` and why, and carries no company. An honest "not sure" costs you nothing; a guess shown as a fact could cost you a contract. `identity` shows the evidence for every name it gives.

**Why does a contract have no rate, no hours or no review?** Upwork does not show every number for every contract. Over the 184,946 contracts held for the clients named in the last 30 days (2026-10-20): `startMonth` and `freelancerName` 100%, `totalCharge` 91%, `totalHours` 63%, `ratingToClient` 56%, `ratingToFreelancer` 53%, `hourlyRate` 43%, `reviewToClient` 35%, `reviewToFreelancer` 31%. A fixed-price contract has no hours and no rate; a contract nobody reviewed has no rating. They arrive as null, not as zero.

**How do I get the rows out?** They land in the run's dataset: download them as JSON, CSV, Excel or XML from the run page, or read them over the Apify API. Save a run as a task, schedule it, and connect it to webhooks, Zapier, Make or Google Drive, or let an agent call the Actor over MCP. Join contract rows to their job row on `upworkId`, and the same client across runs on `buyerRef`.

**Who makes this, and what else is there?** [Moxlade](https://moxlade.com) — corpora your agent can ask. Its other actors are the [Upwork freelancer census](https://apify.com/moxlade/upwork-freelancers), the [Vinted sellers database](https://apify.com/moxlade/vinted-users), the [Instagram email scraper](https://apify.com/moxlade/instagram-email-scraper), the [Reddit scraper](https://apify.com/moxlade/reddit-scraper) and the [Idealista scraper](https://apify.com/moxlade/idealista-scraper).

### What it costs

**One price per named client, contracts included.** You pay `named_buyer` once for each job ID answered with the company behind it, at high confidence. The evidence, the summary of what the client paid and, when you ask for them, the contract rows come with it at no extra charge. No start fee, no minimum, no platform usage billed to you, and an empty run is free. The price falls with your Apify plan.

| per named client | Free | Starter | Scale | Business |
|---|---|---|---|---|
| Named client (`named_buyer`) | $0.10 | $0.085 | $0.07 | $0.05 |

Free: a job we do not hold, a job whose client we cannot name at high confidence, a job posted more than 30 days ago, an ID the run's time limit did not reach, and every contract row. A row is charged only once it is in your dataset. `maxItems` counts the named clients only, so it is your spending cap: 50 IDs with `maxItems: 10` cost at most $1.00 on the Free plan. Enterprise plans have their own rates; the Actor's Pricing tab shows the price for your plan.

| event | what one event is | per 1,000 |
|---|---|---|
| `named_buyer` | One Upwork job ID answered with the company behind it, at high confidence, with the evidence, the summary of what the client paid and, on request, the contract rows. Rows that name nobody and contract rows are free. Charged only once the row is in your dataset. | $100.00 |

### Who asks this

**Freelancers and agencies on Upwork** — who the client is before you spend connects on a proposal: their company and site, what they pay per hour by kind of work, and what other freelancers wrote about working for them.

**Sales teams that sell to Upwork clients** — the companies behind the jobs your scraper already collects, with a domain to enrich and a spend history to rank them.

**Agents and data pipelines** — one call per batch of up to 5,000 IDs, JSON rows with a stable `upworkId` and `buyerRef`, a free row for every ID it cannot answer.

### Questions

support@moxlade.com

*Every figure on this page was measured on our own runs or read from our own tables, on the date given next to it.*

# Actor input Schema

## `jobIds` (type: `array`):

One per line. A bare id, a ~-prefixed id, or the job URL — all three work.

## `jobIdsText` (type: `string`):

Paste a column straight from a spreadsheet or another Actor's output. Separated by newlines, commas or spaces — all fine. Combined with the list above, duplicates removed.

## `includeContracts` (type: `boolean`):

For each client we can name, add their contracts: what the work was, who did it, what it paid, hours billed, and the reviews in both directions. Free.

## `contractsPerBuyer` (type: `integer`):

Most recent first. Upwork's own profile shows the last 50; we hold more for a small number of heavy buyers.

## `maxItems` (type: `integer`):

Caps the named clients and therefore spend. A row is charged only once it is in your dataset. Rows that name nobody and contract lines are free and not counted. Defaults to the number of ids you gave.

## Actor input object example

```json
{
  "jobIds": [
    "https://www.upwork.com/jobs/~022112651010648303714"
  ],
  "includeContracts": false,
  "contractsPerBuyer": 25
}
```

# Actor output Schema

## `results` (type: `string`):

This run's rows, read from its default dataset. One row per job ID you give (`recordType: job`), and with `includeContracts` one row per contract of each named client (`recordType: contract`):

## `runSummary` (type: `string`):

How the run went: IDs asked, in the corpus and named, rows delivered and free, the charges by event, and why it stopped when it stopped early.

# 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 = {
    "jobIds": [
        "https://www.upwork.com/jobs/~022112651010648303714"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("moxlade/upwork-buyer-intel").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 = { "jobIds": ["https://www.upwork.com/jobs/~022112651010648303714"] }

# Run the Actor and wait for it to finish
run = client.actor("moxlade/upwork-buyer-intel").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 '{
  "jobIds": [
    "https://www.upwork.com/jobs/~022112651010648303714"
  ]
}' |
apify call moxlade/upwork-buyer-intel --silent --output-dataset

```

## MCP server setup

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

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/rSKCmYMwnagBzeEua/builds/qA0ianzG4oZ34QOM4/openapi.json
