---
name: zoho-people-oauth-app
description: >-
  The Zoho People layer on top of the shared `zoho-api-console-oauth` skill —
  the forms/record data model where employee data lives in customer-configured
  forms addressed by formLinkName, so what the `forms` scope returns depends
  on configuration and the authorizing user's role; the documented and
  undocumented scope families; the three API generations and the
  `/people/api/` versus `/api/` path split; the `people.zoho.*` hosts that
  `api_domain` does not give you; the organization date format; and the
  per-endpoint threshold-and-lock rate model. Use when asked to get Zoho
  People OAuth credentials, choose Zoho People scopes, debug a People call
  returning 400 with code 7202 or an empty record set, or explain why one
  customer's form is invisible. 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 People OAuth2 App Registration

The client registration is the base skill's job. What is specific to People is that **there is almost nothing to
scope.** CRM scopes a module, Books scopes a ledger area; People has one `forms` scope that reaches every form in
the account — the Employee form, the Asset form, and whatever the customer built last quarter. You cannot narrow it
and you cannot enumerate it in advance, because forms are customer-created objects with customer-chosen link names.
What a token actually returns is decided after consent, by the customer's form configuration and the authorizing
user's role.

Two more People-specific things. **The published scope table is not the authority, and Zoho says so on the page
itself** — scopes in live use are documented only on the endpoint pages that need them. And **People is three APIs
wearing one product name**: a legacy forms API, a `v2` leave tracker, and a `v3` tree, all served from the same
host, all current, with no migration notice and no version in the base path you register against.

## 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, and taking the API host from `api_domain`.
- Capturing and rotating the secret, and limits being a per-organization concept.

Two of those need a People-shaped footnote rather than a repeat. **Taking the host from `api_domain` is not enough
here** — People does not live on the host `api_domain` names (§4). And the base's warning that Zoho's scope casing
drifts between pages does not apply to the People *prefix*, which is `ZOHOPEOPLE` in capitals on every Zoho page
checked; People drifts on the scope *name* instead (§2).

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 People-specific ones:

| Input | Notes |
| --- | --- |
| **Which HR areas must the integration reach?** | Forms, leave, attendance, time tracking are separate scope families (§2) |
| **Read-only, or read-write?** | `forms` has per-operation strings; most other families only publish `.ALL` (§2) |
| **Does the integration create or update records?** | A write-only scope set does not include read, and most write flows read back (§2) |
| **Will it touch custom forms?** | It already does — `forms` cannot exclude them, and it cannot name them either (§1) |
| **Which endpoint generation is the integration built on?** | Legacy, v2 and v3 coexist and are scoped and shaped differently (§3) |
| **Do you have a form and field permission map from the customer?** | Decides what the token can actually see (§1) |
| **What plan are target customers on?** | The API is not sold below Essential HR, and the daily cap scales by plan (§5) |

## Quick Start

1. Work the base skill: Server-based client, redirect URIs, Multi DC, secrets.
2. Settle which HR areas are in play, then build the scope set from §2 — and take the scope string from the
   endpoint page you intend to call, not from the scope table.
3. If anything creates or updates, add the matching read operation explicitly; write does not imply read (§2).
4. Pin the endpoint generation per call family and write it down — there is no single version in the base path (§3).
5. Resolve the API host to `people.zoho.{domain}`; do not call the host `api_domain` returns (§4).
6. Read the organization's date format once at connect and express every date parameter in it (§4).
7. Discover the customer's forms at run time rather than hardcoding link names (§1).
8. Size the sync against the **per-endpoint** threshold and its five-minute lock, not against a daily total (§5).
9. Run the base skill's round trip, plus the People checks in §6.

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

- **Two scope tables exist and they are identical** — `…/people/api/scopes.html` and `…/people/api/v3/scopes.html`
  list the same seven families. Neither is exhaustive (§2).
- **The API is a paid feature.** Zoho states it is "available in Essential HR, Professional, Premium, and Enterprise
  plans" — on both the legacy and the v3 overview pages, which are otherwise word-for-word identical and mention no
  version at all.
