# Foundit Scraper \[$2.50/1K💰] | India Jobs & Salary Data (`ahmed_jasarevic/foundit-scraper`) Actor

Build an India job database from Foundit (formerly Monster India) - title, company, city, INR salary, experience band, skills, taxonomy, full description and apply URL. Two-stage run, no browser.

- **URL**: https://apify.com/ahmed_jasarevic/foundit-scraper.md
- **Developed by:** [Ahmed Jasarevic](https://apify.com/ahmed_jasarevic) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Foundit Scraper

Build a **structured India job database** from [Foundit](https://www.foundit.in) (formerly Monster India) — job title, company, city, **INR salary**, experience band, skills, full taxonomy, complete description and direct apply URL. Built for recruiters, job-board aggregators, HR analytics, salary benchmarking and labor-market research.

Two-stage run: a fast search API fills the dataset first, then full descriptions are backfilled — so **your dataset is usable before the slow phase even starts**.

> **Read this before you run it.** Foundit sits behind Akamai bot protection that rejects datacenter IPs, and this Actor works around that block by design. That has consequences for you. See [Legal & Compliance](#legal--compliance) — it is not a formality.

### What you get

#### INR salary, normalised to a monthly figure

`salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPerMonthMin`, `salaryPerMonthMax`, `salaryText` and `salaryHidden`. Phase 2 reads Foundit's `baseSalary` from the page's schema.org `JobPosting` JSON-LD, which states the unit explicitly (`MONTH` or `YEAR`) — so annual postings get converted rather than silently compared against monthly ones. That is the difference between a salary dataset you can actually benchmark and one where every annual row looks 12× too high.

`salaryHidden` is a first-class flag, not a gap: it is true when the employer hid the figure **or** when no salary was published at all. Filter on it rather than treating `null` salary as zero.

#### Experience bands that don't lie

`experienceMinYears`, `experienceMaxYears`, `experienceText`. Foundit's own middleware API returns an **empty result set** for some experience filters (`experience=3-5` is a known case), which would silently give you zero jobs rather than an error. This Actor filters the experience band **client-side after parsing**, so a restrictive filter returns correctly filtered jobs instead of nothing.

#### The full taxonomy, already split into arrays

`skills`, `skillsWithSynonyms`, `designations`, `functions`, `industries`, `employerTypes`, `jobType`, `employmentTypes`, `workplaceType`. Every one of these is a list field, pre-split — not the pipe-delimited string Foundit returns. That means you can build a skills taxonomy or a remote-work filter without regex cleanup.

#### Application intelligence

`applyUrl`, `jobUrl`, `companyUrl`, `externalApply`, `recruiterName`, `postedBy`, `totalApplicants`, `urgentlyHiring`. `totalApplicants` is the competitive-demand signal: a role with 400 applicants in three days is a market-intelligence fact about that employer, not noise about the job. `urgentlyHiring` and `postedBy` help you separate direct employer postings from agency reposts.

#### Freshness you can filter on

`postedDate`, `lastUpdatedDate`, `postedRelative`, `validThrough`, `enrichedAt`, `scrapedAt`. `postedRelative` ("3 days ago") comes straight from the API for human-readable recency; `validThrough` is the closing date; `enrichedAt` separates phase-2 enrichment time from phase-1 collection time.

### How it works

Foundit's own front end calls an **unauthenticated JSON middleware endpoint**. The Actor calls exactly that same endpoint — this is first-party app traffic, not HTML scraping:

**Phase 1 — search**

```
GET https://www.foundit.in/middleware/jobsearch?keyword=…&locations=…&page=…
```

Returns the job objects directly. Merged across every keyword × location combination and **deduplicated by Foundit job ID**. Each result is pushed to the dataset immediately with `descriptionStatus: "pending"`.

**Phase 2 — descriptions**

```
GET <mirror>/job/<slug>-<jobId>?amp=1
```

Foundit's middleware payload carries summary data but **not** the full description, posting/expiry dates or base salary. Those only exist in a schema.org `JobPosting` JSON-LD block on the detail page. Phase 2 fetches those pages, extracts the JSON-LD, and **re-pushes the same `jobId` with the description merged in** and `descriptionStatus: "complete"` — so you get an updated record, not a duplicate.

#### Why phase 2 uses regional mirrors — and why that matters

`www.foundit.in` is fronted by Akamai Bot Manager, which applies aggressive bot protection and **403s most datacenter IP ranges**. The Actor therefore fetches detail pages from Foundit's own regional sibling domains, which serve identical job slugs with far weaker edge protection:

```
https://www.founditgulf.com  →  https://www.foundit.sg  →  https://www.foundit.in
```

They are tried **in order, least-protected first**, with `www.foundit.in` only as the last-resort fallback. On a blocked or missing page the Actor hops to the next mirror instead of burning retries on a dead host, and per-job attempt counts stop a dead slug from ever looping through the chain.

The same IP block is why the Actor **explicitly warns you at startup when you run without a proxy**, recommending the Apify **RESIDENTIAL** group. Running on datacenter IPs without one is the single most common reason for empty results.

#### Cost control built in

- `maxConcurrency` (default **5**) for phase 1, deliberately low to stay under Foundit's rate limits.
- `detailConcurrency` (default **15**) for phase 2 — these are static AMP pages on the least-protected mirrors, so they tolerate and benefit from much higher parallelism.
- Phase 2 is capped independently by `maxDescriptionItems` (default **200**), so a large search run doesn't inherit a proportionally large description bill.
- A request budget of `maxItems × 3 mirrors + 10` bounds the whole run.

### Input

| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| `keywords` | string\[] | `["software engineer"]` | Job titles, roles or skills. Each is searched separately, then merged and deduplicated. |
| `locations` | string\[] | `[]` | City or region filters — Bengaluru, Mumbai, Hyderabad, Pune, Delhi NCR. Empty searches all of India. |
| `experienceMin` | integer | `0` | Minimum years of experience. `0` = no minimum. |
| `experienceMax` | integer | `0` | Maximum years. `0` = no maximum. |
| `maxItems` | integer | `50` | Cap on job records in the dataset. |
| `maxPages` | integer | `5` | Safety limit on result pages per keyword/location combination. |
| `includeDescriptions` | boolean | `true` | Enable phase 2. `false` gives you the cheapest possible run. |
| `maxDescriptionItems` | integer | `200` | How many collected jobs get a full description in phase 2. `0` = no descriptions. |
| `maxConcurrency` | integer | `5` | Parallel phase-1 requests. Keep at 3–8. |
| `detailConcurrency` | integer | `15` | Parallel phase-2 requests. Safe higher than phase 1. |
| `proxy` | object | `{ "useApifyProxy": false }` | **Set this to RESIDENTIAL.** Datacenter IPs get 403. |

`maxDescriptionItems` defaults to 200 while `maxItems` defaults to 50, so with default input **every collected job gets a description**. Lower `maxDescriptionItems` if you're collecting a large volume and don't need all of them described.

### Output

One flat dataset of 44 fields per job. Records appear twice by design — once from phase 1 with `descriptionStatus: "pending"`, once from phase 2 with `descriptionStatus: "complete"` and the description merged. **Deduplicate on `jobId`, keeping the record with `descriptionStatus: "complete"`.**

| Group | Fields |
| --- | --- |
| Identity | `jobId`, `title`, `company`, `jobUrl`, `companyUrl`, `applyUrl`, `externalApply` |
| Location | `location`, `country` |
| Compensation | `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPerMonthMin`, `salaryPerMonthMax`, `salaryText`, `salaryHidden` |
| Experience | `experienceMinYears`, `experienceMaxYears`, `experienceText` |
| Taxonomy | `skills`, `skillsWithSynonyms`, `designations`, `functions`, `industries`, `employerTypes`, `jobType`, `employmentTypes`, `workplaceType` |
| Dates | `postedDate`, `lastUpdatedDate`, `postedRelative`, `validThrough` |
| Description | `description`, `descriptionHtml`, `descriptionStatus`, `enrichedAt` |
| Employer signal | `recruiterName`, `postedBy`, `totalApplicants`, `urgentlyHiring` |
| Provenance | `source`, `searchQuery`, `searchLocation`, `scrapedAt` |

Field notes that change how you read the data:

- `descriptionStatus` is `pending` after phase 1, `complete` after phase 2. A job may remain `pending` forever if it wasn't in `maxDescriptionItems` or no mirror served it — filter on `complete` if you need descriptions.
- `salaryPerMonthMin`/`salaryPerMonthMax` are populated **only** when Foundit states a `MONTH` unit. For `YEAR` postings you get the amount in `salaryText` and must convert it yourself. Check `salaryText` before assuming the monthly fields exist.
- `postedDate` and `lastUpdatedDate` from JSON-LD are in **DD-MM-YYYY**, not ISO. Reformat before loading to a database.
- `recruiterName` can be a real person's name — see [Legal & Compliance](#legal--compliance) before storing it.
- `searchQuery` and `searchLocation` record which input combination found the job. The same `jobId` can legitimately appear under several; that's your provenance trail.
- `skillsWithSynonyms` is broader than `skills` — use `skills` for strict matching and `skillsWithSynonyms` for recall.
- `descriptionHtml` is the raw HTML from the JSON-LD block, not page chrome.

### Example input

Standard recruiter run:

```json
{
  "keywords": ["software engineer", "backend developer"],
  "locations": ["Bengaluru"],
  "maxItems": 100
}
```

Salary research across Indian tech hubs, mid-level only:

```json
{
  "keywords": ["data scientist", "machine learning engineer"],
  "locations": ["Bengaluru", "Hyderabad", "Pune"],
  "experienceMin": 3,
  "experienceMax": 8,
  "includeDescriptions": false,
  "maxItems": 500
}
```

Cheapest possible run — no detail phase at all:

```json
{
  "keywords": ["accountant"],
  "locations": ["Mumbai"],
  "maxItems": 200,
  "includeDescriptions": false
}
```

Full-description run capped for cost:

```json
{
  "keywords": ["product manager"],
  "locations": ["Delhi NCR"],
  "maxItems": 300,
  "includeDescriptions": true,
  "maxDescriptionItems": 150
}
```

Nationwide sweep through a residential proxy:

```json
{
  "keywords": ["nurse", "teacher"],
  "locations": [],
  "maxItems": 1000,
  "maxPages": 10,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

### Example output

```json
{
  "jobId": "INV-3xj8k2mq9p4r",
  "title": "Senior Backend Engineer (Go)",
  "company": "Razorpay Software Pvt Ltd",
  "location": "Bengaluru, Karnataka",
  "country": "India",
  "salaryMin": 1800000,
  "salaryMax": 2600000,
  "salaryCurrency": "INR",
  "salaryPerMonthMin": 150000,
  "salaryPerMonthMax": 216666,
  "salaryText": "1,800,000 INR per year",
  "salaryHidden": false,
  "experienceMinYears": 4,
  "experienceMaxYears": 7,
  "experienceText": "4-7 years",
  "skills": ["Go", "Kubernetes", "PostgreSQL", "gRPC"],
  "skillsWithSynonyms": ["Go", "Golang", "Kubernetes", "K8s", "PostgreSQL", "Postgres", "gRPC"],
  "designations": ["Senior Software Engineer", "Backend Developer"],
  "functions": ["Engineering", "Information Technology"],
  "industries": ["Banking", "Financial Services", "Fintech"],
  "employerTypes": ["Full-time", "Regular"],
  "jobType": "fulltime",
  "employmentTypes": ["Full-time"],
  "workplaceType": "Hybrid",
  "postedDate": "2026-09-14",
  "lastUpdatedDate": "2026-10-01",
  "postedRelative": "20 days ago",
  "validThrough": "2026-11-14",
  "description": "We are looking for a Senior Backend Engineer to build payment infrastructure serving millions of transactions per day. You will design distributed services in Go, own service reliability, and mentor two mid-level engineers. Our stack is Go, Kubernetes and PostgreSQL.",
  "descriptionHtml": "<p>We are looking for a Senior Backend Engineer to build payment infrastructure…</p>",
  "descriptionStatus": "complete",
  "enrichedAt": "2026-10-04T06:14:02.118Z",
  "applyUrl": "https://www.foundit.in/jobs/senior-backend-engineer-go-inv-3xj8k2mq9p4r",
  "jobUrl": "https://www.foundit.in/jobs/senior-backend-engineer-go-inv-3xj8k2mq9p4r",
  "companyUrl": "https://www.foundit.in/company/razorpay-software-pvt-ltd",
  "externalApply": false,
  "recruiterName": "Priya Menon",
  "postedBy": "Razorpay Software Pvt Ltd",
  "totalApplicants": 214,
  "urgentlyHiring": true,
  "source": "foundit.in",
  "searchQuery": "software engineer",
  "searchLocation": "Bengaluru",
  "scrapedAt": "2026-10-04T06:12:41.220Z"
}
```

### Integrations & automation

Call it over the Apify API (`POST /v2/acts/ouNFgKxKFYizng7Rh/runs`), or from Python, JavaScript, the CLI or MCP. Output exports as JSON, CSV, Excel, XML or RSS.

For **recruiter pipelines**, run nightly on a keyword × city grid, dedupe on `jobId`, and alert on `postedRelative` of "today" or `urgentlyHiring`. For **salary benchmarking**, use `includeDescriptions: false` and rely on `salaryMin`/`salaryMax`/`salaryText` — you get pay bands for a fraction of the cost, and the phase-2 dates aren't needed for aggregation. For **employer intelligence**, group by `company` and compare `totalApplicants` growth against posting cadence.

Typical pipeline: run on schedule → dedupe on `jobId` keeping `descriptionStatus: "complete"` → normalise `salaryPerMonthMin`/`salaryMax` (converting `YEAR` yourself) → reformat `DD-MM-YYYY` dates → explode `skills` into a bridge table → upsert into your ATS or warehouse.

#### What a run costs

| Configuration | Results | Cost |
| --- | --- | --- |
| 50 jobs, `includeDescriptions: false` | 50 × result | **$0.13** |
| 100 jobs, descriptions on | 100 × result | **$0.25** |
| 500 jobs, `includeDescriptions: false` | 500 × result | **$1.25** |

**$0.0025 per result.** Phase 2 re-pushes the *same* job with a description merged in, and those re-pushes count as results — which is why a described run costs roughly double an undescribed one of the same size. To describe fewer jobs cheaply, lower `maxDescriptionItems` rather than `maxItems`. Free accounts get a trial quota; paying accounts get full `maxItems` and tiered rates.

### Related Actors

| Actor | Why |
| --- | --- |
| [`naukri-scraper`](https://apify.com/ahmed_jasarevic/naukri-scraper) | India's largest job board — the essential cross-check for salary and volume benchmarking against Foundit. |
| [`linkedin-jobs-scraper`](https://apify.com/ahmed_jasarevic/linkedin-jobs-scraper) | Corporate-side hiring signal, complementing a job-board-side dataset. |
| [`indeed-scraper`](https://apify.com/ahmed_jasarevic/indeed-scraper) | Global job data for international hiring-market comparison. |
| [`glassdoor-scraper`](https://apify.com/ahmed_jasarevic/glassdoor-scraper) | Company reviews and salary reports to pair with posting-level pay data. |
| [`ambitionbox-scraper`](https://apify.com/ahmed_jasarevic/ambitionbox-scraper) | Indian salary and career-intelligence data, useful for enriching Foundit records. |

### FAQ

#### I ran it without a proxy and got nothing. Why?

Because `www.foundit.in` is behind Akamai Bot Manager, which **403s most datacenter IP ranges** — which is exactly where Apify runs without a proxy. Set `proxy` to `{ "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }` and re-run. The Actor warns you explicitly at startup when it detects it's running without a proxy, because this is by far the most common cause of an empty run.

#### Why do some jobs appear twice?

By design. Phase 1 pushes every job with `descriptionStatus: "pending"`; phase 2 **re-pushes the same `jobId`** with the description, dates and salary merged in and `descriptionStatus: "complete"`. Deduplicate on `jobId` and keep the `complete` record. A job that never reached phase 2 stays `pending` permanently — filter on `descriptionStatus == "complete"` when you need descriptions.

#### Why did an experience filter return zero jobs when the jobs exist?

Because Foundit's middleware API returns an **empty payload** for some experience bands — `experience=3-5` is a known case — rather than an error. This Actor filters the experience band **client-side after parsing**, so you get correctly filtered results instead of an empty set. If you still get nothing, widen `experienceMax` or clear `locations`.

#### `salaryPerMonthMin` is null but `salaryText` shows a number. Why?

The monthly fields are populated **only when Foundit states a `MONTH` unit**. For a `YEAR` posting, the amount lands in `salaryText` and the monthly fields stay null. Always read `salaryText` before assuming the monthly fields exist, and divide `YEAR` figures by 12 yourself if you need a monthly comparison.

#### `salaryMin` is a string. Why?

Foundit's middleware returns salary as text (for example `"1,800,000"`), so it stays a string in `salaryMin`/`salaryMax` unless phase 2 replaced it from JSON-LD. Cast to number and strip commas before any arithmetic; `salaryPerMonthMin`/`salaryPerMonthMax` are already parsed numbers.

#### Why are the dates in DD-MM-YYYY?

Because they come straight out of Foundit's schema.org JSON-LD, which uses `DD-MM-YYYY`. Reformat before loading to anything date-typed, or your recency filters will silently misbehave.

#### Is there a browser or captcha solving involved?

No browser, and no captcha solving. It's plain HTTP GETs against Foundit's own JSON middleware API and its AMP detail pages. The only obstacle is IP reputation — which is a proxy concern, not a rendering one.

#### Can I get descriptions without paying for the whole run?

Yes, that's what `maxDescriptionItems` is for. It caps phase 2 independently of `maxItems`, so you can collect 500 jobs and describe only the 150 that matter. Leave `includeDescriptions: false` entirely for the cheapest possible run.

### SEO Keywords

foundit scraper, monster india jobs, india job scraper, job board data, india salary data, inr salary database, job market research india, recruiter data, hiring trends india, it job listings, remote work india jobs, skills taxonomy jobs, job posting api, tech jobs india, job aggregation, employment data india, salary benchmarking, job market analysis, talent acquisition data, hiring intelligence, india tech hiring, job description scraping, career data pipeline, workforce analytics

### For AI Agents & LLM Apps

Returns Foundit (Monster India) job listings as **one flat dataset, 44 fields per record**.

**Minimal input** — smallest input that produces a valid run:

```json
{ "keywords": ["software engineer"] }
```

**Cheapest possible run** (no detail phase):

```json
{ "keywords": ["accountant"], "locations": ["Mumbai"], "includeDescriptions": false, "maxItems": 200 }
```

**Salary research, mid-level only**:

```json
{ "keywords": ["data scientist"], "locations": ["Bengaluru", "Hyderabad"], "experienceMin": 3, "experienceMax": 8, "includeDescriptions": false, "maxItems": 500 }
```

**Full descriptions, cost-capped**:

```json
{ "keywords": ["product manager"], "locations": ["Delhi NCR"], "maxItems": 300, "maxDescriptionItems": 150 }
```

**Output fields (44):** jobId, title, company, location, country, salaryMin, salaryMax, salaryCurrency, salaryPerMonthMin, salaryPerMonthMax, salaryText, salaryHidden, experienceMinYears, experienceMaxYears, experienceText, skills, skillsWithSynonyms, designations, functions, industries, employerTypes, jobType, employmentTypes, workplaceType, postedDate, lastUpdatedDate, postedRelative, validThrough, description, descriptionHtml, descriptionStatus, enrichedAt, applyUrl, jobUrl, companyUrl, externalApply, recruiterName, postedBy, totalApplicants, urgentlyHiring, source, searchQuery, searchLocation, scrapedAt

**Behaviours an agent should know**

- **You must set a residential proxy or the run returns nothing.** `www.foundit.in` 403s datacenter IPs. Use `proxy: { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }`. This is the most common failure mode.
- **Records appear twice.** Phase 1 pushes with `descriptionStatus: "pending"`, phase 2 re-pushes the same `jobId` with `descriptionStatus: "complete"`. Dedupe on `jobId`, keeping `complete`. Jobs outside `maxDescriptionItems` stay `pending` permanently.
- **Phase 2 re-pushes are billed too** ($0.0025 each). A described run costs roughly double an undescribed run of the same size. To describe fewer, lower `maxDescriptionItems`, not `maxItems`.
- `maxDescriptionItems` defaults to **200** while `maxItems` defaults to **50**, so default input describes everything. Set both explicitly if you want partial description.
- `salaryMin`/`salaryMax` may be **strings with commas** from the API; `salaryPerMonthMin`/`salaryPerMonthMax` are parsed numbers and are populated **only for MONTH-unit postings**. Read `salaryText` and convert YEAR figures yourself.
- `salaryHidden: true` means the employer hid pay **or** nothing was published. Filter on it — don't read a null salary as zero.
- Dates from JSON-LD are **DD-MM-YYYY**, not ISO. Reformat before date comparisons.
- Experience filtering happens client-side because Foundit's API returns empty results for some bands (e.g. `3-5`). Widen filters rather than assuming no jobs exist.
- Detail pages are fetched from **regional Foundit mirror domains** in order (`founditgulf.com` → `foundit.sg` → `foundit.in`), least-protected first, because the main domain blocks datacenter IPs.
- Concurrency is split: `maxConcurrency` (default 5) for search, `detailConcurrency` (default 15) for descriptions. Raise the latter freely, keep the former at 3–8.
- `recruiterName` can be a real person's name — treat it as personal data.
- Keywords and locations combine multiplicatively; results dedupe by `jobId`, and `searchQuery`/`searchLocation` show which combination matched.
- No browser, no captcha solving — plain HTTP only.

### Legal & Compliance

This Actor is an independent third-party tool. It is not affiliated with, endorsed by, or sponsored by Foundit (Monster India), or by any employer whose listings appear in its output.

**Be aware of what this Actor does, because it is not neutral.** `www.foundit.in` is fronted by **Akamai Bot Manager**, which rejects most datacenter IP ranges with HTTP 403. This Actor deliberately works around that protection: detail requests are routed across Foundit's own regional sibling domains (`founditgulf.com`, `foundit.sg`), chosen specifically because they serve identical content with weaker edge protection, and the run is expected to use **residential proxies**. That is circumvention of a technical access control, and it means:

- You should only run this against Foundit if you have a genuine basis to access its job data — your own listings, an agreement with the company, or another lawful basis.
- If your use is automated, high-volume or repeated, **ask Foundit for written permission first.** That is the correct route, and it is far less work than defending a circumvention-based collection.
- Review Foundit's Terms of Use, which restrict automated access, and its `robots.txt` — which could not be verified because the request was itself blocked.

**Data subject rights.** This output contains **personal data**: `recruiterName` is a named individual's name, and `applyUrl`/`jobUrl` identify specific vacancies. Under India's **Digital Personal Data Protection Act (DPDP Act, 2023)** and the broader GDPR regime, job-seeker and recruiter data attracts specific obligations. You are the data fiduciary in that relationship, not this tool.

If you do process this data lawfully, you must still: give the applicable **notice** to the people concerned, honour **erasure and access requests**, and keep the data only as long as a stated purpose requires. Do not use `recruiterName` for unsolicited outreach that the individual has not consented to or has objected to. Job descriptions belong to the employers who wrote them — do not republish them wholesale without permission.

**Terms of the platform.** Foundit's Terms of Use reserve rights over the job database, the taxonomy, and the company/recruiter profile data. Building a competing job board, or a scraped-aggregator product, from this data is very likely to breach them.

You are solely responsible for compliance with Foundit's Terms of Use, applicable data-protection law, and any permission you obtain. If you need India job data at scale on a defensible footing, a commercial data licence from Foundit or a partner aggregator is the durable answer.

# Actor input Schema

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

Job titles, roles or skills to search for. Each keyword is searched separately and results are merged + deduplicated by Foundit job ID.

## `locations` (type: `array`):

Cities or regions to filter by, e.g. Bengaluru, Mumbai, Hyderabad, Pune, Delhi NCR. Leave empty to search all of India.

## `experienceMin` (type: `integer`):

Only return jobs requiring at least this many years of experience. 0 = no minimum.

## `experienceMax` (type: `integer`):

Only return jobs requiring at most this many years of experience. 0 = no maximum.

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

Maximum number of job records to save to the dataset.

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

Safety limit on how many result pages are fetched for each keyword/location combination.

## `includeDescriptions` (type: `boolean`):

After the listings are stored, fetch each job's detail page in a second background phase and read the schema.org JobPosting JSON-LD block to get the full description, posting/expiry dates and base salary. Adds one request per job, but the dataset is already usable before this phase starts. Disable for the cheapest runs.

## `maxDescriptionItems` (type: `integer`):

How many of the collected jobs get a full description in phase 2. 0 = no descriptions. Remaining jobs stay with descriptionStatus 'pending'. Only used when 'Include full job descriptions' is on.

## `maxConcurrency` (type: `integer`):

Parallel requests for the phase-1 search API calls. Keep this low (3-8) to stay under Foundit's rate limits.

## `detailConcurrency` (type: `integer`):

Parallel requests for the phase-2 description fetches. These are static AMP pages on the least-protected regional mirrors, so they tolerate (and benefit from) much higher parallelism than the search API.

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

Select proxies to be used by your crawler.

## Actor input object example

```json
{
  "keywords": [
    "software engineer"
  ],
  "locations": [],
  "experienceMin": 0,
  "experienceMax": 0,
  "maxItems": 50,
  "maxPages": 5,
  "includeDescriptions": true,
  "maxDescriptionItems": 200,
  "maxConcurrency": 5,
  "detailConcurrency": 15,
  "proxy": {
    "useApifyProxy": false
  }
}
```

# 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 = {
    "keywords": [
        "software engineer"
    ],
    "locations": [],
    "experienceMin": 0,
    "experienceMax": 0,
    "maxItems": 50,
    "maxPages": 5,
    "includeDescriptions": true,
    "maxDescriptionItems": 200,
    "maxConcurrency": 5,
    "detailConcurrency": 15,
    "proxy": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ahmed_jasarevic/foundit-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": ["software engineer"],
    "locations": [],
    "experienceMin": 0,
    "experienceMax": 0,
    "maxItems": 50,
    "maxPages": 5,
    "includeDescriptions": True,
    "maxDescriptionItems": 200,
    "maxConcurrency": 5,
    "detailConcurrency": 15,
    "proxy": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("ahmed_jasarevic/foundit-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": [
    "software engineer"
  ],
  "locations": [],
  "experienceMin": 0,
  "experienceMax": 0,
  "maxItems": 50,
  "maxPages": 5,
  "includeDescriptions": true,
  "maxDescriptionItems": 200,
  "maxConcurrency": 5,
  "detailConcurrency": 15,
  "proxy": {
    "useApifyProxy": false
  }
}' |
apify call ahmed_jasarevic/foundit-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ahmed_jasarevic/foundit-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/ouNFgKxKFYizng7Rh/builds/WmpNzb3gzB7KhQ2dj/openapi.json
