# Internshala Scraper - India Jobs & Internships, Salary (`neverempty/internshala-jobs`) Actor

Every job and internship on Internshala with the fields the listing hides: pay as numbers with currency and period (annual CTC for jobs, monthly stipend for internships), experience, the deadline as a real date, skills, duration and the full description. A posting delisted mid-run is never charged.

- **URL**: https://apify.com/neverempty/internshala-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.46 / 1,000 posting 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

## Internshala Scraper - India Jobs & Internships, Salary

Pull **jobs and internships** from **Internshala** with the fields the listing page does not give you: **pay as two numbers with its currency and period**, **years of experience as a number**, and the **application deadline as a real date** - plus skills, duration, start date, perks and the full description.

Jobs carry an annual CTC. Internships carry a monthly stipend. Both land in the same columns with `salaryPeriod` set to `year` or `month`, so a monthly figure is never mistaken for a yearly one.

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

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

***

### Why this one is different

#### A delisted job is an `expired` row, and it is never charged

Jobs come off Internshala while a run is going. Measured on 2026-09-03: a job that was in the listing returned **HTTP 404** a few minutes later.

Most scrapers parse that page anyway and hand you a row where every field is `null` - and charge you for it. This Actor checks the response first and splits the result three ways:

| `status` | What it means | Charged? |
|---|---|---|
| `ok` | The job page was read | **Yes** |
| `expired` | The job is no longer listed (HTTP 404) | **No** |
| `unparsable` | The page could not be read (any other status, or the job block is missing) | **No** |

You always know which of the three you got. An empty result and a broken fetch are never the same row.

#### Salary comes back as numbers, not as a string

The page shows `₹ 4,22,000 - 6,18,000 /year`. You get:

```json
{ "salaryMin": 422000, "salaryMax": 618000, "salaryCurrency": "INR", "salaryPeriod": "year",
  "salaryRaw": "₹ 4,22,000 - 6,18,000 /year" }
```

Indian digit grouping (`4,22,000`) is parsed correctly. A monthly figure comes back as `"salaryPeriod": "month"` - it is never silently reported as a yearly one. **When the page has no figure, you get `null`, not `0`.** The raw string is always kept so you can check the parsing yourself.

#### The deadline is a date you can sort on

`Apply By 11 Oct' 26` becomes `"applyBy": "2026-10-11"`. If the page writes it in a form this Actor cannot read, `applyBy` is `null` and `applyByRaw` still holds the original text. **No date is ever guessed.**

#### It returns this job's values, not the neighbour's

An Internshala job page carries similar jobs on the same HTML. Measured: one page had **4 different salary blocks and 4 company names** in it. This Actor reads only the job's own block, so the salary you get belongs to the job in the row.

The same applies to skills: `Learn MS-Excel`, `Learn Digital Marketing` are Internshala's own course adverts sitting next to the real skill list. They are not returned as required skills.

#### A city Internshala does not have tells you so

Measured on 2026-09-03: `/jobs/bangalore/`, `/bangalore-jobs/` and `/jobs/in-bangalore/` all return **HTTP 200** while quietly ignoring the city - all 50 results are identical to the unfiltered listing, starting with a job in Mumbai. Only `/full-time-jobs-in-<city>/` actually filters.

Worse: a city that does not exist at all (`atlantis`, `zzzznotacity`) also answers **HTTP 200** and returns the unfiltered list. Waiting for a 404 never tells you.

So this Actor checks the result instead of the status code: it fetches the unfiltered listing once and compares. If your filtered page comes back as the same jobs, you get **one `no-listing` row saying the filter does not exist**, and it is not charged - you are never handed all-India jobs while thinking you filtered by city.

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `listing` | select | `jobs` | 9 listings, each checked against the live site: `jobs`, `internships`, `work-from-home-jobs`, `remote-jobs`, `fresher-jobs`, `part-time-jobs`, and the three internship equivalents |
| `city` | string | — | **Jobs only.** e.g. `bangalore`, `delhi`, `mumbai`. Takes priority over the others. Internshala publishes no working city filter for internships, so it is not offered there |
| `category` | string | — | **Jobs only.** e.g. `marketing`, `computer-science`, `finance`. Used when `city` is empty |
| `maxJobs` | integer | 30 | 1-1000. One job = one row = one charged event |
| `includeDescription` | boolean | `true` | The responsibilities/requirements text. Same price either way |
| `maxPages` | integer | 0 | Safety cap on listing pages. 0 = decide from `maxJobs` |
| `keywords` | array | — | Keep only listings containing one of these words |
| `keywordMatch` | string | `any` | `any` or `all` |
| `excludeKeywords` | array | — | Drop listings containing one of these |
| `companyNames` | array | — | Keep only these companies (partial match) |
| `minSalary` | integer | 0 | **A listing with no published pay is kept** |
| `requireSalary` | boolean | `false` | Drop listings that do not state the pay |
| `postedWithinDays` | integer | 0 | Apply-by within N days. **An unreadable date is kept** |
| `monitoringMode` | boolean | `false` | Return only listings not returned on a previous run |
| `resetMonitoringState` | boolean | `false` | One-shot: forget what was already returned |