- **Three endpoint generations serve simultaneously**: legacy forms (`/api/forms/…`), a v2 leave tracker
  (`/api/v2/leavetracker/…`) and a v3 tree (`/api/v3/…`). **No Zoho People deprecation or end-of-life notice could
  be found on a Zoho People page** (§3, and see "Unverified").
- **There is no full-access scope.** No `ZohoPeople.fullaccess.all` equivalent is documented. The widest grant is
  the family-level `.ALL` — and `ZOHOPEOPLE.forms.ALL` is effectively that for HR record data (§2).
- **Limits are plain call counts with a per-endpoint throttle**, not credits: a per-plan daily cap plus a per-user-
  licence allowance, and every endpoint page carries its own "Threshold Limit … | Lock period" pair (§5).
- **People publishes no multi-DC or domain-specific-API-URL page**, unlike CRM, Recruit, Mail, Meeting, Sign and
  Bookings. The host pattern in §4 is derived from the documented US host plus live probing, not from a Zoho table.
- **Authentication failures on the forms family arrive as HTTP `400`, not `401`.** Zoho's own status-code page
  defines `400` as "The request **or the authentication** considered is invalid" (§5).

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

## 1. The data model is forms, and the customer owns it

Zoho People has no fixed object catalogue. Everything — employees, departments, designations, assets, exit
interviews, HR cases — is a **form**, and forms are things the customer creates and configures. `GET /api/forms`
returns the account's list, each entry carrying `displayName`, an `iscustom` flag, an `isVisible` flag, a
`PermissionDetails` block with separate `Add` / `Edit` / `View` levels, and — the field that matters — a
**`formLinkName`**.

Every record call is addressed by that link name: `/api/forms/{formLinkName}/getRecords`,
`/api/forms/json/{formLinkName}/insertRecord`, `/api/forms/{formLinkName}/getDataByID`. Zoho's admin guide is
explicit about why it exists: *"The Form Link name is a unique name associated with each form that helps during
integration. While integrating with third-party applications, you may have more than one form with the same name.
In this case, the system will take the Form Link Name to sync with third-party applications."* The customer types
that value in when they create the form.

Four consequences, and they are the whole reason this skill exists:

- **Scope granularity stops at `forms`.** There is no `ZOHOPEOPLE.forms.employee.READ`. Zoho's own Insert Record
  page demonstrates the scope `ZOHOPEOPLE.forms.CREATE` against a form called `test_form`, and the Adding Employees
  page demonstrates the *same string* against `employee`. One grant, every form. **So a connector that only needs
  department names is asking for the same access as one that reads salary history**, and a customer's security
  review will read it that way. There is no narrower ask available to offer them.
- **What the scope yields is decided by permissions you cannot see at registration.** Form-level permissions are
  set per role, and Zoho documents a separate field-level layer on top: *"To configure permissions for fields
  within a form, go to the corresponding service settings page and navigate to Permissions > Field Permissions."*
  Roles range from a team member limited to "My Data" up to an administrator with all data. The error codes exist
  specifically for this — `7040` permission denied to view records, `7038` to add, `7039` to edit, `7037` for the
  action generally. **A correct scope plus a restricted authorizing user is the single most common People support
  ticket, and it looks exactly like a broken integration.**
- **Discover forms at run time.** The list is per-customer and includes custom forms you have never seen. Hardcoding
  link names builds against your own trial account; `7011 Invalid form name` is what the next customer gets. The
  `isVisible` and `PermissionDetails` values on each entry tell you what is actually usable — read them rather than
  attempting every form and interpreting the failures.
- **Standard forms are not immutable.** A customer can clone a system form, add fields, or extend a service, and
  the extra fields come back in the record payload as ordinary keys. A mapping that assumes a fixed field set is a
  mapping against one org.

**Ask the customer for a form and field permission map before promising a field.** It is a five-minute question and
it is the difference between a scoped integration and an open-ended one.

