# Salary & Talent Market Benchmark (`thirdwatch/salary-benchmark`) Actor

Produce one salary benchmark per role and location — p25, median and p75, top employers, top skills and remote share — aggregated from live public job and compensation sources, with a stated sample size and confidence grade.

- **URL**: https://apify.com/thirdwatch/salary-benchmark.md
- **Developed by:** [Thirdwatch](https://apify.com/thirdwatch) (community)
- **Categories:** AI, Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Salary & Talent Market Benchmark

Get a defensible salary benchmark for a role in a market — 25th percentile,
median, 75th percentile, top employers, top skills and remote share — computed
from live public compensation and job-listing data, with the sample size and a
confidence grade printed next to every number.

This is **not** a job-listing scraper. One run returns **one benchmark record
per role and location**, not a thousand job rows. The value is in the
composition: pooling salary observations from several independent public
sources, normalising them to a single currency and a single annual period, and
refusing to publish a number when the evidence is too thin.

### What you get

One row per role + location:

| Field | Description |
|---|---|
| `role` | The role you asked for |
| `location` | City/region benchmarked, or empty for nationwide |
| `country` | Market code (`in`, `us`, `uk`, …) |
| `currency` | Reporting currency, fixed by country. Never mixed |
| `sampleSize` | Number of usable salary observations behind the percentiles |
| `p25` | 25th percentile annual salary |
| `median` | 50th percentile annual salary |
| `p75` | 75th percentile annual salary |
| `min` / `max` | Lowest and highest observation in the sample |
| `topEmployers[]` | Employers appearing most often in the sample, with counts |
| `topSkills[]` | Skills most associated with the role, with mention counts |
| `remoteShare` | Share of listing-based observations that are remote-friendly |
| `sourcesUsed[]` | Sources that actually contributed observations |
| `sourcesAttempted[]` / `sourcesFailed[]` | Full audit trail of what was tried |
| `observationsPerSource` | Observation count per source |
| `medianPerSource` | Median per source, so you can see cross-source agreement |
| `referenceMedians` | The source's own published median, where one exists |
| `confidence` | `low` / `medium` / `high` |
| `salaryPeriod` | Always `year` — everything is normalised to annual |
| `generatedAt` | UTC timestamp |

### Sources

| Source | Coverage | What it contributes |
|---|---|---|
| **Adzuna** | Global (11 markets) | Job listings that publish an explicit salary or salary range |
| **AmbitionBox** | India | Individual, dated, employer-attributed CTC reports plus employer and skill context |
| **Naukri** | India | Job listings where the employer discloses the salary band, plus role skills |

`topSkills` comes from AmbitionBox's role skill list and Naukri's per-listing
skill tags. Both are filtered against an explicit denylist of state, city and
job-type tags, because those sources mix SEO boilerplate ("Karnataka",
"Remote jobs", "Full time") into the same field as real skills. Adzuna
contributes salary observations only — its refinement rail is facet navigation,
not skills, so no skill signal is taken from it.

The Actor degrades gracefully. A benchmark built from **two of three sources is
still valid and billable**. Zero sources is not — that run emits nothing.

India has the deepest coverage because two of the three sources are India-only.
Outside India the benchmark runs on Adzuna alone, and the `confidence` field
reflects that honestly.

### The statistical rules this Actor will not break

These exist because a salary benchmark that is quietly wrong is worse than no
benchmark at all — someone makes an offer with it.

1. **Minimum sample size.** No benchmark is emitted below `minSampleSize`
   (default 8) usable observations. Below the threshold the role produces no
   row and **is not charged**.
2. **One currency per benchmark.** Observations denominated in anything other
   than the market's currency are excluded, never converted at some unstated
   FX rate.
3. **Everything is annualised before the maths.** Hourly, daily, weekly and
   monthly figures are converted to an annual basis (40h × 52w for hourly,
   260 working days, 12 months) *before* percentiles are computed. Mixing
   periods is the single most common way public salary numbers go wrong.
4. **Hidden salaries are dropped, not guessed.** Most Indian job listings hide
   the band. Inferring a number for those would invent the majority of the
   sample.
5. **Confidence reflects agreement, not just volume.** 200 observations from a
   single source do not earn a `high` grade. Cross-source disagreement caps
   the grade downward.
6. **Implausible values are filtered.** Anything outside a wide sanity band is
   a parsing artefact (a reference number read as a salary), not a wage.

### Input

```json
{
  "roles": ["data scientist", "backend engineer"],
  "location": "Bengaluru",
  "country": "in",
  "experienceLevel": "any",
  "sources": ["adzuna", "ambitionbox", "naukri"],
  "minSampleSize": 8,
  "pagesPerSource": 3
}
```

| Field | Type | Default | Notes |
|---|---|---|---|
| `roles` | array of strings | — | **Required.** One benchmark per role |
| `location` | string | `""` | Empty means nationwide |
| `country` | enum | `in` | Fixes the reporting currency |
| `experienceLevel` | enum | `any` | `any` / `entry` / `mid` / `senior` |
| `sources` | array enum | all three | Sources not covering the country are skipped |
| `minSampleSize` | integer | `8` | Suppression threshold |
| `pagesPerSource` | integer | `3` | Listing pages read per paginated source |
| `proxyConfiguration` | object | residential | Used for AmbitionBox and Adzuna; Naukri is never proxied |

#### About the proxy

Leave the proxy on. AmbitionBox and Adzuna both refuse datacenter IP ranges —
AmbitionBox drops the connection outright — so without a residential proxy the
run falls back to Naukri alone and the sample collapses (65 observations to 8
in a measured comparison). The proxy country is set from `country`
automatically, because an in-country exit matters for all three sources.
Naukri is deliberately never proxied: it serves its public API to plain HTTP,
so proxying it would add cost and a failure mode for no benefit.

### Output example

```json
{
  "role": "data scientist",
  "location": "Bengaluru",
  "country": "in",
  "currency": "INR",
  "experienceLevel": "any",
  "sampleSize": 61,
  "p25": 920000.0,
  "median": 1610000.0,
  "p75": 2280000.0,
  "min": 410000.0,
  "max": 6400000.0,
  "topEmployers": [{ "name": "TCS", "observations": 4 }],
  "topSkills": [{ "skill": "Python", "mentions": 2 }],
  "remoteShare": 0.12,
  "sourcesUsed": ["adzuna", "ambitionbox", "naukri"],
  "observationsPerSource": { "adzuna": 8, "ambitionbox": 44, "naukri": 9 },
  "medianPerSource": { "adzuna": 1500000.0, "ambitionbox": 1655000.0, "naukri": 1400000.0 },
  "confidence": "high",
  "salaryPeriod": "year",
  "generatedAt": "2026-08-03T09:14:22.000000+00:00"
}
```

### Pricing

Pay per event. You are charged **only for a benchmark row that is actually
delivered** — after it has been pushed to the dataset, and only when it cleared
the minimum sample size. Suppressed roles, failed roles and empty runs cost
nothing.

| Event | FREE | BRONZE | SILVER | GOLD |
|---|---|---|---|---|
| `benchmark` — one delivered benchmark row | $0.25 | $0.22 | $0.19 | $0.15 |

### Who this is for

- **Recruiters and talent acquisition** — pricing a requisition before it goes
  to market, and defending the band to a hiring manager.
- **Compensation and HR teams** — periodic market checks across a role
  catalogue without buying a full survey subscription.
- **Founders and hiring managers** — a fast, sourced answer to "what does this
  role actually cost in this city?"
- **Talent-market analysts** — tracking percentile drift over time by
  re-running the same role set on a schedule.

### Notes and limits

- Percentiles use linear interpolation (the same method as numpy's default and
  most spreadsheets), so any number here can be reproduced from the raw sample.
- `remoteShare` is computed only over listing-derived observations, because
  employee-reported CTC records carry no work-mode signal. It is `null` when
  no listing-based observation was collected.
- Salary data reflects what employers and employees publish publicly. Markets
  where most employers hide the band will show smaller samples and a lower
  confidence grade — that is the honest reading, not a defect.
- Runs on 256 MB with no browser, so the run cost stays low even across a large
  role catalogue.

# Actor input Schema

## `roles` (type: `array`):

Job titles to benchmark. One benchmark row is produced per role for the selected location. Use the plain market title, e.g. 'data scientist', not an internal job code.

## `location` (type: `string`):

City or region to benchmark within the selected country. Leave empty for a nationwide benchmark.

## `country` (type: `string`):

Market to benchmark. This fixes the reporting currency — observations in any other currency are excluded rather than converted. India has the deepest coverage because AmbitionBox and Naukri are India-only.

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

Narrow the benchmark to an experience band where the source supports it. 'Any' pools all experience levels and gives the largest sample.

## `sources` (type: `array`):

Which public salary sources to pool. Sources that do not cover the selected country are skipped automatically. A benchmark built from two of three sources is still valid.

## `minSampleSize` (type: `integer`):

A benchmark is emitted only when at least this many usable salary observations were collected. Roles below the threshold produce no row and are not charged — percentiles over a handful of points are worse than no number at all.

## `pagesPerSource` (type: `integer`):

How many listing pages to read from each paginated source. More pages means a larger sample and a slower run.

## `proxyConfiguration` (type: `object`):

AmbitionBox and Adzuna both refuse datacenter IPs, so residential proxy in the benchmarked country is used for them by default and is strongly recommended — without it the sample shrinks sharply. Naukri is fetched over plain HTTP and is never proxied.

## Actor input object example

```json
{
  "roles": [
    "product manager"
  ],
  "location": "Bengaluru",
  "country": "in",
  "experienceLevel": "any",
  "sources": [
    "adzuna",
    "ambitionbox",
    "naukri"
  ],
  "minSampleSize": 8,
  "pagesPerSource": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

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

No description

# 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 = {
    "roles": [
        "data scientist",
        "backend engineer"
    ],
    "location": "Bengaluru",
    "sources": [
        "adzuna",
        "ambitionbox",
        "naukri"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thirdwatch/salary-benchmark").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 = {
    "roles": [
        "data scientist",
        "backend engineer",
    ],
    "location": "Bengaluru",
    "sources": [
        "adzuna",
        "ambitionbox",
        "naukri",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("thirdwatch/salary-benchmark").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 '{
  "roles": [
    "data scientist",
    "backend engineer"
  ],
  "location": "Bengaluru",
  "sources": [
    "adzuna",
    "ambitionbox",
    "naukri"
  ]
}' |
apify call thirdwatch/salary-benchmark --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thirdwatch/salary-benchmark"
        }
    }
}

```

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/8WWw1gZFx72KpZa0b/builds/84gTzAD0NrbfoYj3C/openapi.json
