# Shine Scraper - India Jobs, Salary, Skills & Expiry (`neverempty/shine-jobs`) Actor

Every job on Shine.com with the salary as two numbers - published on 82% of listings, measured. Plus the skills asked for, the years of experience wanted, every city the role covers, the employer, the industry and the expiry date. Recruiter emails and phone numbers are not collected. Unofficial.

- **URL**: https://apify.com/neverempty/shine-jobs.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.83 / 1,000 job returneds

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?

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

## Shine Scraper - India Jobs, Salary, Skills & Expiry

Pull jobs from **Shine.com** — one of India's largest job boards — as clean JSON, with the part most job feeds leave out: **the salary, on 82% of listings**, as two numbers in INR per year.

*Unofficial. This Actor is not affiliated with, endorsed by, or sponsored by Shine.com. All product names are trademarks of their respective owners.*

No API key. Reads the same public pages a visitor sees.

***

### Why this one is different

#### Recruiter emails and phone numbers are removed, on purpose

Shine's own page data carries the recruiting contact's **personal email address on every listing** and a **mobile number on most of them** — and advertisers often repeat them inside the advert text (`Call & Drop Updated cv : 93108…  parveen…@gmail.com`).

This Actor **does not output those fields at all**, and it **removes email addresses and mobile numbers from the description text** as well. Job data, not a contact list. If you need contact data, use a tool that says so.

#### The salary is there, and it is honest about when it is not

Measured on 80 live listings on 2026-09-04: **66 of 80 (82%) publish a salary**. That is far more than most boards.

Shine writes it as text (`Rs 14 - 26 Lakh/Yr`) and writes the literal string **`[Salary Hidden]`** when the employer withheld it. Passing that through would sell you a salary of "\[Salary Hidden]", so:

- `[Salary Hidden]` becomes `null` — never a zero, never a string pretending to be a number
- `Rs 14 - 26 Lakh/Yr` becomes `salaryMin: 1400000`, `salaryMax: 2600000`, `salaryCurrency: "INR"`, `salaryPeriod: "year"` (1 Lakh = 100,000)
- **any other wording is left as raw text with the numbers `null`** — a form we have not verified is never guessed into numbers

#### Codes we have not verified are not translated

Shine marks employment type with a number. Across 80 listings the value was always `1`, and the job's own structured data said `FULL_TIME` — so `1` maps to `FULL_TIME` and **every other code comes back as `null` with the raw number in `employmentTypeCode`**. A second code (`engagementTypeCode`) is passed through untranslated because we could not confirm what its values mean.

#### One request, twenty complete jobs

Shine ships the full job — description, salary, location, experience, expiry — inside the listing page itself. This Actor reads all twenty from that single page, so **it never opens individual job pages**. 200 jobs costs 10 requests, not 210.

#### Every city the role covers

A role open in nine cities comes back with all nine in `locations`. The job's own detail page shows only the first one.

#### Dates you can actually compare

Shine writes timestamps without a timezone. Its structured data writes the same instant as `+05:30`, so this Actor attaches India Standard Time rather than letting your database read them as UTC and shift them by five and a half hours. `expiresAt` tells you whether a vacancy is about to close.

#### An empty search is an answer, not a silent fallback

A keyword Shine does not have returns **zero results with a total count of zero** — it never quietly falls back to every job. You get **one `no-results` row saying so, and it is not charged**. A page that could not be read at all is a different row (`unreadable`) carrying the HTTP status.

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `keyword` | string | `developer` | Job title or skill. Spaces become hyphens |
| `city` | string | — | `bangalore`, `mumbai`, `delhi`, `hyderabad`, `pune`… |
| `maxJobs` | integer | 20 | 1-1000. One job = one row = one charged event |
| `includeDescription` | boolean | `true` | Full advert text, HTML stripped, contacts removed |
| `keywords` | array | — | Keep only jobs containing one of these words |
| `keywordMatch` | string | `any` | `any` or `all` |
| `excludeKeywords` | array | — | Drop jobs containing one of these |
| `companyNames` | array | — | Keep only these employers (partial match) |
| `minSalary` | integer | 0 | INR per year. **A job with no published salary is kept** |
| `requireSalary` | boolean | `false` | Drop the ~18% that hide the salary |
| `postedWithinDays` | integer | 0 | 0 = no limit. **An unreadable date is kept** |
| `monitoringMode` | boolean | `false` | Return only jobs not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |
| `useProxy` | boolean | `true` | Route through an Apify proxy |
| `maxPages` | integer | 0 | Safety cap; 0 = decide from `maxJobs` (20 per page) |

