Zoho Recruit Jobs API: Get Zoho Job Listings as JSON

CleanJobData Engineering

TL;DR

Zoho Recruit career sites call a public, keyless JSON API at {company}.zohorecruit.com/recruit/v2/public/Job_Openings. The list call needs a pagename parameter that differs per company, returns every published job in one response, and rejects extra parameters such as page or limit. The description (Job_Description) and the date (Date_Opened, date only) come from a detail call per job, and location comes as separate City, State and Country fields.

On this page

To get a company's jobs from Zoho Recruit, call https://{company}.zohorecruit.com/recruit/v2/public/Job_Openings?pagename={PageName} for the list, then /recruit/v2/public/Job_Openings/{id}?pagename={PageName} for each job's description. No API key, cookie or token is needed; the only catch is finding the right pagename for the company.

That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain a Zoho Recruit scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the Jobs API docs.

At a Glance

EndpointGET https://{company}.zohorecruit.com/recruit/v2/public/Job_Openings?pagename={PageName}
Detail endpointGET https://{company}.zohorecruit.com/recruit/v2/public/Job_Openings/{id}?pagename={PageName}
API keyNone
PaginationNone: one call returns every published job; extra parameters are rejected
Full descriptionDetail call per job (Job_Description)
Salary fieldNo reliable one; some companies fill a free-text Salary
RemoteRemote_Job, a boolean or "Yes" / "No"

Finding the Company and Pagename

Two values identify a Zoho Recruit career site: the company subdomain and the page name.

  • The subdomain is the first part of the host. For acme.zohorecruit.com, it's acme.
  • The pagename is the name of the career-site page the company published. It's often Careers, but not always, and you can't guess it.

On newer career sites the pagename is in the URL: https://acme.zohorecruit.com/jobs/Careers/{id}/{title} means the pagename is Careers. On older sites the URL (for example /careers) doesn't show it. Then view the page source and search for page_name, which holds the value. The list response also echoes it back as info.page_name, so you can confirm you have the right one.

The List Request

curl "https://acme.zohorecruit.com/recruit/v2/public/Job_Openings?pagename=Careers"

Trimmed response:

{
  "code": "success",
  "data": [
    {
      "id": "530797000017438001",
      "Posting_Title": "MDM Architect",
      "Job_Opening_Name": "MDM Architect",
      "Job_Type": "Full time",
      "City": "Dublin",
      "Country": "United States",
      "$url": "https://acme.zohorecruit.com/jobs/Careers/530797000017438001/MDM-Architect?source=CareerSite",
      "Publish": true,
      "Is_Locked": false,
      "Keep_on_Career_Site": false
    },
    {
      "id": "530797000017606174",
      "Posting_Title": "Senior Client Advisor",
      "Remote_Job": "Yes",
      "City": "",
      "State": "",
      "Country": "",
      "Zip_Code": "",
      "Job_Type": "Full time",
      "$url": "https://acme.zohorecruit.com/jobs/Careers/530797000017606174/Senior-Client-Advisor?source=CareerSite"
    }
  ],
  "info": {
    "page_name": "Careers",
    "fields": [{ "api_name": "Job_Opening_Name", "field_label": "Posting Title" }]
  }
}

There's no pagination. Every published job comes back in one response, and any extra query parameter, such as page, per_page or limit, is rejected with 400 EXTRA_PARAM_FOUND. The boards we've checked were small, so we can't say how the endpoint behaves for a company with hundreds of open jobs.

The Detail Request

curl "https://acme.zohorecruit.com/recruit/v2/public/Job_Openings/530797000017438001?pagename=Careers"

The detail is wrapped the same way as the list, as a one-element data array, not a bare object:

{
  "code": "success",
  "data": [
    {
      "id": "530797000017438001",
      "Posting_Title": "MDM Architect",
      "Job_Description": "<span id=\"spandesc\"><p>Acme is seeking an experienced Architect...</p></span>",
      "Date_Opened": "2026-07-20",
      "City": "Dublin",
      "State": "Ohio",
      "Country": "United States",
      "Zip_Code": "43017",
      "Remote_Job": false,
      "Job_Type": "Full time",
      "Industry": "IT Professional Services",
      "Work_Experience": "5+ years",
      "$appy_url": "https://acme.zohorecruit.com/jobs/Careers/530797000017438001/MDM-Architect?$apply=true&source=CareerSite"
    }
  ]
}

Response Fields

FieldWhereWhat it holds
idList and detailJob ID, the same in both
Posting_Title, Job_Opening_NameList and detailJob title
Job_DescriptionDetailFull HTML description
Date_OpenedDetail (sometimes list)Date only, YYYY-MM-DD
City, State, Country, Zip_CodeList and detailSeparate location fields; Country is a full name
Remote_JobList and detailtrue / false, or "Yes" / "No"
Job_TypeList and detailFree text, such as Full time
Industry, Work_ExperienceDetailFree text, when set
SalaryDetail, some companiesFree text, often a placeholder
$urlListThe job page
$appy_urlDetailThe job page with the application form opened

The field names are the same on every Zoho Recruit career site we've checked, which makes this one of the more predictable platforms.

Fetching Jobs (JavaScript)