## 2. The scope families — and why the scope table is not the authority

Service name `ZOHOPEOPLE`, capitalised. Operation types `CREATE`, `UPDATE`, `READ`, `DELETE`, `ALL`. The published
table lists seven families:

| Family | Documented operation types | Reaches |
| --- | --- | --- |
| **`forms`** | `ALL`, `CREATE`, `READ`, `UPDATE` | Every form and every record in it — employees, departments, designations, assets, custom forms (§1) |
| **`leave`** | `ALL`, `READ`, `CREATE`, `UPDATE` | Leave requests, leave types, balance and booked reports |
| **`attendance`** | `ALL` (plus `CREATE` and `UPDATE` on endpoint pages) | Check-in/out, entries, bulk import, regularization |
| **`timetracker`** | `ALL` (plus `READ` on endpoint pages) | Timesheets, jobs, projects, job schedules |
| **`employee`** | `ALL` | "Employee related operations" — no endpoint page found that requires it (see "Unverified") |
| **`dashboard`** | `ALL` | "Dashboard related operations" |
| **`automation`** | `ALL` | "Automation related operations" |

**The table is incomplete, and the page admits it.** Under "Scope name" the scopes page says: *"It may vary based on
the API you are using. You can find the appropriate scope on the corresponding API sample URLs in our help
documentation."* That is Zoho naming the endpoint page as the source of truth over its own table. Treat it that way.
Two families verified in the docs are absent from the table entirely:

- **`ZOHOPEOPLE.organization.READ`**, required by the Get Organization Info API — the call that returns the org's
  date format (§4), which a great many integrations cannot function without.
- **`ZOHOPEOPLE.hrprocess.READ`**, required by the v3 Get HR Process API.

And the table's own example text contradicts its own rows: it illustrates the grammar with
`ZOHOPEOPLE.timesheet.READ` and then lists the family as **`timetracker`**. The Get Job Schedule endpoint page
resolves it — `ZOHOPEOPLE.timetracker.ALL (or) ZOHOPEOPLE.timetracker.READ`. **Use `timetracker`.**

Four things to get right:

- **Take the string from the endpoint page you will call.** Not from the table, not from another product's skill,
  not from a connector you found. Every People endpoint page states its scope at the top; that is the contract.
- **Write does not imply read, and most write flows read.** `ZOHOPEOPLE.forms.CREATE` creates a record and returns
  an identifier — nothing else. Any flow that confirms the write, re-fetches the created record, or resolves a
  lookup value needs `ZOHOPEOPLE.forms.READ` in the *same* consent. A scope set built as "write" alone authorizes
  cleanly and then fails on the read-back with a permission error that reads like a role problem.
- **Most families publish only `.ALL`.** Only `forms` and `leave` document the full operation spread. For
  `attendance` and `timetracker` the narrower strings exist on endpoint pages but not in the table, so a read-only
  ask for those areas is something to verify against the endpoint page before promising it to a customer.
- **There is no full-access scope to reach for.** `ZOHOPEOPLE.forms.ALL` is the nearest thing and it is a very large
  ask: create, read, update and delete on every form in the HR system. Where read is enough, say `READ`.

## 3. Three API generations, none of them in the base path

Zoho People serves three endpoint families concurrently, from one host, with no version segment in the part you
register:

| Generation | Shape | Example | Scope stated on the page? |
| --- | --- | --- | --- |
| **Legacy forms** | `/api/forms/…`, `/api/forms/json/…/insertRecord`, `/api/attendance/…`, `/api/timetracker/…` | `/api/forms/employee/getRecords` | Usually yes |
| **v2 leave tracker** | `/api/v2/leavetracker/…` | `/api/v2/leavetracker/leaves/records` | **No** |
| **v3** | `/api/v3/…` | `/api/v3/leave-tracker/leaves`, `/api/v3/attendance/entries/{id}`, `/api/v3/organization` | Yes |

What to know before you pick one:

