The Mine Works
← All posts
tutorial October 8, 2026 · 10 min read Updated October 8, 2026

Track New Shine.com Jobs for a Role Every Day

Get only the Shine.com jobs posted since your last check, for one role and city, using freshness sort, monitor mode and a daily schedule.

Try the scraper

The actor referenced in this article. Pay only for results delivered.

View the scraper →

Shine.com is one of India’s larger job boards, and a lot of its hiring moves fast: staffing consultancies post a role, collect applications for a couple of weeks and take it down. If you check the site by hand once a week, you see the postings that have been sitting there longest and miss the ones that went up yesterday. Shine’s own email alerts go to one inbox in one format, and they do not give you a spreadsheet you can filter, share or feed into anything else.

Try it live: Shine.com Jobs Scraper. Pay per result delivered. Failed and empty results are never charged.

The Shine.com Jobs Scraper has two settings built for exactly this job. sortBy: "freshness" puts the newest postings first, and monitorMode: true remembers what it has already sent you and delivers only jobs you have not seen. Put both on a daily schedule and you get a short list of new jobs each morning, and you pay only for that list.

What you get and who it is for

Each run returns one row per new job, with up to 22 fields:

FieldWhat it holds
job_idShine’s job id, the key monitor mode uses
title, companyThe job title as posted, and the employer or agency
locations, locationThe cities as a list, and the same list as one string
experience_min_years, experience_max_yearsShine’s experience text parsed into numbers
salary_min_lakhs, salary_max_lakhsPay in lakhs a year, only when the recruiter shows it
salary_textPay as Shine shows it, or [Salary Hidden]
skillsShine’s own keyword tags for the job
posted_date, posted_days_ago, expiry_dateHow old the posting is and when it closes
apply_urlThe job on shine.com

This is for job seekers who want to apply in the first day or two instead of the third week, recruiters and staffing firms watching who else is hiring for the roles they fill, placement teams at colleges tracking openings in one city, and anyone building a job alert bot for a niche role that general alerts bury. If that bot is going to cover more boards than Shine, read which boards allow scraping and how they differ first.

The input

{
  "searchKeywords": ["python developer"],
  "location": "Bangalore",
  "experienceLevel": "3to5",
  "employmentType": "regular",
  "sortBy": "freshness",
  "maxJobs": 100,
  "monitorMode": true
}
  • sortBy: "freshness" is what makes a daily feed work. The default, relevance, mixes new and old postings. In our September 30, 2026 run of this search with relevance sorting, the median job was 14.5 days old and 35 of 100 had been posted more than 30 days earlier. In our freshness test with a data analyst search, all 40 jobs returned had been posted that same day, against an average age of 5.3 days for the same search sorted by relevance.
  • monitorMode: true skips every job id the actor has already delivered to you. The first run returns up to maxJobs. Every run after that returns only new ids. Keep the input the same from run to run; the actor’s own note on this setting says it requires the same input across runs.
  • maxJobs is a ceiling per run, not a target. Set it above the number of new jobs you expect in a day so a busy day is not cut short. The hard cap is 1,000. Leave it unset and you get only 5, which is the default.
  • location takes one city. 39 large cities, such as Bangalore, Delhi NCR, Gurugram and Kochi, match Shine’s location filter exactly. Other names go through Shine’s “jobs in city” search instead. Leave it out for all of India.
  • experienceLevel and employmentType are optional. They take Shine’s own values: lt1, 1to2, 3to5, 6to8, 9to10, 11to15 or gt15 for experience, and regular, contractual, internship or work-from-home for employment type.
  • searchKeywords can hold several keywords. Each runs as its own Shine search, and a job found under two keywords is delivered and charged once.

What the output looks like

