---
name: zoho-recruit-oauth-app
description: The Zoho Recruit layer on top of the shared `zoho-api-console-oauth` skill — the Recruit scope families and the singular-versus-plural module-name inconsistency that makes scope strings fail, the candidate / job-opening / application / interview module model and the Staffing Agency versus Corporate HR edition split that decides which modules exist at all, the dedicated `recruit.zoho.*` API host that disagrees with the `api_domain` the token response returns, the five-refresh-tokens-per-minute ceiling, and the credit model whose concurrency limit is counted per user rather than per organization. Use when asked to get Zoho Recruit OAuth credentials, choose Zoho Recruit scopes, debug an invalid-scope or wrong-host error on a Recruit connection, or explain why a Recruit integration cannot see clients or a custom module. Read `zoho-api-console-oauth` first for the console and data-centre mechanics; use the Zoho CRM or Zoho Books skill for those products.
---

# Zoho Recruit OAuth2 App Registration

The client registration is the base skill's job. What is specific to Recruit is that **its documentation contradicts
itself more than any other Zoho product's**, in exactly the two places that decide whether a connection works: the
spelling of module scope names, and which host the API lives on. Both fail with errors that point somewhere else.

Beyond that, Recruit is an ATS with two quite different shapes — staffing agency and in-house corporate HR — and the
module set differs between them. A scope list that assumes Clients and Vendors exist is a scope list written for an
agency, and it will look broken on a corporate customer.

## Built on: `zoho-api-console-oauth`

**Read `zoho-api-console-oauth` first, then this file.** It owns all the console and protocol mechanics, and this
file does not repeat them:

- The Zoho API Console, client types, and why a multi-tenant connector needs **Server-based**, not Self Client.
- **The data-centre model** — `location` / `accounts-server` on the callback, the per-DC accounts hosts, the Multi DC
  toggle, and the rule that **one client ID serves every DC while the client secret is per-DC by default**.
- Redirect-URI rules and the platform's per-data-center callback list.
- The generic scope grammar `service.scope.OPERATION`, and that scopes are **comma-delimited**.
- `access_type=offline` plus `prompt=consent`, and the **20-refresh-tokens-per-user-per-client** cap that silently
  revokes the oldest.
- The `Authorization: Zoho-oauthtoken {token}` header form.
- Capturing and rotating the secret, and limits being a per-organization concept.

If you are reading only this file, you are missing all of the above.

## Inputs to collect before you start

The base skill lists the console inputs. These are the Recruit-specific ones:

| Input | Notes |
| --- | --- |
| **Staffing agency customers, corporate HR customers, or both?** | Decides which modules exist to scope (§3) |
| **Which Recruit modules must the integration reach?** | Candidates and Job Openings are rarely the whole list (§2) |
| **Read-only, or read-write?** | `.READ` versus `.ALL` per module (§1) |
| **Are custom modules in scope?** | Needs its own scope and run-time discovery (§3) |
| **Does it need field metadata?** | A separate scope family, and almost always yes (§1) |
| **Does it need resumes or attachments?** | Confirm against the endpoint's own documented scope (§3) |
| **What edition are the target customers on?** | Sets their credit ceiling (§5) |

## Quick Start

1. Work the base skill: Server-based client, redirect URIs, Multi DC, secrets.
2. Settle the module list with the user, remembering it depends on the customer's edition (§3).
3. Build the scope set from §1 — and **copy the module-name spellings from the endpoint you will call**, not from
   the scopes overview (§1 is about why).
4. Add the field-metadata scope; a connector that maps fields needs it (§1).
5. Decide the API host deliberately: the dedicated Recruit domain or `zohoapis` (§4). This is a real fork.
6. Size the sync against the credit table, noting that a plain record read costs three (§5).
7. Run the base skill's round trip, plus the Recruit checks in §6.

## Recruit platform state (verified 2026-09-20 — re-verify before trusting)

- **Current API version is v2**, at `/recruit/v2/…`. There is no v3 documentation tree.
- **Two host families are documented, both by Zoho.** The Recruit v2 announcement gives a **dedicated domain**
  `https://recruit.zoho.com/recruit/v2/` and its per-DC siblings; the Recruit multi-DC page gives
  `https://www.zohoapis.eu/recruit/v2/Candidates`; and the token response's `api_domain` is a `zohoapis` host. §4.
