Summary and recommendation
The ServiceDesk Plus Cloud API (v3) exposes separate resource trees for requesters (`/api/v3/requesters`) and technicians (`/api/v3/technicians`), each with distinct endpoints, scopes, and licensing consequences.
Authentication on Cloud is OAuth 2.0 issued by Zoho Accounts (`accounts.zoho.com/oauth/v2/token`) - not ManageEngine's own domain - requiring an app registration in the Zoho API Console and the `SDPOnDemand.technicians.ALL` or `SDPOnDemand.users.ALL` scope depending on the resource.
On-Premise uses a static API key passed as `TECHNICIAN_KEY`; the base URL structure and endpoint paths differ entirely from Cloud v3. When building an identity graph across your stack, map both `requester.id` and `technician.id` per user, since a single employee can exist in both resource trees simultaneously and each must be deprovisioned independently.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (Cloud); API Key via request header (On-Premise) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: OAuth 2.0 (Cloud); API Key via request header (On-Premise)
Setup steps
- Cloud (OAuth 2.0): Register an application in the Zoho API Console at api-console.zoho.com.
- Cloud: Select 'Server-based Application' or 'Self Client', choose the SDPOnDemand scope (SDPOnDemand.users.ALL or granular scopes).
- Cloud: Generate authorization code, exchange for access_token and refresh_token via POST to https://accounts.zoho.com/oauth/v2/token.
- Cloud: Pass access token in Authorization header: 'Authorization: Zoho-oauthtoken {access_token}'.
- On-Premise: Navigate to Admin > API > Generate API Key in the ServiceDesk Plus web console.
- On-Premise: Pass the key as query parameter 'TECHNICIAN_KEY={api_key}' or in the request header.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| SDPOnDemand.users.ALL | Full read/write access to user (requester) records | Create, read, update, delete requesters/users |
| SDPOnDemand.users.READ | Read-only access to user records | List and get requester/user details |
| SDPOnDemand.users.WRITE | Write access to create and update user records | Create and update requesters/users |
| SDPOnDemand.technicians.ALL | Full access to technician (agent) records | Manage technician accounts |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | long | Unique system-generated identifier for the user | auto-generated | read-only | Required in URL path for update/delete operations |
| name | string | Full display name of the requester | required | optional | |
| email_id | string | Primary email address of the user | required | optional | Used as login identifier for cloud; must be unique |
| phone | string | Primary phone number | optional | optional | |
| mobile | string | Mobile phone number | optional | optional | |
| department | object | Department the user belongs to; contains id and name | optional | optional | Pass as {"id": " |
| job_title | string | User's job title | optional | optional | |
| employee_id | string | Employee ID from HR system | optional | optional | |
| location | object | Site/location object; contains id and name | optional | optional | |
| is_vip_user | boolean | Marks user as VIP for priority handling | optional | optional | |
| is_technician | boolean | Indicates if the user is a technician/agent | read-only | read-only | Technicians are managed via separate /technicians endpoint |
| user_scope | string | Access scope: GLOBAL_SCOPE or SITE_SCOPE | optional | optional | |
| time_zone | object | User's time zone preference | optional | optional | |
| language | object | Preferred language for UI | optional | optional | |
| created_time | datetime | Timestamp when the user record was created | auto-generated | read-only |
Core endpoints
List Users (Requesters)
- Method: GET
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/requesters - Watch out for: All query parameters must be JSON-encoded and passed as the 'input_data' query parameter, not as standard query params.
Request example
GET /api/v3/requesters?input_data={"list_info":{"row_count":10,"start_index":0}}
Authorization: Zoho-oauthtoken {access_token}
Response example
{
"requesters": [{"id":"123","name":"Jane Doe","email_id":"jane@example.com"}],
"list_info": {"total_count":50,"row_count":10,"start_index":0}
}
Get User (Requester) by ID
- Method: GET
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/requesters/{id} - Watch out for: Returns a single 'requester' object (not array) wrapped in the key 'requester'.
Request example
GET /api/v3/requesters/123
Authorization: Zoho-oauthtoken {access_token}
Response example
{
"requester": {
"id": "123",
"name": "Jane Doe",
"email_id": "jane@example.com",
"department": {"id":"10","name":"IT"}
}
}
Create User (Requester)
- Method: POST
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/requesters - Watch out for: Body must be form-encoded with 'input_data' key containing JSON string, not raw JSON body with Content-Type: application/json.
Request example
POST /api/v3/requesters
Content-Type: application/x-www-form-urlencoded
input_data={"requester":{"name":"John Smith","email_id":"john@example.com","job_title":"Engineer"}}
Response example
{
"requester": {
"id": "456",
"name": "John Smith",
"email_id": "john@example.com"
},
"response_status": {"status_code":2000,"status":"success"}
}
Update User (Requester)
- Method: PUT
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/requesters/{id} - Watch out for: Uses PUT (full-style), but only fields included in input_data are updated; omitted fields are not cleared.
Request example
PUT /api/v3/requesters/456
Content-Type: application/x-www-form-urlencoded
input_data={"requester":{"job_title":"Senior Engineer","phone":"555-1234"}}
Response example
{
"requester": {"id":"456","name":"John Smith","job_title":"Senior Engineer"},
"response_status": {"status_code":2000,"status":"success"}
}
Delete User (Requester)
- Method: DELETE
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/requesters/{id} - Watch out for: Deleting a requester who has open tickets may fail or require ticket reassignment first.
Request example
DELETE /api/v3/requesters/456
Authorization: Zoho-oauthtoken {access_token}
Response example
{
"response_status": {
"status_code": 2000,
"status": "success"
}
}
List Technicians
- Method: GET
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/technicians - Watch out for: Technicians (agents) are a separate resource from requesters (end users); they consume licensed seats.
Request example
GET /api/v3/technicians?input_data={"list_info":{"row_count":10,"start_index":0}}
Authorization: Zoho-oauthtoken {access_token}
Response example
{
"technicians": [{"id":"789","name":"Alice Tech","email_id":"alice@example.com"}],
"list_info": {"total_count":5,"row_count":5}
}
Create Technician
- Method: POST
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/technicians - Watch out for: Creating a technician consumes a licensed technician seat. Exceeding seat count returns an error.
Request example
POST /api/v3/technicians
Content-Type: application/x-www-form-urlencoded
input_data={"technician":{"name":"Bob Agent","email_id":"bob@example.com"}}
Response example
{
"technician": {"id":"999","name":"Bob Agent","email_id":"bob@example.com"},
"response_status": {"status_code":2000,"status":"success"}
}
Search Users by Email
- Method: GET
- URL:
https://{domain}.sdpondemand.manageengine.com/api/v3/requesters - Watch out for: search_fields must be nested inside list_info in the input_data JSON; field names are case-sensitive.
Request example
GET /api/v3/requesters?input_data={"list_info":{"search_fields":{"email_id":"jane@example.com"}}}
Authorization: Zoho-oauthtoken {access_token}
Response example
{
"requesters": [{"id":"123","name":"Jane Doe","email_id":"jane@example.com"}],
"list_info": {"total_count":1}
}
Rate limits, pagination, and events
- Rate limits: ManageEngine does not publicly document specific rate limit thresholds for ServiceDesk Plus Cloud API. Limits are enforced per account/plan but exact numbers are not published in official docs.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: No official rate limit documentation found. Contact ManageEngine support for account-specific limits. On-premise limits depend on server resources.
- Pagination method: offset
- Default page size: 100
- Max page size: 100
- Pagination pointer: list_info.row_count and list_info.start_index
| Plan | Limit | Concurrent |
|---|---|---|
| Standard | Not publicly documented | 0 |
| Professional | Not publicly documented | 0 |
| Enterprise | Not publicly documented | 0 |
- Webhooks available: Yes
- Webhook notes: ServiceDesk Plus Cloud supports webhooks (called 'Notification Rules' or 'Custom Triggers') that can POST to external URLs when ticket or user events occur. Configuration is done via Admin > Automation > Custom Triggers.
- Alternative event strategy: For polling-based sync, use the List Requesters endpoint with date-based filtering on created_time or last_updated_time fields.
- Webhook events: Request created, Request updated, Request closed, User (requester) added, Technician added
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- ServiceDesk Plus (Cloud and On-Premise) does NOT natively support SCIM 2.0 provisioning.
- Other ManageEngine products such as PAM360 and Identity360 do support SCIM.
- Okta and Entra ID integrations with ServiceDesk Plus use SSO (SAML) only, not SCIM provisioning.
- User provisioning must be done via the REST API or manual import (CSV).
- The context scim_reference notes Enterprise plan requirement but this refers to other ME products, not ServiceDesk Plus.
Common scenarios
Three lifecycle operations require explicit sequencing. For provisioning, POST to /api/v3/requesters with a form-encoded body (application/x-www-form-urlencoded) where all fields are nested inside a single input_data key as a JSON string - raw application/json bodies return a 400.
For department or attribute sync, there is no bulk update endpoint; each user requires a separate PUT call, so implement retry logic and back-off for large orgs. For offboarding, the DELETE on /api/v3/requesters/{id} will be blocked if the user has open tickets; query /api/v3/requests filtered by `requester.
idfirst, reassign or close tickets, then delete. If the user also holds a technician account, a separate DELETE to/api/v3/technicians/{technician_id}` is required to free the licensed seat - omitting this step leaves a billable ghost record.
Provision a new employee as a requester on hire
- POST /api/v3/requesters with input_data containing name, email_id, department.id, job_title, employee_id.
- Capture the returned requester.id for future reference in your HR system.
- Optionally assign the user to a site/location via the location field in the same create call.
- If the employee is also an IT agent, separately POST /api/v3/technicians with their details to create a technician account (consumes a license seat).
Watch out for: Body must be form-encoded (application/x-www-form-urlencoded) with input_data as a JSON string. Raw JSON bodies will return a 400 error.
Sync department changes from HR system
- Query your HR system for users with department changes since last sync.
- For each changed user, GET /api/v3/requesters?input_data={"list_info":{"search_fields":{"email_id":"{email}"}}} to find their SDP requester ID.
- PUT /api/v3/requesters/{id} with input_data containing the updated department object {"id": "
"}. - Log the response_status.status_code (2000 = success) for audit trail.
Watch out for: There is no bulk update endpoint; each user update requires a separate API call. For large orgs, implement rate limiting and retry logic in your sync script.
Deprovision a user on offboarding
- GET /api/v3/requesters with search_fields on email_id to retrieve the requester ID.
- Check if the user has open tickets by querying /api/v3/requests with requester.id filter.
- Reassign or close any open tickets before proceeding.
- DELETE /api/v3/requesters/{id} to remove the requester record.
- If the user was also a technician, separately DELETE /api/v3/technicians/{technician_id} to free the license seat.
Watch out for: DELETE will fail if the user has open/pending tickets. There is no 'deactivate' or 'suspend' state for requesters in ServiceDesk Plus - deletion is the only deprovisioning option via API.
Why building this yourself is a trap
The most common integration failure point is the non-standard request encoding: every write operation and every filtered list query must encode parameters as a JSON string inside the input_data form field, not as a JSON body or standard query string.
Pagination uses list_info.start_index (0-based) and list_info.row_count inside the same input_data object, with a hard maximum of 100 records per page. Rate limits are not publicly documented for any plan tier and no Retry-After header is returned on throttle responses - callers must implement conservative back-off without guidance from the API itself.
Finally, ServiceDesk Plus has no SCIM 2.0 endpoint; any IdP integration (Okta, Entra ID) covers SAML SSO only, meaning the REST API is the sole path for programmatic lifecycle management in this product.
Automate ManageEngine 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.