[
  {
    "job_id": "19654312",
    "title": "Senior Python Developer",
    "company": "Integrated Personnel Services Limited",
    "locations": ["Bangalore", "Pune"],
    "experience_min_years": 5,
    "experience_max_years": 10,
    "experience_text": "5 to 10 Yrs",
    "salary_min_lakhs": 1,
    "salary_max_lakhs": 14,
    "employment_type": "regular",
    "shift_type": "full-time",
    "skills": ["python", "django", "microservices", "rest api", "fastapi"],
    "apply_url": "https://www.shine.com/jobs/senior-python-developer/integrated-personnel-services-limited/19654312",
    "posted_date": "2026-09-30T12:16:21.000Z",
    "posted_days_ago": 1,
    "expiry_date": "2026-10-15T12:16:19.000Z"
  },
  {
    "job_id": "19659417",
    "title": "Python Developer",
    "company": "CAREERGUIDE",
    "locations": ["Bangalore", "Chennai", "Hyderabad"],
    "experience_min_years": 3,
    "experience_max_years": 8,
    "salary_text": "[Salary Hidden]",
    "employment_type": "regular",
    "shift_type": "full-time",
    "skills": ["python", "sql", "rest api", "authentication", "tableau", "streamlit"],
    "apply_url": "https://www.shine.com/jobs/python-developer/careerguide/19659417",
    "posted_date": "2026-09-30T15:47:08.000Z",
    "posted_days_ago": 1,
    "expiry_date": "2026-10-15T15:47:06.000Z"
  }
]

These two rows come from our October 2, 2026 run of python developer in Bangalore with the 3to5 experience band, regular, full time, 10 jobs. That run used relevance sorting and had monitor mode off, so it is not the exact input above, but the fields are the same. We trimmed the description, location and scraped_at, and left salary_text out of the first row (it holds Shine’s own Lakh/Yr text next to the parsed numbers). Rows that name a recruiter carry a recruiter_name field, which is often a person’s name; it is not shown here.

Every run also ends with a free summary row, marked with _type. With monitor mode on, it tells you what was skipped. In our monitor mode test on September 30, 2026 (data analyst, Hyderabad, 15 jobs), the second run reported previously_seen: 15, skipped_duplicates: 15 and new_this_run: 15: it passed over the 15 jobs from the first run and delivered 15 new ones. Drop any row with a _type field to keep jobs only.

Run it in Apify Console

  1. Open https://apify.com/themineworks/shine-jobs-scraper and click Try for free.
  2. In Search keywords, enter the role, one keyword per line.
  3. Type the city in Location, or clear it for all of India.
  4. Set Experience level and Employment type, or set both to Any. The form comes prefilled with 3 to 5 years, regular and full time, so change those if they do not fit.
  5. Set Sort by to Freshness (newest first) and Max jobs to 100.
  6. Tick Monitor mode (only new results), click Save as a new task, then Start.
  7. The first run fills the Output tab with up to 100 jobs. Run the task again later and you get only jobs posted since.

Run it from Python

pip install apify-client
import csv
import json
from datetime import date
from pathlib import Path
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("themineworks/shine-jobs-scraper").call(run_input={
    "searchKeywords": ["python developer"],
    "location": "Bangalore",
    "experienceLevel": "3to5",
    "employmentType": "regular",
    "sortBy": "freshness",
    "maxJobs": 100,
    "monitorMode": True,
})

# Recruiters often repost the same role under a new job_id.
# Keep a small file of title + company pairs already seen.
seen_path = Path("shine_seen_pairs.json")
seen = set(json.loads(seen_path.read_text())) if seen_path.exists() else set()

new_jobs, summary = [], None
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item.get("_type") == "summary":
        summary = item
        continue
    if "_type" in item:
        continue  # the free info row
    pair = f'{item["title"].strip().lower()}|{item["company"]}'
    if pair in seen:
        continue
    seen.add(pair)
    new_jobs.append({
        "posted_date": item["posted_date"][:10],
        "title": item["title"],
        "company": item["company"],
        "cities": ", ".join(item.get("locations", [])),
        "experience": item.get("experience_text", ""),
        "pay_lakhs": (f'{item["salary_min_lakhs"]} to {item["salary_max_lakhs"]}'
                      if "salary_max_lakhs" in item else item.get("salary_text", "")),
        "skills": ", ".join(item.get("skills", [])[:8]),
        "expires": item["expiry_date"][:10],
        "apply_url": item["apply_url"],
    })

if new_jobs:
    out = Path(f"shine_new_{date.today().isoformat()}.csv")
    with out.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=list(new_jobs[0].keys()))
        writer.writeheader()
        writer.writerows(new_jobs)

seen_path.write_text(json.dumps(sorted(seen)))
if summary:
    print(f'Shine returned {summary["jobs_scraped"]} new ids, '
          f'{summary.get("previously_seen", 0)} already seen before this run')