#### Filters are applied before you are charged

The filters narrow what was already read - **nothing extra is fetched from Internshala** - and a listing that does not match is never delivered and never charged. If a search returns listings but none match your filters you get **one unpaid row saying exactly that** (`no-filter-match`), so you can tell "the filter was too tight" apart from "there is nothing there".

`minSalary` **keeps** a listing whose pay is not stated rather than treating it as low. Remember that jobs carry a **yearly CTC** and internships carry a **monthly stipend** - `salaryPeriod` tells you which, so set the threshold with the listing type you chose in mind.

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

Schedule it with `monitoringMode: true` and each run returns **only the listings it has not returned before**. Already-seen listings are skipped **before their pages are opened**, so a repeat run is fast as well as free.

⚠️ Measured on 2026-09-04: **Internshala reorders its listing between requests** - the first page is not stable. So the first few monitoring runs return listings that are new *to you* but not necessarily newly posted, as the shuffle brings different postings into the window you scan. It settles: four consecutive runs with `maxJobs: 6` returned 6, 1, 3, then **0**. Use a larger `maxJobs` if you want it to settle sooner.

The minimal call is an empty input: it returns 30 jobs from the full listing.

***

### Output columns

| Column | Example |
|---|---|
| `jobId` | `1788172448` |
| `postingType` | `job` or `internship` |
| `url` | the job page |
| `status` | `ok` / `expired` / `unparsable` / `no-listing` |
| `title` | `Business Development Manager` |
| `company` | `Swing Saga` |
| `location` | `Ghaziabad, Greater Noida, Delhi, Noida, Gurgaon` |
| `salaryMin` / `salaryMax` | `422000` / `618000` |
| `salaryCurrency` / `salaryPeriod` | `INR` / `year` |
| `salaryRaw` | `₹ 4,22,000 - 6,18,000 /year` |
| `experienceYears` / `experienceRaw` | `1` / `1 year(s)` |
| `duration` | `4 Months` (internships) |
| `applyBy` / `applyByRaw` | `2026-10-11` / `11 Oct' 26` |
| `startDate` | `Starts Immediately` |
| `skills` | `["Business Development", "Client Interaction", ...]` |
| `perks` | `["Informal dress code", "5 days a week"]` |
| `postedRaw` | `Posted 2 days ago` |
| `description` | the full responsibilities text |
| `scrapedAt` | ISO timestamp of the fetch |

***

### Measured, not estimated

Everything above was checked against the live site on 2026-09-03, not read off documentation:

- **Listing paging**: 8 pages in a row, all HTTP 200, no rate limiting, 329 job URLs, about 1 second per page.
- **The 404 case**: a job present in the listing returned HTTP 404 minutes later. That is where the `expired` row comes from.
- **The silent-filter case**: `/jobs/bangalore/` returned the unfiltered listing with HTTP 200 (50 of 50 URLs identical to `/jobs/`). `/full-time-jobs-in-bangalore/` returned 42 of 50 in Bangalore.
- **The neighbour-jobs case**: one job page contained 4 salary blocks and 4 company names.

The Actor ships with **22 checks** run against saved copies of real pages (a job page, an internship page, a delisted page and a listing page), and a **mutation suite of 20 deliberate breakages - all 20 are caught**. Breakages that the checks do *not* catch are listed in `test/mutate.mjs` with the reason, rather than quietly counted as passes.

***

### Typical uses

- Track which companies in India are hiring, and at what CTC band.
- Build a salary benchmark by role, city or experience level - the numbers are already parsed.
- Watch a category (`marketing`, `computer-science`) and alert on new postings before the deadline.
- Feed a job board or a newsletter with fresh postings that still have an open `applyBy` date.
- Compare fresher vs experienced pay for the same title.