```json
{ "keyword": "developer", "city": "bangalore", "maxJobs": 100,
  "keywords": ["python", "aws"], "keywordMatch": "all",
  "excludeKeywords": ["intern"], "minSalary": 1500000 }
```

#### Filters are applied before you are charged

The filters narrow the jobs already read — **nothing extra is fetched from Shine**, and a job that does not match is never delivered and never charged. If a search returns jobs but none match your filters, you get **one unpaid row saying exactly that**, so you can tell "the filter was too tight" apart from "the board is empty".

Two of them refuse to guess: a job with **no published salary is kept** by `minSalary` (an unknown salary is not a low one), and a job with an **unreadable posted date is kept** by `postedWithinDays`. Turn on `requireSalary` if you want the unknowns dropped.

#### Monitoring mode turns this into a daily feed

Schedule it and set `monitoringMode: true`: each run returns **only the jobs it has not returned before**. The first run returns everything and remembers it. Jobs that were read but filtered out are remembered too, so a later change of filters does not resurrect them as false "new" postings. You pay for new postings, not for the same list every morning.

Measured on 2026-09-04: `developer` 38,305 jobs nationwide and 4,923 in Bangalore, `accountant` 12,768, `nurse` 1,110.

***

### Output

One row per job:

```json
{
  "source": "shine",
  "jobId": "19506404",
  "url": "https://www.shine.com/jobs/backendsenior-csharp-developer/iquest-management-consultants-pvt-ltd/19506404",
  "title": "Backend (Senior C# Developer)",
  "company": "IQuest Management Consultants Pvt Ltd.",
  "companyId": 694125,
  "industry": "IT Services & Consulting",
  "locations": ["Bangalore", "Noida", "Chennai", "Hyderabad", "Gurugram", "Kolkata", "Pune", "Mumbai City", "Delhi"],
  "salaryText": "Rs 8 - 18 Lakh/Yr",
  "salaryMin": 800000,
  "salaryMax": 1800000,
  "salaryCurrency": "INR",
  "salaryPeriod": "year",
  "experienceText": "5 to 8 Yrs",
  "experienceMinYears": 5,
  "experienceMaxYears": 8,
  "skills": ["sql server", "t-sql", "performance tuning", "c#", ".net"],
  "employmentType": "FULL_TIME",
  "employmentTypeCode": 1,
  "engagementTypeCode": 2,
  "postedAt": "2026-08-28T17:20:00+05:30",
  "expiresAt": "2026-10-27T00:00:00+05:30",
  "description": "…",
  "status": "ok",
  "scrapedAt": "2026-09-04T05:20:05.658Z"
}
```

#### Rows that are never charged

| `status` | when |
|---|---|
| `no-results` | Shine has no jobs for this search. That is its answer, not a failure |
| `unreadable` | The page could not be read, or its embedded data was missing |

A page whose embedded data cannot be found is reported as **`unreadable`, never as "no jobs"** — so a redesign on Shine's side shows up as an error rather than as a quiet, permanent "there are no vacancies".

***

### Pricing

Pay per job returned. Rows that report an empty search or a page that could not be read are **not** charged. A job that appears twice is dropped before delivery, so it is never charged twice.

***

### Notes

- `robots.txt` was fetched on 2026-09-04: the `User-agent: *` section disallows `/api/*`, `/jobs/1*`, `/jobs/9*`, `/job-search/simple/` and similar. This Actor reads only `/job-search/<keyword>-jobs`, which is not disallowed.
- Only public pages are read. No login, no API key.

# Actor input Schema

