# Zoho Recruit & Keka Jobs Scraper - India Career Sites (`neverempty/zoho-recruit-keka-jobs`) Actor

Scrape open jobs from any company careers page on Zoho Recruit or Keka: title, department, city, state, country, experience, salary, skills, posted date and apply link in one row shape. Full description optional. Zero open roles and a missing careers page come back as their own rows. Unofficial.

- **URL**: https://apify.com/neverempty/zoho-recruit-keka-jobs.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.60 / 1,000 job returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

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

## Zoho Recruit & Keka Jobs Scraper - India Career Sites

Get every open job from a company careers page on **Zoho Recruit** or **Keka** as one clean table: title, company, department, employment type, experience, city, state, country, salary where published, skills, posted date and the apply link. Enter a company name or paste the careers page URL. No login or API key needed.

You get the jobs the company itself has published, in the same columns for Zoho Recruit and Keka, ready for Excel, CSV or your own tool.

***

### What you get

- **One row per open job**, same 31 columns on Zoho Recruit and Keka, so one spreadsheet or filter works for both
- **City, state and country as separate columns**, so filtering by Hyderabad or India is one click in a spreadsheet
- **Experience** (for example "20+ years") and **department** where the company publishes them
- **Full job description as plain text** on request (`includeDescription`); with it on, Zoho Recruit rows also get **skills, salary and industry** where the company published them
- **Posted date in ISO 8601**, so "jobs from the last 7 days" works with `postedWithinDays`
- **Remote jobs flagged as the company set them** (`isRemote`); a separate `remoteHint` column gives a best guess from the title or location
- **Salary shown only where the company published it**; otherwise the cell stays empty

### Know why a company returned nothing

Every company you enter returns at least one row, so an empty result is never a mystery. Only `job` rows are charged. The `rowType` column says which case you got:

| `rowType` | What it means | Charged? |
|---|---|---|
| `job` | One open job | Yes |
| `no-open-roles` | The careers page is live and has no open jobs (or none matching your filters - the `reason` column says which) | No |
| `board-not-found` | No careers page with that name exists on Zoho Recruit or Keka | No |
| `unavailable` | The page could not be read after retries (temporary error) | No |
| `budget-reached` | Your maximum total charge for the run was reached; says how many jobs you got of how many | No |

If a filter cannot work for a company - for example `remoteOnly` on Keka, whose careers pages have no remote field - you get a free row that says so, instead of a silent zero.

***

### Input

| Field | Example | What it does |
|---|---|---|
| `companies` | `zoho:talent-shore`, `keka:pennanttech` | One entry per company, up to 200 per run |
| `titleContains` | `developer` | Keep jobs whose title contains this text |
| `departmentContains` | `sales` | Keep jobs whose department contains this text |
| `locationContains` | `Bengaluru` | Matches city, state, country or any of several locations |
| `remoteOnly` | `true` | Only jobs the company marked as remote |
| `postedWithinDays` | `7` | Only jobs opened in the last N days |
| `maxJobsPerCompany` | `50` | Cap rows per company |
| `includeDescription` | `true` | Add the full description (slower on Zoho Recruit: one extra page per job) |

#### Three ways to name a company

```json
{
  "companies": [
    "zoho:talent-shore",
    "https://pennanttech.keka.com/careers/",
    "pennanttech"
  ]
}
```

- `zoho:<company>` - the part before `.zohorecruit.com`. Regional addresses such as `acme.zohorecruit.in` are accepted too
- `keka:<company>` - the part before `.keka.com`
- A pasted careers page URL
- Just the name - Zoho Recruit is tried first, then Keka

Left empty, the run uses the two examples above.

***

### Output example

A real row from a run on 2026-10-29 (some columns left out here):

```json
{
  "rowType": "job",
  "ats": "Keka",
  "companySlug": "pennanttech",
  "companyName": "Pennant Technologies",
  "jobId": "143320",
  "title": "Chief Delivery Officer",
  "department": "Client Services",
  "employmentType": "full-time",
  "experience": "20+ years",
  "locationRaw": "Corporate Office, Hyderabad, TG, India",
  "city": "Hyderabad",
  "state": "TG",
  "country": "India",
  "isRemote": null,
  "postedAt": "2026-10-16T09:54:29.917Z",
  "jobUrl": "https://pennanttech.keka.com/careers/jobdetails/143320",
  "applyUrl": "https://pennanttech.keka.com/careers/jobdetails/143320"
}
```

All columns: `rowType`, `ok`, `reason`, `ats`, `companySlug`, `companyName`, `jobId`, `title`, `department`, `employmentType`, `experience`, `locationRaw`, `locationsAll`, `country`, `state`, `city`, `isRemote`, `remoteHint`, `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`, `salaryText`, `skills`, `industry`, `postedAt`, `jobUrl`, `applyUrl`, `descriptionSnippet`, `descriptionText`, `source`.

***

### Measured on the live sites (2026-10-29)

- Default input (one Zoho Recruit and one Keka company): **250 jobs in under 5 seconds**
- With full descriptions for three companies: **280 jobs, every one with its full description**, about 3 minutes (Zoho Recruit reads one page per job)
- Two company names that do not exist: **two free `board-not-found` rows**, no charge

### Questions this answers

