All platforms

Zoho Recruit Jobs API

Query jobs from Zoho Recruit boards through one API with ?source=zohorecruit — plus how Zoho Recruit's careers page really behaves.

?source=zohorecruitsource reference

Zoho Recruit is the ATS in Zoho's business software suite, used by small companies and staffing agencies that already run Zoho for CRM or books. Boards live at <tenant>.zohorecruit.com/jobs/Careers. It's 3,918 postings in this dataset — a number that moves hourly, so read it from /v1/meta rather than trusting this sentence.

There is no public JSON API. There is a careers page with the job list stuffed into a hidden form input, and there is a detail page with the body inside a JavaScript string literal. It works. It is not pretty, and it is the most expensive platform per job we have measured anywhere.

Try it#

BASH
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=zohorecruit" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
Illustrative — the shape, not a live response
{
  "data": [
    {
      "id": "zohorecruit:486372000000512041",
      "title": "Business Development Executive",
      "company": {
        "slug": "northwind-staffing",
        "name": "Northwind Staffing",
        "domain": "northwindstaffing.com",
        "website": "https://northwindstaffing.com",
        "logo": "https://logo.hyperjobs.io/northwindstaffing.com",
        "industry": "Staffing and Recruiting",
        "employee_count": 45
      },
      "location": {
        "remote": false,
        "countries": ["India"],
        "cities": ["Chennai"],
        "regions": ["Tamil Nadu"],
        "raw": ["Chennai, Tamil Nadu, India"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "On-site",
      "seniority": null,
      "salary": {
        "min": null,
        "max": null,
        "currency": null,
        "unit": null,
        "annual_min": null,
        "annual_max": null,
        "summary": "30000"
      },
      "skills": ["business development", "lead generation"],
      "taxonomies": ["Sales"],
      "apply_url": "https://northwind-staffing.zohorecruit.com/jobs/Careers/486372000000512041",
      "url": "https://northwind-staffing.zohorecruit.com/jobs/Careers/486372000000512041",
      "source": "zohorecruit",
      "posted_at": "2026-07-11T00:00:00.000Z",
      "expires_at": null,
      "updated_at": "2026-07-14T09:02:55.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}

Look at salary.summary. It says "30000" and the structured fields are all null, which is not us failing to parse a range — it's the whole of what Zoho gives. See below.

How Zoho Recruit boards actually work#

Two requests per job, and neither is an API call:

BASH
# 1. List — the jobs are inside a hidden <input>, not the markup
curl -L "https://<tenant>.zohorecruit.com/jobs/Careers"
 
# 2. Detail — the body is inside a JS string literal
curl -L "https://<tenant>.zohorecruit.com/jobs/Careers/<id>/<slug>"

<tenant> is the subdomain. The slug on the detail URL is decorative: /jobs/Careers/<id>/anything serves the right job.

The TLD varies and isn't derivable

Tenants live on both zohorecruit.com and zohorecruit.in, and nothing about the tenant name tells you which. You try one and fall back to the other.

The list is hidden in a form input#

The careers page renders its jobs client-side. The data is sitting in the HTML the whole time, HTML-escaped into the value of a hidden input:

HTML
<input type="hidden" value="[{&#34;id&#34;:&#34;4863720000005…" id="jobs">

Three separate things about that line have cost someone an afternoon:

value= comes BEFORE id=. The regex everyone writes first is id="jobs"[\s\S]*?value="…" and it matches nothing, so the board reads as empty at HTTP 200. You have to anchor on the id and walk backwards to the opening <input.

The escaping is numeric. Zoho uses &#34;, not &quot;. An unescaper that only knows the named entities leaves the payload unparseable as JSON.

There is a decoy input. A moduleMeta input holds a 67KB field schema, in which Job_Description appears as a "richtext" label. Grep the page for field names and you will find that instead of the jobs. Select by id="jobs" and the problem doesn't exist.

The detail is a JavaScript string literal#

The description is only on the detail page, and only inside this:

JS
var jobs = JSON.parse('[{\x22Job_Description\x22:\x22\x3Cp\x3E…');

A single-quoted JS literal, \xNN-escaped. It needs a real left-to-right one-pass decoder that consumes \\ as a unit. The obvious .replace(/\\x22/g, '"') is wrong: it rewrites the x22 that follows an escaped backslash, and corrupts the payload silently rather than throwing.

The list is thin, so the N+1 is unavoidable#

The list carries id, title, city, country, Remote_Job and Job_Type. It has no description, no date and no salary. All three are detail-only. There is no "give me the bodies too" parameter, so one request per job is the floor.

What bites you#

The economics: ~1.75MB per job#

This is the headline problem with Zoho Recruit and the reason it sits last in our refresh order.

Page weight here is fixed overhead, not job count. A 4-job board is still 1.73MB. A 74-job board is roughly 130MB. You are downloading a full styled careers page per posting and discarding about 90% of it to reach one JSON blob.

Why we never proxy this one

Because there's no lean JSON endpoint, a residential proxy would be metering 10–22MB of mostly-CSS per board straight onto the bill. Zoho Recruit scrapes fine direct, so proxying it would only ever burn money. It's flagged neverProxy in our stack for exactly that reason — and if you build this yourself, bandwidth is the line item that will surprise you, not requests.

The mitigation that actually works is not fetching a job twice: keep the ids you already have and only pull detail pages for the ones you don't. That's what keeps a steady-state Zoho rotation affordable, and it's most of why this source is viable at all.

Salary is a bare number#

Salary is a string like "30000". No currency. No period. Is that ₹30,000 a month or $30,000 a year? The field does not say, and on a platform with heavy Indian and US tenancy both readings are plausible.

So we keep it as text in salary.summary and let the shared parser decide rather than assert a unit the source never gave. Building a {min, max, currency} out of that number means inventing two thirds of it.

What Zoho does give you, straight#

Not everything here is a trap. Remote_Job is a real boolean, present on both list and detail — that's better than the string-typed or absent remote flags on plenty of larger platforms. Work_Experience is a usable band (1-3 years, 5+ years). Industry is populated. Date_Opened is already YYYY-MM-DD. And the ids are 18-digit and globally unique, so unlike most per-tenant boards the key needs no tenant scoping.

Or use ours#

Zoho Recruit is a hidden input with a backwards attribute order and numeric entities, a JS literal that needs a hand-written decoder, a TLD you have to guess, an unavoidable N+1, and 1.75MB of bandwidth for every single job you want.

You get the rows already extracted and decoded, the detail joined to the list, Remote_Job and Work_Experience mapped through, the bare salary kept honest as text instead of dressed up as a range, and the whole bandwidth problem absorbed on our side. Plus dedup, expiry by board diffing, and the company join.

Zoho Recruit sits in the heavy refresh tier rather than the fast one, and the cost above is exactly why — see coverage.

Query it with ?source=zohorecruit. Quickstart, then pricing.

Every source above normalises onto the same schema, with the hiring company already joined onto each posting.

Get an API key