## `keyword` (type: `string`):

What to search for: `developer`, `accountant`, `nurse`, `data analyst`. Spaces are turned into hyphens for you. Measured on 2026-09-04: developer 38,305 jobs, accountant 12,768, nurse 1,110. A word Shine does not have returns zero results with a total count of zero, so you get one unpaid row saying so instead of a silently unfiltered list.

## `city` (type: `string`):

Narrow to one city, for example `bangalore`, `mumbai`, `delhi`, `hyderabad`, `pune`. Measured: `developer` in Bangalore is 4,923 of the 38,305 nationwide. Leave empty for all of India.

## `maxJobs` (type: `integer`):

How many jobs to return at most. One job = one row = one charged event. Shine lists 20 jobs per page and this Actor reads all 20 from a single request, so 200 jobs costs 10 requests. A job that appears again is dropped, so it is never charged twice.

## `includeDescription` (type: `boolean`):

Default: true. The full advert text with HTML stripped and HTML entities decoded. Turn it off for a much smaller dataset - the price is the same either way.

## `keywords` (type: `array`):

Keep only jobs whose title, description, company, industry, skills or location contains one of these words. Case is ignored. Leave empty to keep everything. Nothing extra is fetched from Shine - this narrows the jobs already read, and jobs that do not match are never charged.

## `keywordMatch` (type: `string`):

`any` keeps a job that contains at least one keyword. `all` keeps only jobs that contain every keyword.

## `excludeKeywords` (type: `array`):

Drop any job containing one of these words - for example `intern`, `commission only`, a staffing agency you do not want. Applied after the keyword filter.

## `companyNames` (type: `array`):

Keep only jobs from employers whose name contains one of these (partial match, case ignored). Leave empty for all employers.

## `minSalary` (type: `integer`):

Keep only jobs paying at least this much. Shine publishes a salary on about 82% of listings. A job with no published salary is **kept**, not dropped - an unknown salary is not a low one. Turn on `requireSalary` if you want those dropped too.

## `requireSalary` (type: `boolean`):

Default: false. Turn it on to drop the roughly 18% of jobs that hide the salary.

## `postedWithinDays` (type: `integer`):

Keep only jobs posted in the last N days. 0 means no limit. A job whose posted date cannot be read is **kept**, not dropped.

## `monitoringMode` (type: `boolean`):

Default: false. Turn it on and schedule this Actor daily: each run returns only the jobs it has not returned before, so you pay for new postings instead of the same list again. The first run returns everything and remembers it. Jobs read but filtered out are remembered too, so they never come back as false 'new' ones.

## `resetMonitoringState` (type: `boolean`):

Default: false. Turn it on for a single run to forget which jobs were already returned and start monitoring again from scratch.

## `useProxy` (type: `boolean`):

Default: true. Runs go out through an Apify proxy so a shared datacenter address does not get in the way. Turn it off if you are running from a network Shine already accepts.

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

Safety cap on how many listing pages to walk. 0 lets the Actor decide from Maximum jobs (20 per page). The run stops early on its own when a page brings no new jobs.

## Actor input object example

```json
{
  "keyword": "developer",
  "city": "",
  "maxJobs": 20,
  "includeDescription": true,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "companyNames": [],
  "minSalary": 0,
  "requireSalary": false,
  "postedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false,
  "useProxy": true,
  "maxPages": 0
}
```

# Actor output Schema

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

One row per job: title, employer, industry, every city the role covers, the skills asked for, the years of experience wanted, the posted date and the expiry date, and the salary as two numbers in INR per year when the employer published one. Searches with no results and pages that could not be read come back as their own rows and are not charged. Recruiter emails and phone numbers are not collected.

# 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 = {
    "keyword": "developer"
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/shine-jobs").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 = { "keyword": "developer" }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/shine-jobs").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 '{
  "keyword": "developer"
}' |
apify call neverempty/shine-jobs --silent --output-dataset

```

## MCP server setup

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

```

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/xHViO4WdXy7bPldER/builds/eqcbKXbhsHJ5YcPUh/openapi.json
