# Shine Jobs Scraper — India Jobs with Recruiter Emails & Phones (`memo23/shine-jobs-scraper`) Actor

Scrape Shine.com jobs across India with the hiring recruiter's real email and phone — Shine's fake placeholder contact is detected and stripped, so every contact you get is genuine. Filter by experience, salary, industry and employment type. Schedule it for only-new postings.

- **URL**: https://apify.com/memo23/shine-jobs-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 3 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job or recruiter scrapeds

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?

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 Jobs Scraper — India Jobs with Recruiter Emails & Phones

Scrape jobs from [Shine.com](https://www.shine.com), one of India's largest job boards, and get the **hiring recruiter's email address and phone number** on every posting that publishes one — not as a paid enrichment step, but because Shine puts them in the search results themselves.

<p align="center">
  <img src="https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-shine.png" alt="How the Shine Jobs Scraper works" width="900" />
</p>

### Why use this scraper

Most job scrapers give you a title, a company and a link, and leave you to find a human to contact. Shine attaches the hiring recruiter's contact details to the posting itself, and this Actor returns them wherever the recruiter published them — verified real, never a placeholder.

Measured across eight keywords and 480 jobs on 2026-09-05: **a real recruiter email on 43% of postings, a phone on 38%.**

Those numbers are lower than they first appear, and deliberately so. Shine does not return an empty field when a recruiter opts out of publishing contact details — it substitutes a working-looking Gmail address and a valid-looking mobile number, the same pair on 56% of jobs. This Actor detects that pair and returns `null` instead, so **every contact in your output is a real one**. See the FAQ.

That makes it two products in one. As a **job scraper** it gives you salary, experience, skills, industry, locations and the full description. As a **lead source** it gives you a named recruiter with an email and a phone, filterable by industry and city.

### Supported inputs

| Input | What it does |
|---|---|
| **Search keywords** | `["python", "sales manager"]` — each becomes a Shine search |
| **Location** | Restrict every keyword to one city, e.g. `Bangalore` |
| **Start URLs** | Paste `shine.com/job-search/...` URLs you already built |
| **Maximum results** | Row cap. 20 jobs per request, so 100 results is 5 requests |
| **Years of experience** | Jobs suited to a candidate with that much experience |
| **Minimum salary** | In lakh per year, e.g. `12` |
| **Industry** | One of Shine's 26 industries, by name — `BFSI`, `Medical / Healthcare` |
| **Functional area** | One of 37 job functions — `Testing`, `Finance / Accounts / Tax` |
| **Employment type** | Regular, Contractual, Internship or Work from home |
| **Sort order** | Shine's relevance ranking, or newest first |
| **Posted within (days)** | Only recent postings, and stop paging once past the window |
| **Only new jobs since the last run** | For schedules — returns just what you haven't had |
| **Also scrape each job page** | Adds apply counts, numeric salary, state and tags — see below |
| **One row per recruiter** | Group by contact instead of by job — see below |
| **Only jobs with a contact** | Drop postings with no email and no phone |
| **Skip descriptions** | Smaller exports when you only need the structured fields |

### Use cases

**Recruitment lead generation** — pull every recruiter hiring for a skill in a city, with a working email and phone, then filter by industry.

**Talent market research** — track salary bands, required experience and in-demand skills across thousands of postings per keyword.

**Competitive hiring intelligence** — see which consultancies are staffing which industries, how many roles each is running, and how recently they posted.

**Job board aggregation** — feed Shine listings into your own board or ATS with a stable, typed schema.

### Monitoring: only what's new

Run this on a schedule with **Only new jobs since the last run** on, and each run returns the postings you haven't received yet instead of the same feed again. The first run delivers everything it finds and becomes the baseline; after that a daily run on a keyword like `python` returns the day's new jobs rather than 30,000 rows you already have.

Two things it gets right that are easy to get wrong. A job is remembered only once it has actually been **delivered**, so anything cut off by your result limit still arrives on the next run rather than being marked seen and lost. And a run that finds nothing new **succeeds** and says so in the log — it doesn't report a failure that would invite a pointless retry.

Use **Monitoring state key** to keep separate schedules apart (`nursing-delhi`, `python-blr`), and **Forget everything and start over** after changing keywords, so the old memory doesn't hide jobs that now match.

### One row per recruiter

Indian recruitment runs through consultancies that post the same contact against many vacancies. Across 480 jobs that was **1.8 postings per recruiter**, with the busiest holding 8; a single `sales manager` run collapsed 60 jobs into 38 recruiters.

Paying per row for the same email eight times is waste. Switch on **One row per recruiter** and the output collapses to one row per contact:

```json
{
  "recruiterEmail": "niyukti.nmc@gmail.com",
  "recruiterPhone": "9990622996",
  "companyName": "Niyukti Management Consultants",
  "companyId": "166042",
  "jobCount": 7,
  "jobTitles": ["Candidate Required For MNC Insurance Sales Manager salary 4Lac", "…"],
  "locations": ["Bareilly", "Varanasi", "Dehradun", "Kanpur", "Lucknow", "…"],
  "industries": ["Insurance"],
  "latestPostedDate": "2026-09-05T18:23:56",
  "jobIds": ["19506520", "19506522", "19506523", "…"]
}
```

`latestPostedDate` is what separates an active recruiter from a stale listing.

### How it works

1. **Search** — each keyword (plus optional city and any filters) becomes a Shine search URL.
2. **Read the page's own JSON** — Shine is a Next.js site that ships its full result set inside the page, so there is no per-job request and no browser needed.
3. **Page through** — 20 jobs per page, in parallel batches, stopping at your limit or the end of the results.
4. **Optionally open each job page** — only when **Also scrape each job page** is on, and only for rows that survived your filters and your limit.
5. **Emit** — one row per job, or one per recruiter if grouping is on.

You can also paste job URLs straight into **Start URLs** (`shine.com/jobs/...`) to scrape specific postings. Those always come back with the full field set.

### Output

| Field | Description |
|---|---|
| `jobId` | Shine's job identifier |
| `jobTitle` | Job title |
| `companyName` / `companyId` | Hiring company |
| `recruiterEmail` | **Recruiter's email address** |
| `recruiterPhone` | **Recruiter's phone number** |
| `salary` | Salary band as published, e.g. `Rs 9 - 12 Lakh/Yr` |
| `experience` | Experience band, e.g. `5 to 10 Yrs` |
| `locations` | Array — a job may list several cities |
| `industry` | Industry category |
| `skills` | Array of required skills |
| `jobDescription` / `jobDescriptionHtml` | Description as text and as HTML |
| `postedDate` / `closingDate` | ISO timestamps |
| `jobUrl` | Link to the posting |
| `vacancies` | Openings for this posting |
| `employmentType` | Regular, Contractual, Internship or Work from home |
| `isPremium` | Whether the posting is boosted — useful for filtering recruiter spam |
| `searchKeyword` | Which search produced the row |

#### With **Also scrape each job page** switched on

These come from the job's own page, so they cost one extra request per row.

| Field | Description |
|---|---|
| `applyCount` | **How many people have already applied.** Observed from 0 on a fresh posting to 991 on a saturated one |
| `salaryMin` / `salaryMax` | Salary band as integers in ₹/year, so you can sort and filter on it |
| `locationStates` | Each city paired with its state, e.g. `Bangalore → Karnataka` |
| `companyDescription` | The employer's own description of itself |
| `tags` | Shine's category tags for the role |
| `functionalArea` / `roleCategories` | Shine's higher-level job taxonomy |

### Sample output

One row, exactly as the Actor emitted it with **Also scrape each job page** switched on:

```json
{
  "jobId": "19086960",
  "jobTitle": "Developers (.NET / Java / Python) React.js & Ai Exposure B'lore & Hyd 2+Yrs",
  "companyName": "WHITE HORSE MANPOWER CONSULTANCY (P) LTD",
  "recruiterEmail": "whmshineprofiles@gmail.com",
  "recruiterPhone": "8884572014",
  "salary": "Rs 4.0  - 6 Lakh/Yr",
  "salaryMin": 400000,
  "salaryMax": 600000,
  "experience": "2 to 8 Yrs",
  "locations": ["Hyderabad"],
  "locationStates": [{ "location": "Hyderabad", "state": "Telangana" }],
  "industry": "IT Services & Consulting",
  "employmentType": "Regular",
  "skills": ["Java", "Spring Boot", "NET", "Python", "Microservices", "ReactJS", "REST APIs"],
  "applyCount": 991,
  "postedDate": "2026-06-05T15:26:05",
  "vacancies": 99,
  "isPremium": true,
  "jobUrl": "https://www.shine.com/jobs/developers-net-java-python-reactjs-ai-exposure-blore-hyd-2yrs/white-horse-manpower-consultancy-p-ltd/19086960",
  "searchKeyword": "python-jobs"
}
```

Without that option the row is the same minus `applyCount`, `salaryMin`, `salaryMax`, `locationStates`, `companyDescription`, `tags`, `functionalArea` and `roleCategories`. `jobDescription` and `jobDescriptionHtml` are omitted above for length.

### FAQ

**Do I need a proxy?**
No — the Actor handles it. It requests directly first, which is fastest, and automatically retries through a residential exit when Shine withholds the data. You can supply your own proxy in the Advanced section if you'd rather route all traffic yourself, but you don't need to.

**Does monitoring cost extra?**
It saves. The Actor still reads the search pages, but only new jobs become rows — and rows are what you pay for. On a daily schedule that's the difference between paying for the whole feed every day and paying for the handful that are new.

**Are the filters applied by Shine or after the fact?**
By Shine, on its own server, so a filtered run reads far fewer pages rather than downloading everything and discarding most of it. Experience, salary, industry, functional area, employment type and sort order are all Shine's own search parameters. Industry and functional area are matched against the live list Shine returns for your search, so a partial name works and the run logs exactly what it matched.

**How does "Posted within days" work if Shine has no date filter?**
It doesn't have one, so the Actor sorts newest-first and walks until **two** pages in a row fall outside your window. Two rather than one because Shine's first page carries promoted listings that sit outside the sort — on a live test, page 1 was entirely stale while page 2 held jobs posted that morning. On a keyword with 1,500 pages and a 2-day window this still costs only a handful of requests. If your filters exclude everything, the run says so and finishes successfully rather than reporting a failure.

**What does "Also scrape each job page" actually add?**
Six fields the search listing doesn't carry, the useful one being `applyCount` — how many candidates have already applied. It also gives salary as integers rather than as the string `"Rs 9 - 12 Lakh/Yr"`. The trade is speed: the Actor goes from one request per 20 jobs to one request per job.

**Can I combine it with One row per recruiter?**
No. A recruiter row covers many jobs, and an apply count or a salary band belongs to one of them, so the option is ignored in that mode and the run says so in the log.

**Can it reveal a hidden salary?**
Sometimes. About 61% of postings publish `salary` as `[Salary Hidden]`, and for roughly 3 in 10 of those the job page still returns a real band. The rest fall back to what looks like Shine's floor placeholder of 200000–500000, so treat a number as informative only where `salary` itself was hidden and the band isn't that default.

**Do I need to log in?**
No. Everything scraped here is public.

**How many jobs can I get?**
Popular keywords run deep — `python` returns over 30,000 jobs across 1,500+ pages, `sales` over 65,000. Your result limit is the practical ceiling.

**Is the recruiter email always there?**
No, and be careful with any tool that claims it is. Across 480 jobs, a genuine email was on 43% and a phone on 38%. Coverage is Shine's to decide and varies a lot by keyword — `sales manager` and `accountant` publish contacts far more often than `python`. Use **Only jobs with a contact** if you need every row to have one.

**What is the placeholder contact you strip?**
When a recruiter opts out of publishing their details, Shine's search results don't return an empty field — they return `shinetest12345@gmail.com` and `8765243096`, the same pair every time, on 56% of the jobs we sampled. The job's own page is honest about it and says `hidden_email` / `hidden_mobile` for exactly those postings, which is how we identified the substitution. Left in, it would mean mailing one dead address on most of your rows, **Only jobs with a contact** keeping everything because every row "has" a contact, and one-row-per-recruiter collapsing hundreds of jobs into a single phantom. This Actor returns `null` for both fields instead.

**Why do the same email and company repeat?**
Recruitment consultancies post many vacancies under one contact. Use **One row per recruiter** to collapse them.

### Support

Found a bug or need another India job board? Open an issue on the Actor's **Issues** tab, or email <muhamed.didovic@gmail.com>.

### Explore more scrapers

Other India and Gulf job boards: **Naukri**, **Instahyre**, **iimjobs**, **CutShort**, **Foundit**, **Internshala**, **Naukrigulf**.

### 🤖 For AI Agents & LLM Apps

**Purpose.** Search Shine.com (India) and return structured job rows, including the hiring recruiter's real email and phone where published.

**Minimal tested input**

```json
{ "keywords": ["python"], "location": "Bangalore", "maxItems": 50 }
```

**Output fields** (one row per job): `jobId`, `jobTitle`, `companyName`, `companyId`, `recruiterEmail`, `recruiterPhone`, `salary`, `experience`, `locations`, `industry`, `employmentType`, `skills`, `jobDescription`, `jobDescriptionHtml`, `postedDate`, `closingDate`, `jobUrl`, `vacancies`, `isPremium`, `searchKeyword`.

With `scrapeJobDetails: true`, each row also carries `applyCount`, `salaryMin`, `salaryMax`, `locationStates`, `companyDescription`, `tags`, `functionalArea`, `roleCategories` — at the cost of one request per job instead of one per twenty.

With `uniqueRecruiters: true` the shape changes to one row per contact: `recruiterEmail`, `recruiterPhone`, `companyName`, `companyId`, `jobCount`, `jobTitles`, `locations`, `industries`, `latestPostedDate`, `jobIds`.

**Billing.** Pay-per-event: one charge per row written to the dataset, plus one per run start. Fewer rows means a smaller bill, so use `requireContact`, `postedWithinDays` and `monitorMode` to narrow before paying.

**Behaviours worth knowing**

- `recruiterEmail` and `recruiterPhone` are `null` on roughly 57% of jobs. That is correct: Shine substitutes a fixed fake pair for opted-out recruiters and this Actor strips it. Do not treat `null` as a scrape failure.
- An empty dataset can be a valid result — with `monitorMode` or `postedWithinDays`, "nothing new" and "nothing recent" both finish as SUCCEEDED and explain themselves in the log.
- `monitorMode` keeps state across runs in a named key-value store, so repeated calls with the same `monitorStateKey` return only what you have not already received.
- Filters are applied by Shine's own search, not after the fact, so narrowing costs fewer requests rather than more.

### ⚠️ Disclaimer

This Actor collects only data that Shine.com publishes publicly, without logging in and without bypassing any access control. Recruiter contact details are published by Shine and the recruiters themselves as part of each job advert, for the purpose of being contacted about that role.

You are responsible for how you use the output. If you contact recruiters, applicable law may apply to you — including India's DPDP Act, and the GDPR or ePrivacy rules where recipients are in the EU/EEA or UK. Honour opt-outs, and do not use this data for unsolicited bulk marketing unrelated to recruitment. This Actor is not affiliated with, endorsed by, or connected to Shine.com or HT Media.

### SEO Keywords

shine.com scraper, shine jobs scraper, india job scraper, indian job board scraper, recruiter email scraper, recruiter contact scraper, job leads india, shine job listings api, bangalore jobs scraper, mumbai jobs scraper, delhi jobs scraper, india recruitment leads, hiring data india, job posting scraper india, salary data india, tech jobs india scraper, consultancy contacts india, apify shine actor

# Actor input Schema

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

What to search for on Shine.com, e.g. "python", "sales manager", "staff nurse". Each keyword becomes a Shine search that is paged through.

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

Restrict every keyword to one city, e.g. "Bangalore", "Mumbai", "Delhi". Leave empty to search all of India. Ignored for URLs pasted into Start URLs.

## `startUrls` (type: `array`):

Shine.com search URLs to scrape as-is, e.g. https://www.shine.com/job-search/python-jobs-in-bangalore. Use this instead of keywords when you have already built the search on Shine.

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

Stop after this many rows. Each Shine search page yields 20 jobs, so 100 results is 5 requests.

## `maxPagesPerKeyword` (type: `integer`):

Safety cap on how deep to page for each keyword, regardless of the result limit. Popular keywords run to 1,500+ pages.

## `minExperienceYears` (type: `integer`):

Filter to jobs suited to a candidate with this many years of experience. Shine applies this as a band rather than a floor: 3 returns "0 to 4 Yrs" roles, 15 returns "8 to 12" and "10 to 14". Values outside 2–25 return nothing.

## `minSalaryLakh` (type: `integer`):

Only jobs paying at least this many lakh rupees per year. Measured on a python search: 12 cuts 30,000 jobs to 3,770 and 25 cuts it to 922. Note that Shine keeps listings with a hidden salary in the results at every threshold, so some rows still read "\[Salary Hidden]" — switch on “Also scrape each job page” to get a numeric band for many of those.

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

Restrict to one Shine industry, e.g. "IT Services & Consulting", "BFSI", "Medical / Healthcare", "Education / Training", "BPO / Call Center". Matched against the live list Shine returns for your search, so partial names work and the run logs what it matched. A numeric Shine industry id is also accepted.

## `functionalArea` (type: `string`):

Restrict to one job function, e.g. "Testing", "Finance / Accounts / Tax", "Network / System Administration", "Teacher / Tutor". Matched the same way as Industry. Narrower than industry, so pair it with a broad keyword.

## `employmentType` (type: `string`):

Keep only one kind of contract. Work-from-home and internship listings are a small fraction of Shine, so expect far fewer results than an unfiltered search.

## `sortBy` (type: `string`):

Shine’s own ranking, or newest postings first. Newest-first is what makes a date window cheap, so setting one switches this automatically.

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

Keep only jobs posted in the last N days. Shine has no date filter of its own, so the Actor sorts newest-first and stops paging as soon as a whole page falls outside the window — a 1-day window on a 1,500-page keyword reads a handful of pages, not all of them.

## `scrapeJobDetails` (type: `boolean`):

Fetch every job’s own page as well as the search listing. The search page already carries the full description, recruiter email and phone, so this is only for the extra fields a job page adds: applyCount (how many people applied — observed 0 to 991), numeric salaryMin/salaryMax, each city mapped to its state, the employer’s company description, and Shine’s category tags. Costs one request per job instead of one per 20, so runs take longer. Job URLs pasted into Start URLs get these fields regardless. Has no effect when “One row per recruiter” is on, since these fields describe one job.

## `uniqueRecruiters` (type: `boolean`):

Group jobs by recruiter contact instead of returning one row per job. Indian recruitment consultancies post the same contact against many vacancies (measured ~3.2x), so this turns the output into a de-duplicated contact list with job counts, titles, locations and industries attached.

## `requireContact` (type: `boolean`):

Skip any job that has neither a recruiter email nor a phone number. Measured over 480 jobs, a genuine email is on 43% of postings and a phone on 38%, so this typically removes more than half the results — switch it on when every row must be actionable, leave it off to see the whole market. Shine’s placeholder contact for opted-out recruiters is already stripped, so this filter never keeps a row on the strength of a fake address.

## `skipDescriptions` (type: `boolean`):

Leave out jobDescription and jobDescriptionHtml. Descriptions are the largest part of each row, so turning them off makes exports much smaller.

## `enrichEmails` (type: `boolean`):

When on, each job without a recruiterEmail is looked up by company name + city to attach contact\_email, contact\_website and an emailEnrichment object. Adds time per uncovered row and is charged per enriched record.

## `monitorMode` (type: `boolean`):

Remember which jobs have already been delivered and return only ones you have not received before. Built for a schedule: the first run delivers everything it finds and becomes the baseline, and every run after it returns just that day’s new postings, so you stop paying for the same feed over and over. A run that finds nothing new finishes successfully and says so — that is the mode working, not a failure. Only jobs actually delivered are remembered, so anything cut by your result limit still arrives next time.

## `monitorStateKey` (type: `string`):

Separate memories for separate schedules. Two schedules on different keywords should use different keys, e.g. "nursing-delhi" and "python-blr", or each will treat the other’s jobs as already seen. Leave as "default" if you run only one schedule.

## `resetMonitorState` (type: `boolean`):

Wipe the remembered jobs before this run, so it delivers everything again and becomes the new baseline. Use it after changing keywords or filters, when the old memory no longer matches what you are asking for.

## `proxy` (type: `object`):

The Actor starts direct and falls back to its own residential exit when Shine withholds the data, so you do not need to set anything here. Supply a proxy only if you want all traffic routed through your own.

## Actor input object example

```json
{
  "keywords": [
    "python"
  ],
  "maxItems": 100,
  "maxPagesPerKeyword": 500,
  "employmentType": "any",
  "sortBy": "relevance",
  "scrapeJobDetails": false,
  "uniqueRecruiters": false,
  "requireContact": false,
  "skipDescriptions": false,
  "enrichEmails": false,
  "monitorMode": false,
  "monitorStateKey": "default",
  "resetMonitorState": false
}
```

# Actor output Schema

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

One row per job with title, company, recruiter email and phone, salary band, experience, skills, industry, locations, description and posting URL.

# 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 = {
    "keywords": [
        "python"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/shine-jobs-scraper").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 = { "keywords": ["python"] }

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

```

## MCP server setup

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

```

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/4OAi6cIS6xxKGy1uw/builds/nhXxufzn8SPqTGmz7/openapi.json