- **The v3 documentation tree is not a v3 API.** `…/people/api/v3/overview.html` is word-for-word identical to the
  unversioned overview and does not mention a version, a base path, or a migration. Only the individual v3 endpoint
  pages are genuinely new. There is no v3 index that tells you which calls have a v3 form.
- **The two leave generations differ in spelling as well as shape.** v2 is `leavetracker`, v3 is `leave-tracker`;
  v2 takes `from` / `to` / `startIndex` / `dataSelect`, v3 takes `from_date` / `to_date` / `offset` / `data_select`
  and adds sorting and department, location and leave-type filters. **These are not aliases.** A hyphen typed into a
  v2 path is a 404, and a v2 parameter name sent to v3 is a missing-mandatory-parameter error.
- **v2 pages state no scope at all.** Both Zoho pages documenting the v2 leave records endpoint omit the Scope line
  that every other People page carries. The v3 equivalent states `ZOHOPEOPLE.leave.READ`, and that is the string to
  use — but note you are inferring it, which is the one place in this file where the endpoint page could not be the
  authority.
- **`dataSelect` defaults to `MINE` on the v2 leave endpoint.** So an integration that authorizes as an HR admin and
  forgets the parameter reads *that admin's own leave* and nothing else. It returns `200`, it returns records, and
  it is wrong. v3's `data_select` defaults to `ALL` instead — a silent behaviour change between generations on the
  same logical call. Set it explicitly on both.
- **Pin the generation per call family and write it down.** There is no single version to pin, so "which version is
  this integration on?" has no one answer; it has one answer per endpoint family, and that is what has to be
  recorded.

**No Zoho People end-of-life or deprecation notice was found.** A "V2 API end-of-life extended to 31 December 2026"
announcement circulates widely and is signed by **the Zoho Projects team** — it is a Zoho Projects notice and says
nothing about Zoho People. Do not repeat it as a People fact.

## 4. Hosts, paths, and the organization date format

### The host `api_domain` gives you is the wrong one

The token response's `api_domain` is the generic Zoho APIs host. **Zoho People is not served there.** Every People
endpoint page documents the product host `people.zoho.{domain}`, and calling the generic host with a People path
does not reach the product.

The People hosts verified live on 2026-09-20 — all ten answered a v3 request with a Zoho People JSON auth error,
proving the product is served there:

```
people.zoho.com   people.zoho.eu    people.zoho.in   people.zoho.com.au   people.zoho.jp
people.zohocloud.ca   people.zoho.sa   people.zoho.uk   people.zoho.ae   people.zoho.com.cn
```

`people.zoho.ca` does not resolve. **Zoho publishes no People multi-DC table**, so this list is empirical: derive
the People host from the callback's data-centre key, keep the mapping in one place, and **fail loudly on a key you
do not recognise rather than defaulting to the US host** — a silent US default sends a European or Emirati
customer's HR data requests to a data centre their account does not exist in, and the result is an authorization
error that nobody reads as a region problem.

### `/people/api/` and `/api/` are the same thing

Zoho's docs are inconsistent within single pages: the Fetch Forms page gives the Request URL as
`https://people.zoho.com/people/api/forms` and the sample request as the same, while the Organization, Insert
Record and v2 leave pages give `https://people.zoho.com/api/…`, and the v3 leave page prints both forms — the
documented URL with `/people` and the sample without.

**Verdict: both work.** Probed unauthenticated on 2026-09-20, `/people/api/forms` and `/api/forms` returned the
identical error body, and that body echoes the path the server resolved: `"uri":"/api/forms"` in both cases. The
same held for `/people/api/v3/organization` versus `/api/v3/organization` and for the v2 leave path. The server
normalises the `/people` prefix away. Pick one, use it consistently, and do not treat a doc page's prefix as
meaningful.

### Every date is in the organization's format

This is the People gotcha with no analogue in the sibling products. Date parameters are not ISO — Zoho's attendance
page says *"Specify the date in organisation date format"* and the v3 attendance page says punch times are *"in
organization date and time format"*. The format is a per-organization setting.