- **Recruit's own docs give three different data-centre lists.** The v2 announcement names US, EU, CN, IN, JP — no
  Australia. The multi-DC page names US, AU, EU, IN, CN, JP — no Canada or Saudi Arabia. The access-and-refresh-token
  page names US, AU, EU, IN, CA, SA, CN, JP. **Use the base skill's list and `serverinfo`**, not any of these.
- **Module scope names appear in both singular and plural forms in Zoho's own examples**, and the service name appears
  as both `ZohoRecruit` and `ZohoRECRUIT`. §1.
- **A Recruit-specific token limit:** *"You can only generate a maximum of five refresh tokens in a minute."* This is
  on top of the base skill's 20-per-user cap, not instead of it.
- Limits are **credits** in a rolling 24-hour window, plus concurrency — and Recruit's concurrency is documented
  **per user per app**, not per org (§5).

If the Recruit docs do not look like this, stop and report what you actually see.

## 1. The Recruit scope families — and the spelling problem

Service name `ZohoRecruit`; operation types `ALL`, `READ`, `CREATE`, `UPDATE`, `DELETE`. Six families:

| Family | Reaches | Documented members |
| --- | --- | --- |
| **`modules`** | Record data | `modules.all` plus one per module (see §2) |
| **`settings`** | Configuration and metadata | `settings.all`, `settings.custom_views`, `settings.related_lists`, `settings.modules`, `settings.fields`, `settings.layouts` |
| **`users`** | The account's users | `users.all` |
| **`org`** | Organization details | `org.all` |
| **`bulk`** | Bulk read/write APIs | `bulk.all`, `bulk.read`, `bulk.create` |
| **`notifications`** | Change notifications | `notifications.read`, `.create`, `.update`, `.delete` |

As in CRM: **field metadata is a separate scope** (`ZohoRecruit.settings.fields.…`) and is not implied by any module
scope. A connector that maps Recruit fields to a unified model needs it, and will otherwise authorize cleanly and
fail on the metadata call.

### The inconsistency, stated plainly

Recruit's documentation spells module scope names three different ways, on pages that are all current:

- The **OAuth overview's scope table** lists them **singular**: `modules.candidate`, `modules.jobopening`,
  `modules.client`, `modules.contact`, `modules.interview`, `modules.application`.
- The **same page's worked examples** use **plural**: `ZohoRecruit.modules.candidates.ALL`,
  `ZohoRecruit.modules.jobopenings.ALL`.
- The **Get Records page** lists them **singular** again — `candidate, application, jobopening, interview, client,
  contact, review, department, task, event, vendor, campaign, submission, custommodule` — and writes the service
  name as **`ZohoRECRUIT`**.
- The **Modules API page** cites a scope in a fourth vocabulary entirely: `ZohoRecruit.setup.operation.all`, where
  neither `setup` nor `operation` appears in any scope table.

Operation-type casing is equally mixed: `.ALL`, `.all`, `.READ`, `.read` all appear in Zoho's examples.

**How to work with this rather than around it:**

1. **Take the spelling from the documentation page for the endpoint you are actually calling.** Each Recruit API page
   states its required scope at the top; that is closer to the implementation than the overview table.
2. **When a scope is rejected, try the other number and the other casing before concluding the scope does not
   exist.** `Invalid OAuth scope` does not say which member of a comma-separated list was wrong, so bisect (base
   skill §6).
3. **Test the exact string.** A scope set that has not been through a real authorize call is a hypothesis. This is the
   product where that matters most.
4. **`modules.all` sidesteps the whole problem** — at the cost of requesting every module. That is a real trade and
   §2 has the terms.

## 2. Modules: the ATS object model

The documented module scope names, from the OAuth overview's table:

`modules.all`, `candidate`, `client`, `contact`, `jobopening`, `campaign`, `task`, `event`, `call`, `interview`,
`custom`, `notes`, `assessment`, `candidatestatus`, `jobopeningstatus`, `todo`, `custommodule`, `vendors`,
`application`, `referral`, `department`, `assessment_submission`, `forecast`.

The ATS-shaped ones, and what an integration usually needs:

- **Candidates** — the people. The core object, and the one with the most custom fields in any real deployment.
- **Job Openings** — the requisitions. API name `Job_Openings`, so the plural-with-underscore form, not the label.
- **Applications** — the association between a candidate and a job opening. **This is a module in its own right**,
  and it is the one people forget. A connector scoped to candidates and job openings has both endpoints of the
  relationship and not the relationship: no pipeline, no stage, no per-requisition status.