- **How do I scrape jobs from a Zoho Recruit careers page?** Put `zoho:<company>` or the careers page URL in `companies` and run.
- **How do I export Keka job openings to Excel or CSV?** Use `keka:<company>`, then download the dataset as CSV or Excel.
- **Which of these companies is hiring developers in Bengaluru right now?** `titleContains: developer` with `locationContains: Bengaluru`.
- **Did this company stop hiring, or is the page gone?** Two different rows: `no-open-roles` and `board-not-found`.

### Typical uses

- **Hiring signals for sales** - a company that just opened 20 roles is a different lead than one with none
- **Job boards and aggregators for India** - one schema for two platforms that Indian employers use
- **Recruiting and staffing** - track client and competitor openings daily and compare on `jobId`
- **Salary and skills research** - structured salary where the company published it, required skills from Zoho Recruit job pages (with `includeDescription`)

***

### Pricing

Pay per result: a small start fee once per run, charged only when the first job row is returned, then a fee per job row. Rows that say a page does not exist, has no open jobs, could not be read, or that your limit was reached are free. The same company entered twice in one run is read and charged once.

### Notes and limits

- Unofficial. Not affiliated with, endorsed by, or sponsored by Zoho Corporation or Keka Technologies. All product names are trademarks of their respective owners.
- Only public careers pages are read. Nothing needs a login.
- Keka careers pages have no remote field, so `remoteOnly` keeps no Keka jobs (a free row explains this). Use `remoteHint` for a guess.
- Some companies leave fields empty (for example city or department); those stay `null` rather than guessed.
- Company names on Zoho Recruit come from the page title when it reads "Jobs at ..." or "Careers at ..."; other titles leave `companyName` empty.
- Up to 200 companies per run. Temporary errors (HTTP 429 or 5xx, timeouts) are retried; a page that does not exist is reported, not retried.

### Other tools by NeverEmpty

- **[Job Listing API - Career Site Jobs from Any ATS](https://apify.com/neverempty/ats-jobs-api)** - the same idea for the large global applicant tracking systems
- **[ATS Detection](https://apify.com/neverempty/ats-board-finder)** - find which applicant tracking system a company uses from its domain

### Support

If a company page does not work as expected, or you want another Indian applicant tracking system added, open an issue on the **Issues** tab of this Actor with the company name. It will be reproduced on the live page before anything is changed.

# Actor input Schema

## `companies` (type: `array`):

One entry per company. Use "zoho:<company>" (the part before .zohorecruit.com, for example zoho:talent-shore; other regions like zoho:acme.zohorecruit.in also work) or "keka:<company>" (the part before .keka.com, for example keka:pennanttech). You can also paste the careers page URL, or give just the company name to try Zoho Recruit and then Keka. A company with no careers page is returned as a board-not-found row - never silently dropped. Up to 200 companies per run. Left empty, the run uses the two examples.

## `titleContains` (type: `string`):

Keep only jobs whose title contains this text (case-insensitive). Leave empty for all jobs.

## `departmentContains` (type: `string`):

Keep only jobs whose department contains this text (case-insensitive). Leave empty for all jobs.

## `locationContains` (type: `string`):

Keep only jobs whose location (city, state, country or any of several locations) contains this text, for example Bengaluru or India (case-insensitive). Leave empty for all jobs.

## `remoteOnly` (type: `boolean`):

Keep only jobs the company itself marked as remote (the Remote Job field on Zoho Recruit). Keka careers pages have no remote field, so this filter keeps no Keka jobs; a free row explains it. A guess from the title or location text is never used here - it is shown separately in the remoteHint column.

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

Keep only jobs opened or published within this many days. 0 disables the filter. Jobs with an unknown date are excluded when this filter is on.

## `maxJobsPerCompany` (type: `integer`):

Upper limit of job rows kept per company after filtering. 0 means no limit. You are only charged for job rows actually written to the dataset.

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

Add the full job description as plain text in the descriptionText column. On Zoho Recruit this also fills salary, required skills and industry from each job page, and reads one extra page per job, so runs take longer. Keka jobs always include it at no extra page cost. Every row has a short descriptionSnippet either way.

## `maxRetries` (type: `integer`):

How many times to try a company in total, including the first attempt, when the site returns a temporary error (HTTP 429 or 5xx) or a timeout. Permanent answers such as 404 are not retried.

## Actor input object example

```json
{
  "companies": [
    "zoho:talent-shore",
    "keka:pennanttech"
  ],
  "titleContains": "",
  "departmentContains": "",
  "locationContains": "",
  "remoteOnly": false,
  "postedWithinDays": 0,
  "maxJobsPerCompany": 0,
  "includeDescription": false,
  "maxRetries": 3
}
```

# Actor output Schema

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

All rows produced by this run.

# 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 = {
    "companies": [
        "zoho:talent-shore",
        "keka:pennanttech"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/zoho-recruit-keka-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 = { "companies": [
        "zoho:talent-shore",
        "keka:pennanttech",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/zoho-recruit-keka-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 '{
  "companies": [
    "zoho:talent-shore",
    "keka:pennanttech"
  ]
}' |
apify call neverempty/zoho-recruit-keka-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/zoho-recruit-keka-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/Jcdq6kMjZxGtOqx91/builds/i6tteIe9Rihc1BsFa/openapi.json