- **Read it once and cache it on the connection.** `GET /api/v3/organization` returns the org's `DateFormat`; the
  call needs `ZOHOPEOPLE.organization.READ` (§2) and carries a threshold of 20 requests.
- **It is a Zoho format token string, not your date library's.** Expect values like `dd/MM/yyyy`; translating those
  tokens into whatever your platform parses with is a real conversion step, not a copy.
- **Ambiguous dates are the failure mode.** `03/04/2026` parses without error in either convention and is wrong in
  one of them. Nothing errors; a record lands in the wrong month. Several endpoints accept an explicit
  `dateFormat` parameter — use it wherever it exists rather than relying on the org default.
- **A non-admin authorizing user may not be able to read the org record at all.** Plan a fallback format and treat
  the lookup as best-effort, not as a connect-time hard dependency.

## 5. Limits, and the error shapes

People meters **calls**, not credits. Two independent mechanisms.

**A per-plan daily cap, plus a per-user-licence allowance:**

| Plan | Allowance | Daily ceiling |
| --- | --- | --- |
| Essential HR | 250 calls / user licence | 5,000 calls/day |
| Professional | 250 calls / user licence | 10,000 calls/day |
| Premium | 250 calls / user licence | 15,000 calls/day |
| Enterprise | 500 calls / user licence | 25,000 calls/day |

There is no free tier here — the API is not part of the plans below Essential HR at all.

**A per-endpoint threshold with a lock period**, printed on each endpoint page as *"Threshold Limit: N requests |
Lock period: M minutes"*, and defined there as *"Threshold Limit - Number of API calls allowed within a minute.
Lock Period - Wait time before consecutive API requests."* Observed values on 2026-09-20:

| Endpoint | Threshold / minute | Lock |
| --- | --- | --- |
| Get Organization Info; v3 Edit Attendance Entry | **20** | 5 minutes |
| Fetch Forms; Fetch Record; Get Related Records; Custom Views; v3 Get Leave Requests; v3 Get HR Process | **30** | 5 minutes |
| Get Job Schedule | **50** | 5 minutes |
| Insert Record | **100** | 5 minutes |
| Update Record; Adding Employees | **300** | 5 minutes |
| Bulk Records (`getRecords`); Search Records; Fetch Single Record | **400** | 5 minutes |

Read that as a design brief, because it inverts the usual intuition:

- **The throttle is per endpoint, so a total call budget tells you nothing.** Paging bulk records at 400/min is
  comfortable; looping the same volume through Fetch Forms at 30/min is not. The binding constraint is whichever
  single endpoint your sync leans on hardest.
- **Metadata calls are the tightest.** Fetch Forms is 30/min and Get Organization Info is 20/min — the two calls a
  connector makes to *discover* what it is working with. Cache both on the connection; re-deriving them per sync is
  how a connector locks itself out of the calls it needs before it reads a single record.
- **The penalty is a lock, not a retry-after.** Five minutes of waiting, not a slot freeing up next second. A tight
  retry loop against a locked endpoint does not recover; it extends the outage.
- **`429` is ambiguous.** Zoho's status-code page defines it as *"The number of API requests for the 24 hour period
  is exceeded **or** the concurrency limit of the user for the app is exceeded"* — but the limits page publishes no
  concurrency number, and neither does any endpoint page. So a `429` may be the daily cap, the per-endpoint
  threshold, or an unpublished concurrency ceiling, and the response does not say which.

### Three error envelopes from one product

People does not have one error shape. Probed on 2026-09-20, the same host returned three:

| Family | Shape | Status |
| --- | --- | --- |
| Legacy forms, timetracker | `{"response":{"message":…,"uri":…,"errors":{"code":7202,…}}}` | **400** |
| v3 | `{"code":"INVALID_AUTHTOKEN","message":…}` | **401** |
| v2 leave tracker | `{"error":{"code":7019,…},"uri":…}` | **400** |