- **Interviews** — scheduled interviews, distinct from Events.
- **Candidate Statuses** and **Job Opening Statuses** — the status vocabularies. If the product surfaces a pipeline
  stage as anything other than a raw string, it needs these, and they are separate scopes.
- **Assessments** and **Assessment Submissions** — separate modules, separately scoped.
- **Clients**, **Contacts**, **Vendors** — the agency-side objects (§3).
- **Departments**, **Referrals**, **Campaigns**, **Tasks**, **Events**, **Calls**, **Notes**, **To-Dos**,
  **Forecast** — the rest of the surface.

**Address everything by `api_name`.** Recruit generates an API name internally for every module, field and related
list, precisely so that a customer relabelling something does not break integrations. Standard modules' API names
cannot be changed; custom ones can.

**Discover the module list at run time.** `GET /settings/modules` returns the modules for that account, each with
flags worth reading before you assume anything: `creatable`, `deletable`, `editable`, `convertable` (for example,
whether Candidates can be converted into Contacts), `generated_type` (default, web, custom, linking) and
**`api_supported`**, which Recruit documents as `false` for modules *"currently not accessible by APIs"*. That last
one has no analogue in a scope list: a module can exist, be scoped, and still not be reachable.

**Custom modules** need `modules.custommodule` (or `modules.custom`, per the overview table — the two names appear in
different places, which is §1 again) or `modules.all`. As in CRM, you cannot scope a *specific* custom module in
advance, because you do not know its API name until the customer connects.

## 3. Staffing agency versus corporate HR

Zoho markets Recruit in two shapes, and they are not the same product from an integration's point of view:

- **Staffing agency** — the recruiter works for many client companies. Clients, client contacts, vendors,
  submissions and placements are first-class.
- **Corporate HR / in-house** — the recruiter works for one company, hiring into it. There is no client; the
  hiring-manager relationship is internal.

Consequences:

- **`modules.client`, `modules.contact` and `modules.vendors` are agency concepts.** Requesting them against a
  corporate customer is at best noise on the consent screen and at worst a module that returns nothing while your
  code waits for it.
- **Zoho does not publish a module-availability table by edition** that could be verified on 2026-09-20. So the
  module-discovery call in §2 is not a nicety — **it is the only reliable way to know what this customer has.**
  Build the integration to enumerate, not to assume.
- **Decide which shape the product targets before choosing scopes**, and say so in the handoff. "Supports Zoho
  Recruit" means something different to an agency and to an in-house team.

**Resumes and attachments** are the other ATS-specific surface, and the one to be most careful about: they are the
highest-value data in an ATS and the most sensitive. Confirm the exact scope from the attachment endpoint's own
documentation page before requesting anything — a general attachments page could not be located under the v2 guide
on 2026-09-20 (see "Unverified"), so do not infer the scope from a module name.

## 4. The API host — a genuine fork

Zoho documents two different hosts for the same Recruit API, and the token response suggests a third answer:

| Source | Host |
| --- | --- |
| Recruit "What's New in API v2" | `https://recruit.zoho.com/recruit/v2/` — *"a dedicated domain … helpful in serving CORS requests"*, with `recruit.zoho.eu`, `recruit.zoho.in`, `recruit.zoho.jp`, `recruit.zoho.com.cn` |
| Recruit Modules API and Get Records pages | `https://recruit.zoho.com/recruit/v2/…` |
| Recruit Multi DC page | `https://www.zohoapis.eu/recruit/v2/Candidates`, `https://www.zohoapis.com.cn/recruit/v2/Candidates` |
| The token response | `api_domain`, a `zohoapis` host |

The base skill's general rule — *take the host from `api_domain`* — is the right default for most Zoho services and
is **contradicted by Recruit's own endpoint documentation**, which uses the dedicated domain throughout.

What to do:

- **Pick one, deliberately, and record why.** Both appear in current Zoho documentation. This is not a case where one
  is obviously stale.
- **Map the DC to the host yourself if you use the dedicated domain**, because `api_domain` will not give you a
  `recruit.zoho.*` value. That means maintaining a DC-to-host map, with Recruit's three conflicting DC lists as the
  hazard (Platform state) — resolve it against the base skill's list and `serverinfo`, not Recruit's pages.