***

### Notes and limits

- Internshala publishes Indian jobs. Salaries are in INR.
- `perks` is returned only when the job page has a perks block; otherwise it is an empty array. Nothing is filled in to make the row look complete.
- Fresher postings usually have no experience line at all. Those rows come back with `experienceYears: null`, not `0` - "no requirement stated" and "zero years required" are different answers.
- `postedRaw` is Internshala's own wording (`Posted 2 days ago`). It is not converted to a date, because the site does not publish an exact timestamp and this Actor does not guess one.
- The listing pages are read as a visitor sees them. `https://internshala.com/robots.txt` does not restrict them.

***

### Other tools by NeverEmpty

- **[Job Listing API - Career Site Jobs from Any ATS](https://apify.com/neverempty/ats-jobs-api)** - open roles from company career sites on Greenhouse, Lever, Ashby, Workable and Workday.
- **[ATS Job Board Finder](https://apify.com/neverempty/ats-board-finder)** - turn a company domain into the job board it actually uses.

### Support

Found a row that looks wrong? Open an issue on the Actor page with the `url` from the row - the raw fields (`salaryRaw`, `applyByRaw`, `experienceRaw`) are kept precisely so a mistake can be checked against the page.

# Actor input Schema

## `listing` (type: `string`):

Which Internshala listing to read. Every option here was checked against the live site: it returns different results from the unfiltered listing. Ignored when you set a city or a category below (those are jobs only).

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

Jobs only. Filter by city, for example `bangalore`, `delhi`, `mumbai`, `pune`, `hyderabad`. Reads https://internshala.com/full-time-jobs-in-<city>/. A city Internshala does not have returns a single no-listing row telling you so - it never silently falls back to all jobs. Internshala publishes no working city filter for internships, so this field is ignored when Listing is an internship one.

## `category` (type: `string`):

Jobs only. Filter by category, for example `marketing`, `computer-science`, `finance`, `human-resources`. Reads https://internshala.com/<category>-jobs/. A category Internshala does not have returns a single no-listing row. Used only when City is empty.

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

How many postings to return at most. One posting = one row = one charged event. Postings that are no longer listed, and pages that could not be read, are returned as rows too but are never charged.

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

Default: true. The description is the responsibilities and requirements text from the job page. Turn it off for a smaller dataset - the price is the same either way.

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

Safety cap on how many listing pages to walk. 0 lets the Actor decide from Maximum jobs (about 40 jobs per page).

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

Keep only listings whose title, description, company, location, skills or perks contains one of these words. Case is ignored. Nothing extra is fetched from Internshala - this narrows what was already read, and listings that do not match are never charged.

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

`any` keeps a listing that contains at least one keyword. `all` keeps only listings that contain every keyword.

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

Drop any listing containing one of these words - for example `unpaid`, or a company you do not want. Applied after the keyword filter.

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

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

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

Keep only listings paying at least this much. Note that jobs carry a yearly CTC and internships carry a monthly stipend - `salaryPeriod` tells you which, so set this with the listing type you chose in mind. A listing with no published pay is kept, not dropped - an unknown amount is not a low one.

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

Default: false. Turn it on to drop listings where the stipend or salary was not stated.

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

Keep only listings whose application deadline is within the next N days. 0 means no limit. A listing whose deadline 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 listings it has not returned before. Already-seen listings are skipped before their pages are opened, so repeat runs are fast as well as free.

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

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

## Actor input object example

```json
{
  "listing": "jobs",
  "city": "",
  "category": "",
  "maxJobs": 30,
  "includeDescription": true,
  "maxPages": 0,
  "keywords": [],
  "keywordMatch": "any",
  "excludeKeywords": [],
  "companyNames": [],
  "minSalary": 0,
  "requireSalary": false,
  "postedWithinDays": 0,
  "monitoringMode": false,
  "resetMonitoringState": false
}
```

# Actor output Schema

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

One row per job: title, company, location, annual CTC as numbers with currency and period, years of experience, the application deadline as a date, skills, perks, start date and the full description. Delisted and unreadable jobs come back as their own rows and are not charged.

# 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 = {};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/internshala-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/01dQZi39HUzszMdIf/builds/IkphKvjJpXCdOtb8Q/openapi.json