**An expired or wrong token on the forms family is an HTTP `400`, not a `401`.** Zoho's status-code page states it
as design: `400` is *"The request or the authentication considered is invalid."* Any error handler that treats
`401` as "refresh the token" and `400` as "the caller sent something wrong" will retry a dead token forever on the
forms family and never refresh it. Key off the `7202` code, not the status.

The People error-code list is worth reading once in full; the ones that matter for an OAuth conversation are
`7202` invalid auth token, `7037`/`7038`/`7039`/`7040` the permission-denied family (§1), `7011` invalid form name,
`7019` missing parameter, and `7024` no records found.

## Product fact — what the People connector asks for

> **As of 2026-09-20, Unified.to's Zoho People connector requests** a per-unified-object scope set rather than one
> flat list, and all five strings are real — but two of them are not on Zoho's scope table:
>
> - **Employee, department and designation reads:** `ZOHOPEOPLE.forms.READ` plus `ZOHOPEOPLE.organization.READ`.
>   The second is the date-format lookup (§4) and is **absent from both published scope tables** — it appears only
>   on the Get Organization Info endpoint page.
> - **Employee, department and designation writes:** `ZOHOPEOPLE.forms.CREATE` **alone**, with no read operation.
> - **Leave:** `ZOHOPEOPLE.leave.READ` plus `ZOHOPEOPLE.organization.READ`.
> - **Time tracking:** `ZOHOPEOPLE.timetracker.READ` — **also absent from the scope tables**, which list only
>   `timetracker.ALL`; it is documented on the Get Job Schedule page (§2).
> - **No full-access scope is requested**, consistent with §2: People does not document one.
>
> Behaviour worth knowing, all of it consistent with this file:
>
> - It takes OAuth **client ID and secret per connection** rather than operating one platform-wide Zoho app, so each
>   customer's People account can be wired to its own console client.
> - It resolves the API host to the `people.zoho.*` product host (§4) rather than trusting the generic host from the
>   token response, and it re-resolves after every refresh — the refresh response carries the generic host and
>   would otherwise overwrite a correct one.
> - It reads and caches the organization's date format at connect time and **soft-fails** when the authorizing user
>   cannot read it, falling back to a fixed format (§4).
> - It sends the leave endpoint's data selector explicitly rather than accepting the `MINE` default (§3).
> - It treats a successful HTTP status with a business-error body as a failure on the forms family, which is the
>   right shape given §5.
>
> Three things to raise with the connector's owner rather than copy:
>
> 1. **The write scope set has no read operation in it.** A consent requested for writes alone carries
>    `ZOHOPEOPLE.forms.CREATE` and nothing else, while the create flow re-fetches the new record to return it. That
>    read needs `ZOHOPEOPLE.forms.READ` in the same grant (§2). It is invisible whenever a read set was requested
>    alongside — which is the common case, and is why it survives.
> 2. **The data-centre map omits the UAE**, which is live for Zoho People (§4), and an unmapped region falls through
>    to the **US host** rather than failing. That is the silent-wrong-region failure §4 warns about.
> 3. **Credentials fall back to another Zoho product's app** when per-connection credentials are absent, rather than
>    refusing. A token minted under a client whose consent was granted for a different Zoho service is not a People
>    token, and the resulting failure names neither the client nor the product.
>
> Scope sets change. This note is dated, not live; confirm the current set with the connector's owner before
> submitting anything.

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

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

1. Call `GET /api/forms` and **look at the response, not just the status**. It proves the `forms` scope, and it
   gives you the account's real `formLinkName` list, `iscustom` flags and `PermissionDetails` (§1).
2. Read one record from each form the integration claims, **by link name**, on an account with at least one custom
   form. A trial org with only stock forms has not tested §1.
3. **Authorize as a restricted, non-admin user** and confirm you can tell a permission failure (`7037`–`7040`) apart
   from a scope failure. This is the test that predicts your support load.
4. Call `GET /api/v3/organization`, confirm the date format comes back, and confirm your code converts the Zoho
   format tokens rather than passing them through (§4).