const api = (company, path, pagename) =>
  `https://${company}.zohorecruit.com/recruit/v2/public/${path}?pagename=${encodeURIComponent(pagename)}`;
 
async function getJson(url) {
  const res = await fetch(url, { headers: { accept: "application/json" } });
  if (res.status === 204) return null; // job no longer exists
  if (!res.ok) throw new Error(`Zoho Recruit ${res.status} for ${url}`);
  return res.json();
}
 
export async function fetchZohoRecruitJobs(company, pagename) {
  const list = await getJson(api(company, "Job_Openings", pagename));
  const jobs = [];
  for (const row of list?.data ?? []) {
    const detailRes = await getJson(api(company, `Job_Openings/${row.id}`, pagename));
    const d = detailRes?.data?.[0];
    if (!d || String(d.id) !== String(row.id)) continue;
    const remote = d.Remote_Job === true || String(d.Remote_Job).toLowerCase() === "yes";
    jobs.push({
      id: row.id,
      title: (d.Posting_Title || d.Job_Opening_Name || "").trim(),
      dateOpened: d.Date_Opened ?? row.Date_Opened ?? null, // "YYYY-MM-DD"
      city: d.City || null,
      state: d.State || null,
      country: d.Country || null,
      remote,
      jobType: d.Job_Type ?? null,
      url: row.$url ?? d.$appy_url,
      html: d.Job_Description ?? "",
    });
    await new Promise((r) => setTimeout(r, 250));
  }
  return jobs;
}
 
// fetchZohoRecruitJobs("acme", "Careers").then(console.log);

Gotchas

Read location from the detail, not the list. On some career sites every list row has blank City and Country while the detail has the real values. Build location from the detail response.

Country is a full name. Country is United States or Philippines, not an ISO code, so you need a name-to-code lookup. See the location normalization guide.

Remote_Job has two types. Some companies return a boolean and others the strings "Yes" and "No". Handle both, and don't coerce the string "No" to true.

Date_Opened is a date with no time or time zone. Treat it as a calendar date. It's the only date the API gives you.

$url and $appy_url aren't the same link. $url, on the list row, is the normal job page. $appy_url (spelled that way in Zoho's API), on the detail, adds $apply=true, which opens the application form straight away. Use $url when you want to link to the job.

Salary is free text, when it exists. Some companies have a Salary field with a usable range like "Range: $110,000 to $130,000 DOQ". Others use it for placeholders such as "Negotiable" or "0.00", or leave it empty. Treat it as text to parse, not a number. The salary extraction guide covers that kind of parsing.

Job_Type has no fixed vocabulary. Values range from Full time to longer phrases like "Full time: 35 paid hours per week or more". Map them yourself.

Errors tell you what went wrong. A missing job returns HTTP 204 with an empty body. An unknown company returns 403 with ORG_NOT_FOUND. A wrong or missing pagename returns 400. None of these returns an empty list that looks like success.

Public Career-Site API vs Zoho's Official API

Zoho Recruit also has an official REST API for its customers. It uses OAuth credentials from the employer's own Zoho account and covers that company's recruiting records, such as candidates. The /recruit/v2/public/ endpoints above are what the career site itself calls, and they're the ones to use for published jobs from companies you don't work with.

When This Is Enough

For one company, Zoho Recruit is easy once you have the pagename: one list call, one detail call per job, and fixed field names.

When It Isn't

Across many companies and platforms you end up maintaining a different collector for each, and some, like Zoho, need a per-company value you can't guess. The ATS public job postings API list shows how the platforms compare.

CleanJobData reads Zoho Recruit career sites alongside other platforms and returns one schema, with locations resolved to cities, states and countries, a remote type, and salary parsed from the description when the employer includes it. Try it in the Playground or see the API Documentation.

Skip the scraper: get this data from the CleanJobData API

If you'd rather not maintain a Zoho Recruit collector, you can query normalized jobs from ATS platforms and company career sites with a single request. The response has structured city/state/country locations, parsed salary when the employer publishes one, a remote type, and company metadata, and you page through results with the cursor in pagination.next_page.

curl "https://api.cleanjobdata.com/jobs?title=engineer&domain=acme.zohorecruit.com&limit=20" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"

The domain filter is an exact match on the host of each job's application_url. Zoho Recruit jobs apply on each company's own zohorecruit.com subdomain, so this scopes the query to one employer; use company_website_url or employer_id as alternatives. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.

FAQ

Does Zoho Recruit have a public jobs API?

Yes. Zoho Recruit career sites load jobs from https://{company}.zohorecruit.com/recruit/v2/public/Job_Openings?pagename={PageName}, which needs no API key or login and returns every published job in one response.

What is the pagename in the Zoho Recruit API?

It's the name of the company's career-site page, such as Careers, and it's required on every request. Newer career sites show it in the job URL after /jobs/; on older ones, search the page source for page_name.

Can I paginate the Zoho Recruit jobs API?

No. The list endpoint returns every published job in one call and rejects extra parameters such as page or limit with a 400 EXTRA_PARAM_FOUND error.

How do I get the job description from Zoho Recruit?

Call /recruit/v2/public/Job_Openings/{id}?pagename={PageName} for each job. The response wraps the job in a one-element data array, and the HTML description is in Job_Description.