- **The failure is quiet.** A valid token against the wrong host is not an authorization error; it is a 404, an
  unexpected redirect, or an empty result, and it gets misdiagnosed as a scope problem every time.
- Whichever you choose, **store the resolved host on the connection** rather than reassembling it per call.

## 5. Credits, and concurrency counted per user

Recruit meters **credits** in a rolling 24-hour window, like CRM but with its own table:

| Edition | Allowed credits per 24h | Ceiling |
| --- | --- | --- |
| Free | 5,000 | 5,000 |
| Standard | 5,000 + (users × 250) + add-ons | 100,000 |
| Professional | 10,000 + (users × 500) + add-ons | 500,000 |
| Enterprise / Zoho One / People Plus | 15,000 + (users × 1,000) + add-ons | 1,000,000 |

Credit costs (1 unless listed):

| Operation | Credits |
| --- | --- |
| Get Users / Roles / Profiles; module list; field metadata; module metadata | 1 |
| Get IDs of deleted records | 2 |
| **Records fetched with a custom-view id** | **3** |
| Add / Remove tags | 1 per 50 records |
| Insert / Update | 1 per 10 records (max 100 per call, so max 10) |
| **Bulk Read Initialize** | **50** |
| **Bulk Write Initialize** | **500** |

**Read the "Get Records API" line carefully.** Recruit's own limits page opens by saying *"for a 'Get Records API',
3 credits will be reduced for a single API call"*, while the cost table attributes the 3 credits to reads that use a
custom-view id. The two readings differ by a factor of three for the most common call an ATS integration makes, and
Recruit does not reconcile them. **Budget at 3 and be pleasantly surprised** — on the Free edition's 5,000, the
difference is between roughly 1,600 and 5,000 reads a day.

**Concurrency is documented per user per app**, not per org: 5 (Free) / 10 (Standard) / 15 (Professional) /
20 (Enterprise, Zoho One, People Plus), with a **sub-concurrency limit of 10** for all editions covering sorted or
custom-view reads, multi-record writes, and searches invoked from functions. That is a meaningful difference from
CRM, where the same limit is per org — but do not over-read it: the credit budget is still the account's, so
parallelising across users buys concurrency, not allowance. Zoho states there is no per-minute rate limit; throughput
is bounded by concurrency.

## Product fact — what the Recruit connector asks for

> **As of 2026-09-20, Unified.to's Zoho Recruit connector requests**, for essentially every unified object it
> supports — candidates, jobs, applications, application statuses, interviews, scorecards, documents, activities,
> companies, contacts, groups, events — **the same three scopes**:
>
> - `ZohoRecruit.modules.all`
> - `ZohoRecruit.users.READ`
> - `ZohoRecruit.settings.fields.read`
>
> with a couple of variants escalating the users scope to `ZohoRecruit.users.ALL`. There is no per-module scoping.
>
> Its **identity/login probe** requests `ZohoRecruit.modules.candidate.ALL` — full read-write on the entire candidate
> module merely to identify the connecting user.
>
> Four things to raise with the connector's owner rather than copy:
>
> 1. **`modules.all` for everything** takes §1's spelling problem off the table and puts "full access to all
>    recruiting data" on the consent screen instead. Given §3 — that the module set varies by edition and must be
>    discovered anyway — that is a defensible trade, but it is a trade, and a security-conscious customer will ask.
> 2. **The requested strings mix casing** — `.all`, `.READ` and `.read` all appear across the set. That is Zoho's
>    inconsistency being mirrored rather than resolved, and it is a reason not to treat these strings as a canonical
>    reference.
> 3. **A whole-module grant as a login scope** is worth questioning on its own terms. A narrower identity scope, if
>    one works, is a better consent screen.
> 4. **It deliberately ignores the `api_domain` from the token response** and maps the customer's data centre to a
>    dedicated Recruit host instead — the §4 fork, resolved in favour of the dedicated domain. Its DC map also
>    carries a couple of region keys that are not Zoho DC codes, which is worth a look.
>
> Scope sets change. This note is dated, not live; confirm the current set with the connector's owner before
> submitting anything.

## 6. Recruit-specific end-to-end checks

On top of the base skill's authorize → refresh round trip:

1. Call `GET /settings/modules` and read the returned list — including each module's `api_supported` flag (§2). This
   is what tells you what this customer actually has.
