Summary and recommendation
The Zoho Desk REST API (v1, base URL: `https://desk.zoho.com/api/v1`) uses OAuth 2.0 with short-lived access tokens (1-hour TTL) and requires refresh token rotation via `https://accounts.zoho.com/oauth/v2/token`.
All requests must include the `orgId` HTTP header when the OAuth token spans multiple Zoho organizations - omitting it returns a 400 or silently targets the wrong org.
Data-center routing is the caller's responsibility: EU orgs must use `desk.zoho.eu`, AU orgs `desk.zoho.com.au`, and so on.
The API does not support bulk operations;
every agent or contact create/update is a discrete HTTP call.
Pagination uses a 1-based `from` index with a maximum `limit` of 100 records per page.
Rate limits vary by plan but are not publicly documented per endpoint;
HTTP 429 is returned on breach, with no `Retry-After` header - implement exponential backoff.
For automated, IdP-driven provisioning at scale, SCIM 2.0 is available on Enterprise (and Zoho One) via Zoho Directory, with a separate bearer token distinct from the REST OAuth credential
this is the recommended path for building an identity graph that stays synchronized with your IdP.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Enterprise |
Authentication
Auth method: OAuth 2.0
Setup steps
- Register an application at https://api-console.zoho.com to obtain a Client ID and Client Secret.
- Choose the appropriate OAuth grant type (Authorization Code for user-context, Client Credentials for server-to-server).
- Request the required Zoho Desk scopes (e.g., Desk.contacts.READ, Desk.agents.READ) during authorization.
- Exchange the authorization code for an access token and refresh token via https://accounts.zoho.com/oauth/v2/token.
- Include the access token in the Authorization header as 'Zoho-oauthtoken {access_token}' on all API requests.
- Use the refresh token to obtain new access tokens before expiry (typically 1 hour).
Required scopes
| Scope | Description | Required for |
|---|---|---|
| Desk.contacts.READ | Read contact (end-user) records | List/get contacts |
| Desk.contacts.WRITE | Create and update contact records | Create/update contacts |
| Desk.contacts.DELETE | Delete contact records | Delete contacts |
| Desk.agents.READ | Read agent (support staff) records | List/get agents |
| Desk.agents.WRITE | Create and update agent records | Create/update agents |
| Desk.settings.READ | Read portal/organization settings including roles and departments | Read roles, departments |
| Desk.settings.WRITE | Modify portal/organization settings | Update roles, departments |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Unique system-generated identifier for the agent or contact | auto-generated | immutable | Used as path parameter in all single-resource endpoints |
| firstName | string | First name of the agent or contact | optional | optional | |
| lastName | string | Last name of the agent or contact | required | optional | |
| string | Primary email address; used as login identifier for agents | required for agents | optional | Must be unique per organization for agents | |
| phone | string | Primary phone number | optional | optional | |
| mobile | string | Mobile phone number | optional | optional | |
| roleId | string | ID of the role assigned to the agent | optional | optional | Agents only; references a role object |
| departmentIds | array | List of department IDs the agent belongs to | optional | optional | Agents only |
| isActive | boolean | Whether the agent account is active | optional (defaults true) | optional | Agents only; set false to deactivate |
| type | string | Agent license type (e.g., FULLTIME, LIGHT) | optional | optional | Agents only; affects billing |
| accountId | string | ID of the account/company the contact belongs to | optional | optional | Contacts only |
| description | string | Free-text description or notes about the contact | optional | optional | Contacts only |
| string | Twitter handle of the contact | optional | optional | Contacts only | |
| createdTime | datetime (ISO 8601) | Timestamp when the record was created | auto-generated | immutable | |
| modifiedTime | datetime (ISO 8601) | Timestamp of last modification | auto-generated | auto-updated |
Core endpoints
List Agents
- Method: GET
- URL:
https://desk.zoho.com/api/v1/agents - Watch out for: The orgId header is required on all requests when the OAuth token has access to multiple Zoho organizations.
Request example
GET /api/v1/agents?from=1&limit=50
Authorization: Zoho-oauthtoken {access_token}
orgId: {orgId}
Response example
{
"data": [
{"id":"123","firstName":"Jane","lastName":"Doe","email":"jane@example.com","isActive":true}
]
}
Get Agent
- Method: GET
- URL:
https://desk.zoho.com/api/v1/agents/{agentId} - Watch out for: Returns 404 if the agent ID does not exist in the specified org.
Request example
GET /api/v1/agents/123
Authorization: Zoho-oauthtoken {access_token}
orgId: {orgId}
Response example
{
"id":"123","firstName":"Jane","lastName":"Doe",
"email":"jane@example.com","roleId":"456","isActive":true
}
Create Agent
- Method: POST
- URL:
https://desk.zoho.com/api/v1/agents - Watch out for: Creating an agent sends an invitation email to the specified address. Agent seat limits apply per plan.
Request example
POST /api/v1/agents
Content-Type: application/json
{"firstName":"John","lastName":"Smith","email":"john@example.com","roleId":"456"}
Response example
{
"id":"789","firstName":"John","lastName":"Smith",
"email":"john@example.com","isActive":true
}
Update Agent
- Method: PATCH
- URL:
https://desk.zoho.com/api/v1/agents/{agentId} - Watch out for: Only fields included in the request body are updated; omitted fields retain their current values.
Request example
PATCH /api/v1/agents/789
Content-Type: application/json
{"roleId":"999","isActive":false}
Response example
{
"id":"789","firstName":"John","lastName":"Smith",
"roleId":"999","isActive":false
}
List Contacts
- Method: GET
- URL:
https://desk.zoho.com/api/v1/contacts - Watch out for: Contacts are end-users (customers), not agents. Use the /agents endpoint for support staff.
Request example
GET /api/v1/contacts?from=1&limit=100
Authorization: Zoho-oauthtoken {access_token}
orgId: {orgId}
Response example
{
"data": [
{"id":"321","firstName":"Alice","lastName":"Brown","email":"alice@customer.com"}
]
}
Create Contact
- Method: POST
- URL:
https://desk.zoho.com/api/v1/contacts - Watch out for: Duplicate email addresses may be merged or rejected depending on org duplicate-check settings.
Request example
POST /api/v1/contacts
Content-Type: application/json
{"lastName":"Brown","firstName":"Alice","email":"alice@customer.com"}
Response example
{
"id":"321","firstName":"Alice","lastName":"Brown",
"email":"alice@customer.com","createdTime":"2024-01-15T10:00:00Z"
}
Update Contact
- Method: PATCH
- URL:
https://desk.zoho.com/api/v1/contacts/{contactId} - Watch out for: Partial updates only; full replacement (PUT) is not supported for contacts.
Request example
PATCH /api/v1/contacts/321
Content-Type: application/json
{"phone":"+1-555-0100","accountId":"654"}
Response example
{
"id":"321","firstName":"Alice","lastName":"Brown",
"phone":"+1-555-0100","accountId":"654"
}
Delete Contact
- Method: DELETE
- URL:
https://desk.zoho.com/api/v1/contacts/{contactId} - Watch out for: Deleting a contact is irreversible and may affect associated ticket history visibility.
Request example
DELETE /api/v1/contacts/321
Authorization: Zoho-oauthtoken {access_token}
orgId: {orgId}
Response example
HTTP 204 No Content
Rate limits, pagination, and events
- Rate limits: Zoho Desk enforces API call limits per organization per day. The official documentation states limits vary by plan but does not publish per-endpoint per-second limits publicly.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: Official docs do not explicitly document rate-limit response headers or Retry-After behavior. HTTP 429 is returned when limits are exceeded. Consult Zoho Desk API documentation for current plan-specific limits.
- Pagination method: offset
- Default page size: 50
- Max page size: 100
- Pagination pointer: from (1-based start index) and limit (page size)
| Plan | Limit | Concurrent |
|---|---|---|
| Free | Not publicly documented | 0 |
| Standard | Not publicly documented | 0 |
| Professional | Not publicly documented | 0 |
| Enterprise | Not publicly documented | 0 |
- Webhooks available: Yes
- Webhook notes: Zoho Desk supports webhooks (called 'Notifications' in the UI) that send HTTP POST payloads to a configured URL when specified events occur.
- Alternative event strategy: Zoho Desk also supports Automation Rules and Workflow rules that can trigger HTTP actions, usable as a webhook alternative for some event types.
- Webhook events: ticket.created, ticket.updated, ticket.statusChanged, contact.created, contact.updated, agent.created, agent.updated
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Enterprise
Endpoint: Provisioned via Zoho Directory; the SCIM base URL is generated within the Zoho Directory SCIM configuration panel and is specific to the connected IdP integration. No single static public endpoint is documented.
Supported operations: Create user (POST /Users), Read user (GET /Users/{id}), Update user (PUT /Users/{id}), Deactivate user (PATCH /Users/{id} with active:false), List users (GET /Users)
Limitations:
- SCIM provisioning requires SAML SSO to be configured first in Zoho Directory.
- Enterprise plan (or Zoho One) is required; not available on Free, Standard, or Professional plans.
- SCIM is managed through Zoho Directory, not directly through the Zoho Desk API.
- Group/role provisioning support via SCIM is not explicitly documented in official sources.
- The SCIM bearer token is generated in Zoho Directory and is separate from the Zoho Desk OAuth token.
Common scenarios
Three integration patterns cover the primary lifecycle operations.
To provision a new agent: obtain a token with Desk.agents.WRITE and Desk.settings.READ, resolve roleId via GET /api/v1/roles and departmentIds via GET /api/v1/departments, then POST /api/v1/agents
note that this immediately triggers an invitation email with no documented suppression mechanism.
To deactivate a departing agent: PATCH /api/v1/agents/{agentId} with {"isActive": false};
open ticket reassignment is not automatic and must be handled separately via ticket update endpoints or automation rules.
To sync contacts from an external CRM: paginate GET /api/v1/contacts?from=1&limit=100 to build a local index keyed by email, then upsert via PATCH or POST
duplicate-check behavior on email is org-configurable, so validate in a sandbox before running against production.
Provision a new support agent
- Obtain an OAuth 2.0 access token with Desk.agents.WRITE and Desk.settings.READ scopes.
- GET /api/v1/roles to retrieve available role IDs and identify the correct roleId.
- GET /api/v1/departments to retrieve department IDs if department assignment is needed.
- POST /api/v1/agents with firstName, lastName, email, roleId, and departmentIds in the request body.
- Store the returned agent id for future update or deactivation operations.
Watch out for: Agent creation sends an invitation email automatically. Ensure the email address is correct before calling the API; there is no undo for the invitation.
Deactivate a departing agent
- Obtain an OAuth 2.0 access token with Desk.agents.WRITE scope.
- GET /api/v1/agents to find the agent's id by email if not already known.
- PATCH /api/v1/agents/{agentId} with body {"isActive": false} to deactivate the account.
- Verify the response confirms isActive is false.
Watch out for: Deactivation via API does not automatically reassign open tickets. Ticket reassignment must be handled separately via ticket update endpoints or automation rules.
Sync contacts from an external CRM
- Obtain an OAuth 2.0 access token with Desk.contacts.READ and Desk.contacts.WRITE scopes.
- GET /api/v1/contacts?from=1&limit=100 and paginate through all pages to build a local index of existing contacts keyed by email.
- For each CRM contact: if email exists in index, PATCH /api/v1/contacts/{id} with changed fields; otherwise POST /api/v1/contacts to create.
- Handle 429 responses by backing off and retrying; Zoho does not document a Retry-After header so use exponential backoff.
Watch out for: Duplicate-check behavior on email is org-configurable. Test in a sandbox org first to confirm whether duplicate emails are rejected or silently merged.
Why building this yourself is a trap
The most common integration failure points are auth and identity graph drift. Access tokens expire after 1 hour; missing refresh rotation causes silent failures in long-running sync jobs.
The orgId header omission is a frequent source of cross-org data leakage in multi-tenant OAuth apps. SCIM provisioning runs through Zoho Directory - not the Desk REST API - meaning the SCIM bearer token, SCIM base URL, and supported operations (including PATCH /Users/{id} with active:false for deactivation) are all managed outside the Desk API surface;
teams that conflate the two auth systems end up with incomplete deprovision coverage in their identity graph. Group and role provisioning support via SCIM is not explicitly documented, so role assignment may require a follow-up REST API call after SCIM user creation.
Finally, deactivating an agent via isActive: false in the REST API does not guarantee immediate seat release - billing state is governed separately by Zoho's subscription system.
Automate Zoho Desk workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.