5. Send one date parameter in the **wrong** convention deliberately and confirm you notice. If nothing errors, the
   integration has an ambiguity bug it will not report (§4).
6. Exercise **each endpoint generation** the integration uses — a legacy forms call, a v2 leave call, a v3 call —
   and confirm the parameter names and the leave data selector are right for each (§3).
7. Confirm the API host is `people.zoho.{domain}` and not the host the token response named (§4).
8. Hammer one **metadata** endpoint past 30 calls in a minute and confirm you get the five-minute lock and back off
   rather than retrying (§5).
9. Present a stale token to a **forms** endpoint and confirm the handler refreshes on the `400`/`7202` pair rather
   than only on `401` (§5).

| Symptom | Cause |
| --- | --- |
| Auth succeeds, every forms call is `400` with code `7202` | The token is invalid or expired — People reports auth failures as `400` on this family (§5) |
| Handler never refreshes an expired token on forms calls | It keys off `401`; the forms family returns `400` (§5) |
| Auth succeeds, a form returns `7011 Invalid form name` | Hardcoded link name; this customer's form is named differently or does not exist (§1) |
| Auth succeeds, records come back empty or `7040` | The authorizing user's role, or form/field permissions — not the scope (§1) |
| Some fields are missing for one customer only | Field Permissions on that form, or a form the customer customized (§1) |
| Create works, then the read-back fails | Write scope requested without `forms.READ` in the same consent (§2) |
| `Invalid OAuth scope` on a scope you read in Zoho's docs | Taken from the scope table rather than the endpoint page — or `timesheet` used where the family is `timetracker` (§2) |
| Leave list returns only the connecting admin's own leave | v2 `dataSelect` left at its `MINE` default (§3) |
| A leave endpoint 404s | `leavetracker` versus `leave-tracker` — the v2 and v3 paths are spelled differently (§3) |
| v3 call rejects a parameter that works on v2 | v3 renamed them (`from_date`, `offset`, `data_select`) (§3) |
| Token valid, People path does not answer | Calling the generic Zoho APIs host instead of `people.zoho.{domain}` (§4) |
| Works for US customers, fails for one region | That region is missing from the host map and fell back to the US host (§4) |
| Dates land in the wrong month, no error | Organization date format not read, or read and not converted (§4) |
| Requests start failing for five minutes at a time | A per-endpoint threshold lock, not the daily cap — and retrying extends it (§5) |
| `429` with no further detail | Could be the daily cap, the endpoint threshold, or an unpublished concurrency limit; Zoho does not distinguish (§5) |

## Stop and ask

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

- A customer's security review objects to `forms` access and somebody wants a narrower scope offered — **there is
  none** (§1), and that is a product conversation, not a console setting.
- Nobody has a form and field permission map from the customer and someone wants a field-level commitment (§1).
- A scope string cannot be found on any endpoint page, only inferred from the table or from another integration
  (§2) — the `employee`, `dashboard` and `automation` families are all in this state.
- The integration will create or update and the scope set has not been checked for a matching read (§2).
- The endpoint generation for a call family has not been decided, or a customer is on an older one (§3).
- Customers exist in a data centre that is not in the host map, and somebody proposes defaulting rather than
  failing (§4).
- A **Zoho People** deprecation date is being cited — verify it against a Zoho People page first; the widely
  circulated one belongs to a different Zoho product (§3).
- Sync volume must be committed to and nobody has mapped it onto the **per-endpoint** thresholds rather than the
  daily cap (§5).
- The customer is below Essential HR — they do not have API access to sell you (Platform state).

## Unverified as of 2026-09-20

- **No endpoint page requiring `ZOHOPEOPLE.employee.*`, `ZOHOPEOPLE.dashboard.*` or `ZOHOPEOPLE.automation.*` could
  be found.** All three are on the scope table with a one-line description and nothing else. They may be live, or
  historical; do not build a consent screen around them without testing.