2. Read one record from **Candidates** and one from **Job Openings** using their API names.
3. Read one **Application**, and confirm you can traverse it to both the candidate and the job opening. This is the
   test that catches a scope set covering the two ends and not the link (§2).
4. Read **field metadata** for Candidates. This fails when only module scopes were requested (§1).
5. Confirm which **host family** you are on (§4), and that the connection stores it.
6. If both staffing and corporate customers are in scope, test against **one of each** — or state plainly that only
   one shape has been proven (§3).
7. If custom modules are in scope, test against a real one, not a renamed standard module.
8. Sanity-check the sync's credit cost against §5, budgeting reads at 3 credits.

| Symptom | Cause |
| --- | --- |
| `Invalid OAuth scope` on a scope string from the docs | Singular/plural or casing mismatch — try the other form (§1) |
| Auth succeeds, field-metadata call fails | The field-metadata scope was not requested (§1) |
| Candidates and jobs work; pipeline or stage is missing | Applications and the status modules are separate scopes (§2) |
| A module returns nothing for one customer and data for another | Edition difference — agency versus corporate (§3) |
| A module is listed but unreachable by API | Its `api_supported` flag is `false` (§2) |
| Token is valid; calls 404 or return empty | Wrong host family — dedicated `recruit.zoho.*` versus `zohoapis` (§4) |
| Works in the US, fails elsewhere | Data centre; use the base skill's list, not Recruit's three conflicting ones (§4, base §4) |
| Refresh fails in bursts during onboarding | Five refresh tokens per minute, on top of the 20-per-user cap (Platform state, base §7) |
| `TOO_MANY_REQUESTS` | Concurrency or sub-concurrency, not the daily budget — reduce parallelism (§5) |
| Credits gone by mid-morning | Reads may be costing 3 each; check the pattern against §5 |

## Stop and ask

Beyond the base skill's list, hand back to a human when:

- It is unclear whether the product targets **staffing agencies, corporate HR, or both** (§3) — it changes the module
  list, the scope list and what "supported" means.
- Someone wants `modules.all` replaced with per-module scopes without a plan for the spelling inconsistency and for
  custom modules (§1, §2).
- The **host question** in §4 has not been settled and recorded — do not leave it to whichever page someone read last.
- **Resumes or attachments** are in scope and the exact endpoint scope has not been confirmed from that endpoint's
  own page (§3). This is candidate PII; guessing is not acceptable.
- A customer is hitting credit limits and the answer would be a volume or sync-frequency commitment (§5).

## Unverified as of 2026-09-20

- **The correct spelling of several module scope names.** Zoho's pages disagree (§1), and only a live authorize call
  settles it. Nothing here should be treated as the canonical list.
- **Whether the 3-credit cost applies to all record reads or only to custom-view reads** (§5). Recruit's limits page
  supports both readings.
- **Which modules exist in which edition.** No Zoho page publishing a module-availability table by edition could be
  located (§3). Discover at run time.
- **The scope required for resume and attachment endpoints.** No attachments page could be located under the v2
  developer guide (§3).
- **Which host family Zoho considers canonical** (§4). Both appear in current documentation.

## References

Recruit-specific only; the base skill carries the generic Zoho OAuth references. Verified to resolve on 2026-09-20.

- Zoho Recruit API home — https://www.zoho.com/recruit/developer-guide/apiv2/
- **OAuth 2.0 overview (scope families and the module scope table)** — https://www.zoho.com/recruit/developer-guide/apiv2/oauth-overview.html
- Register client — https://www.zoho.com/recruit/developer-guide/apiv2/register-client.html
- Access & refresh tokens (per-DC accounts URLs, five-per-minute limit) — https://www.zoho.com/recruit/developer-guide/apiv2/access-refresh.html
- Multi DC support — https://www.zoho.com/recruit/developer-guide/apiv2/multi-dc.html
- **API limits (credits, concurrency per user, sub-concurrency)** — https://www.zoho.com/recruit/developer-guide/apiv2/limits.html
- Modules API (`api_name`, `api_supported`, module discovery) — https://www.zoho.com/recruit/developer-guide/apiv2/modules-api.html
- Get Records (module scope names, `ZohoRECRUIT` casing) — https://www.zoho.com/recruit/developer-guide/apiv2/get-records.html
- What's New in API v2 (the dedicated `recruit.zoho.*` domain) — https://www.zoho.com/recruit/developer-guide/apiv2/whats-new.html