print(f"{len(new_jobs)} new jobs after dropping reposts")
for job in new_jobs[:10]:
    print(f'{job["posted_date"]}  {job["title"]}  at  {job["company"]}  ({job["pay_lakhs"]})')

The script writes one CSV per day with only that day’s new jobs, newest first, and prints the first ten. The repost check matters on Shine: in our 100-job run on September 30, 2026, the 100 job ids covered only 87 distinct title and company pairs, because one staffing firm had posted the same Python Developer role several times.

Put it on a daily schedule

In Apify Console, open the task you saved, go to Schedules, click Create new, and set a cron expression such as 30 8 * * * with your time zone set to Asia/Kolkata. That runs the search at 8:30 every morning with the same input, which is what monitor mode needs.

To get the new jobs somewhere useful:

  • Use Apify’s Google Sheets integration to append each run’s dataset to a sheet, one tab per role.
  • Use the Apify app in Make, Zapier or n8n to post each new job to Slack, Telegram or email.
  • Add a webhook on the task that calls your own endpoint when a run succeeds, with the dataset id to fetch.

If you track several roles, put them in one task as several searchKeywords rather than in several monitor tasks. The memory of seen jobs is shared by every task in your account that runs this actor, so two tasks searching overlapping roles will split the overlap between them.

What it costs

Billing is per job delivered: $2.00 per 1,000 jobs on the Bronze plan, $1.75 on Silver, and $1.50 on Gold and above. There is no start fee and no charge per run. Jobs skipped by monitor mode, the same job found under two keywords, the summary and info rows, and empty searches are never charged.

That makes a daily feed cheap, because after the first run you pay only for what is new. Worked examples for the input above:

  • The first run delivers up to 100 jobs: $0.20 on Bronze.
  • If 30 new jobs turn up each day, a month is about 900 jobs: $1.80 on Bronze, about $1.58 on Silver and $1.35 on Gold.
  • If every daily run hits the 100-job ceiling, a month is 3,000 jobs: $6.00 on Bronze, $5.25 on Silver and $4.50 on Gold. If that happens, raise maxJobs, because you are probably missing jobs.
  • A day with nothing new costs nothing.

Limits worth knowing

  • Reposts count as new. Monitor mode keys on job_id, and a staffing firm reposting a role gets a new id. Drop repeats by title and company, as the script does.
  • Most listings come from agencies. In our September 30 run, 73 of 100 rows were posted by one consultancy as “G-Jobs Hiring For” a client, plus several other staffing firms. The client name is the part after “Hiring For”. If you want direct employers only, filter on company.
  • Pay is often hidden. About a third of Shine listings show pay. In the Bangalore python developer run, 14 of 100 did. Hidden pay reads [Salary Hidden] and the number fields are left out.
  • Read posted_date by day, not by hour. In the same run, 5 of 100 rows had a posted_date a few hours later than the row’s own scraped_at, which suggests the timestamp is India time with a UTC marker. Use posted_days_ago or the date part.
  • A listing can outlive its expiry date. 2 of 100 rows in that run were already past expiry_date when collected. Check it before you apply or alert.
  • One city per run. location takes a single city. Run one task per city, or leave it empty and filter locations yourself.
  • Changing the input resets the point of the feed. Monitor mode expects the same input every run. If you widen the search, the first run after the change can return a large batch of older jobs that are new to the memory.
Related Actor

Explore the scraper referenced in this article: inputs, outputs, and pricing, then run it on Apify.

Frequently asked questions

How does monitor mode know which jobs are new? +

It stores the Shine job_id of every job it has already delivered to you, up to 50,000 ids, in your own Apify account, and skips those ids on the next run. You are charged only for the jobs it delivers.

Can I run two different daily searches with monitor mode? +

Yes, but the memory belongs to the actor, not to one task. A job delivered by your first search will not be delivered again by the second, so if both searches can return the same listing, it lands in only one of them.

Why do I sometimes get the same job twice under different ids? +

Recruiters and staffing firms often post the same role several times, and Shine gives each post its own job_id. Monitor mode keys on job_id, so a repost counts as new. The Python script in this guide also drops repeats of the same title and company.

Is there a fee per run? +

No. This actor has no start fee. You pay only per job delivered, so a daily run that finds nothing new costs nothing.

Apify Store

Find a ready-made scraper for your job

Apify has over 80,000 scrapers and automations, ours included. Start free with $5 of platform credit every month.