- **The v2 leave endpoints publish no scope.** `ZOHOPEOPLE.leave.READ` is inferred from the v3 equivalent and from
  the Booked and Balance report, which does state it (§3).
- **No Zoho People API deprecation, sunset or end-of-life date exists on a Zoho People page.** Three generations
  serve concurrently with no migration notice (§3).
- **Zoho documents no concurrency number for People**, although the status-code page names a concurrency limit as a
  cause of `429` (§5).
- **Whether the People Sandbox is reachable by API is undocumented.** Zoho describes it as Enterprise-only,
  replicating configuration and optionally "the first 30 records from the selected forms", deployed to production
  through a change tracker — but says nothing about an API host, a separate token, or whether OAuth reaches it.
- **Zoho publishes no People multi-DC page.** The host list in §4 is live-probed, not documented.

## References

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

- API overview (plan availability) — https://www.zoho.com/people/api/overview.html
- V3 API overview (identical text, no version stated) — https://www.zoho.com/people/api/v3/overview.html
- OAuth steps (authorize and token URLs, comma-separated scopes) — https://www.zoho.com/people/api/oauth-steps.html
- **OAuth scopes table** — https://www.zoho.com/people/api/scopes.html
- **OAuth scopes table (V3 tree)** — https://www.zoho.com/people/api/v3/scopes.html
- **API limits (per-plan daily cap and per-licence allowance)** — https://www.zoho.com/people/api/api-limits.html
- **Status codes (400 covers authentication; 429 is ambiguous)** — https://www.zoho.com/people/api/status-codes.html
- **Error codes (7202, 7037–7040, 7011, 7019)** — https://www.zoho.com/people/api/error-codes.html
- **Fetch Forms API (`formLinkName`, `iscustom`, `PermissionDetails`)** — https://www.zoho.com/people/api/forms-api/fetch-forms.html
- Get Bulk Records (`/forms/{formLinkName}/getRecords`, 200-record cap) — https://www.zoho.com/people/api/bulk-records.html
- Search Records — https://www.zoho.com/people/api/forms-api/search-record.html
- Insert Record (same `forms.CREATE` scope against a custom form) — https://www.zoho.com/people/api/insert-records.html
- Update Record — https://www.zoho.com/people/api/update-records.html
- Adding Employees (same scope against a stock form) — https://www.zoho.com/people/api/adding-employees.html
- **Get Organization Info (`ZOHOPEOPLE.organization.READ`, the date format)** — https://www.zoho.com/people/api/Organization/Get-details-api.html
- Fetch Leave Records API V2 (`dataSelect` defaults to `MINE`, no scope stated) — https://www.zoho.com/people/api/get-records-v2.html
- Get Leave Record API (the same v2 endpoint, documented at a different path) — https://www.zoho.com/people/api/get_record.html
- **Get Leave Requests API v3 (`leave-tracker`, renamed parameters)** — https://www.zoho.com/people/api/v3/leave-tracker/get-leave.html
- Leave Booked and Balance Report — https://www.zoho.com/people/api/leave/reports/bookedandbalance.html
- Attendance Entries (organisation date format) — https://www.zoho.com/people/api/attendance-entries.html
- Edit Attendance Entry v3 — https://www.zoho.com/people/api/v3/attendance/entries-edit.html
- Get Job Schedule (`ZOHOPEOPLE.timetracker.READ`) — https://www.zoho.com/people/api/timesheet/get-job-schedule.html
- Get HR Process v3 (`ZOHOPEOPLE.hrprocess.READ`) — https://www.zoho.com/people/api/v3/get-hrprocess.html
- **Creating and editing forms (Form Link Name, custom forms)** — https://www.zoho.com/people/help/adminguide/permissions-forms.html
- Setting up permissions (form-level and field-level) — https://help.zoho.com/portal/en/kb/people/administrator-guide/common-settings/articles/setting-up-permissions-zoho-people
- Zoho People Sandbox (Enterprise only, configuration + 30 records) — https://help.zoho.com/portal/en/kb/people/administrator-guide/operations/data-administration/articles/sandbox
