02 Get your data in /Integrations
Zoho Desk
Send Zoho Desk tickets and their threads to Dimension Labs with the universal API.
- ZD Zoho DeskTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in each Zoho Desk ticket as one conversation: its threads, the customer’s messages and your agents’ replies, across email, chat, social and forum channels. Private threads and ticket comments (notes) stay out. You run it on your side: a small script pulls from the Zoho Desk API and posts each thread to the universal API.
What you need
Section titled “What you need”- A Zoho Desk user who can see the tickets you want, to create the OAuth client and approve it.
- Your Zoho data centre, which sets both the accounts server and the Desk API address (see below).
- A Dimension Labs API key for a new Universal API source. See Your API key.
| Data centre | Accounts server | Desk API |
|---|---|---|
| United States | https://accounts.zoho.com | https://desk.zoho.com |
| European Union | https://accounts.zoho.eu | https://desk.zoho.eu |
| India | https://accounts.zoho.in | https://desk.zoho.in |
| Australia | https://accounts.zoho.com.au | https://desk.zoho.com.au |
| Japan | https://accounts.zoho.jp | https://desk.zoho.jp |
| Canada | https://accounts.zohocloud.ca | https://desk.zohocloud.ca |
| Saudi Arabia | https://accounts.zoho.sa | https://desk.zoho.sa |
| Singapore | https://accounts.zoho.sg | https://desk.zoho.sg |
| United Arab Emirates | https://accounts.zoho.ae | https://desk.zoho.ae |
Zoho lists the accounts servers at accounts.zoho.com/oauth/serverinfo and the Desk addresses under “API Endpoints by Data Center” in the Zoho Desk API documentation. Zoho Desk also runs in China (https://desk.zoho.com.cn).
1. Get API access in Zoho Desk
Section titled “1. Get API access in Zoho Desk”Use a Zoho Self Client, which is meant for backend scripts that read your own account’s data (Self Client).
- Go to the Zoho API console, click GET STARTED, hover over Self Client and click CREATE NOW. Click CREATE, then OK.
- Copy the Client ID and Client Secret from the Client Secret tab.
- In the Generate Code tab, enter the scope
Desk.tickets.READ, pick an expiry time for the code (3 minutes by default), add a description and click CREATE. If asked, choose Zoho Desk and the portal. Copy the code. - Before the code expires, exchange it for a refresh token:
POST {accounts-server}/oauth/v2/token?client_id=...&client_secret=...&grant_type=authorization_code&code=.... Store therefresh_token: it doesn’t expire (authorization code flow). - The script gets a new access token from the refresh token on each run (
grant_type=refresh_token). Access tokens last one hour.
Send the access token as Authorization: Zoho-oauthtoken YOUR_TOKEN. The token is bound to one Zoho Desk organization, so the orgId header is optional; if you send it, it must match, or the call fails with OAUTH_ORG_MISMATCH. Zoho tells you to use the api_domain from the token response; the script reads the Desk address for your data centre from ZOHO_DESK_URL instead.
2. Pull new tickets
Section titled “2. Pull new tickets”The ticket list has no “updated since” filter, so the code sorts tickets by the time of the customer’s latest message, newest first, and reads back over a lookback window:
curl -H "Authorization: Zoho-oauthtoken $ZOHO_TOKEN" \ "https://desk.zoho.com/api/v1/tickets?sortBy=-customerResponseTime&from=0&limit=100"limitgoes from 1 to 100; page withfrom.sortByacceptsresponseDueDate,customerResponseTimeorcreatedTime, and a leading-sorts newest first.- To read specific departments, pass
departmentIds. Zoho notes that for the “All Departments” view you specify thedepartmentIds. - Each ticket carries
id,ticketNumber,contactId,accountId,channel,status,priority,departmentIdandcustomerResponseTime.
For each ticket, list its threads: GET /api/v1/tickets/{ticket_id}/threads?from=0&limit=200&sortBy=sendDateTime (limit up to 200, oldest first with sendDateTime). The list returns a summary of each thread. Get the full text with GET /api/v1/tickets/{ticket_id}/threads/{thread_id}?include=plainText, which adds a plainText field. Each thread has direction (in or out), author.type (END_USER or AGENT), visibility, status (SUCCESS, PENDING, FAILED or DRAFT), channel and createdTime.
Rate limits, per Zoho: API calls spend credits from a daily pool that depends on your edition and number of users, and resets every 24 hours. Listing tickets or threads costs 3 credits a call, more for records deep in a list; getting one thread costs 1. Concurrent calls are capped by edition. When the daily credits run out, Zoho returns 429 with a Retry-After header in seconds. Each response’s X-Rate-Limit-Remaining-v3 header shows the credits left for the day. Your current usage is under Setup > Developer Space > APIs.
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Zoho Desk | Notes |
|---|---|---|
text | thread plainText | Skip threads where visibility is private, and any whose status isn’t SUCCESS (drafts, failed sends). Ticket comments are notes and aren’t read. |
userId | ticket contactId | Zoho’s internal contact id, on every thread of the ticket. Fall back to the sessionId. |
incoming | thread direction | true when it is in, false when out. |
sessionId | ticket id | Prefix it, for example zoho-desk-1892000000042034. |
conversationId | ticket id | Same value as sessionId. |
dimensionlabs_timestamp | thread createdTime | Convert with toMillis. |
platformJson | ticket ticketNumber, channel, status, priority, departmentId, accountId, language; thread channel | accountId lets you join to account data. |
Personal data. Don’t send the ticket’s email or phone, the contact’s name, or the thread’s fromEmailAddress. Threads often hold names, emails and phone numbers: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// zoho-desk.mjs. Environment: ZOHO_ACCOUNTS_URL (for example https://accounts.zoho.eu), ZOHO_DESK_URL (for example https://desk.zoho.eu),// ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, ZOHO_REFRESH_TOKEN, DL_API_KEY.// Optional: ZOHO_ORG_ID, ZOHO_DEPARTMENT_IDS (comma-separated), ZOHO_LOOKBACK_DAYS (default 7).import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const DAY = 24 * HOUR;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const DESK = process.env.ZOHO_DESK_URL.replace(/\/$/, '');const LOOKBACK = Number(process.env.ZOHO_LOOKBACK_DAYS || 7) * DAY;const departments = process.env.ZOHO_DEPARTMENT_IDS ? `&departmentIds=${process.env.ZOHO_DEPARTMENT_IDS}` : '';let token;
async function newToken() { const params = new URLSearchParams({ client_id: process.env.ZOHO_CLIENT_ID, client_secret: process.env.ZOHO_CLIENT_SECRET, grant_type: 'refresh_token', refresh_token: process.env.ZOHO_REFRESH_TOKEN, }); const res = await fetch(`${process.env.ZOHO_ACCOUNTS_URL}/oauth/v2/token?${params}`, { method: 'POST' }); const body = await res.json(); if (!res.ok || !body.access_token) throw new Error(`Zoho token request failed: ${JSON.stringify(body)}`); token = body.access_token;}
// Gets a fresh token on 401, and retries rate limits (429) and server errors (5xx), honoring Retry-After.// Returns null for 204 No Content (an empty list).async function get(url) { for (let attempt = 0; ; attempt++) { if (!token) await newToken(); const headers = { Authorization: `Zoho-oauthtoken ${token}` }; if (process.env.ZOHO_ORG_ID) headers.orgId = process.env.ZOHO_ORG_ID; const res = await fetch(url, { headers }); if (res.status === 204) return null; if (res.ok) return res.json(); if (res.status === 401 && attempt === 0) { token = null; continue; } if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Zoho Desk returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Sends every public thread written at or after `since` (Unix ms). Returns the new `since`.export async function syncZohoDesk({ since, historical = false }) { token = null; // a new token for each run const cutoff = since - LOOKBACK; // tickets whose customer last wrote before this are not re-read let newest = since; for (let from = 0, done = false; !done; from += 100) { const tickets = (await get(`${DESK}/api/v1/tickets?sortBy=-customerResponseTime&from=${from}&limit=100${departments}`))?.data ?? []; if (tickets.length < 100) done = true; for (const t of tickets) { if (t.customerResponseTime && toMillis(t.customerResponseTime) < cutoff) { done = true; break; } for (let tf = 0; ; tf += 200) { const threads = (await get(`${DESK}/api/v1/tickets/${t.id}/threads?from=${tf}&limit=200&sortBy=sendDateTime`))?.data ?? []; for (const th of threads) { const ts = toMillis(th.createdTime); if (ts < since || th.visibility === 'private' || th.status !== 'SUCCESS') continue; const full = await get(`${DESK}/api/v1/tickets/${t.id}/threads/${th.id}?include=plainText`); const text = full?.plainText?.trim(); if (!text) continue; await sendToDimensionLabs({ text, userId: t.contactId ? String(t.contactId) : `zoho-desk-${t.id}`, incoming: th.direction === 'in', sessionId: `zoho-desk-${t.id}`, conversationId: `zoho-desk-${t.id}`, dimensionlabs_timestamp: ts, platformJson: { ticketNumber: t.ticketNumber, channel: th.channel, ticket_channel: t.channel, status: t.status, priority: t.priority, departmentId: t.departmentId, accountId: t.accountId, language: t.language, }, }, { historical: historical || ts < Date.now() - HOUR }); newest = Math.max(newest, ts); } if (threads.length < 200) break; } } } return newest;}An agent reply is picked up as long as the customer wrote on that ticket within the lookback window (ZOHO_LOOKBACK_DAYS, 7 days by default). A reply on a ticket where the customer has been quiet for longer is missed by the scheduled job: raise the lookback, or re-run a history load now and then. Re-sent threads are removed automatically.
5. Run it
Section titled “5. Run it”Save the code as zoho-desk.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncZohoDesk as sync } from './zoho-desk.mjs';.
- History first:
node run.mjs 2025-01-01sends every public thread since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new threads to
/track, and they are enriched automatically every day. A thread that is more than an hour old when the job sees it goes to/trackhistorical, and needs a backfill. - Re-runs are safe: duplicates are removed automatically.
Each run lists the threads of every ticket in the lookback window (3 credits each), plus 1 credit per new thread. Check that against your daily credits, and run less often or shorten the lookback if it uses too many; when the credits run out, the code waits for Zoho’s Retry-After. Archived tickets are listed separately (GET /api/v1/tickets/archivedTickets) and aren’t read here. You can also upload a file with one row per thread (check it with the file checker), then run a backfill.
Zoho Desk reference
Section titled “Zoho Desk reference”- Zoho Desk API documentation: tickets, threads, API credits, scopes and data centres
- Self Client overview
- Self Client: authorization code flow
- Zoho accounts servers by data centre
Checked against Zoho Desk’s documentation on 2026-10-26.