02 Get your data in /Integrations
Zendesk Support
Send Zendesk Support tickets and comments to Dimension Labs with the universal API.
- ZS ZendeskTickets, 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 Zendesk ticket’s public comments, from the customer and from your agents, as one conversation per ticket. You run it on your side: a small script pulls from the Zendesk API and posts each comment to the universal API.
What you need
Section titled “What you need”- A Zendesk Support account with admin access to Admin Center, to create an API token.
- A Zendesk API token and the email address of the user it acts as.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Zendesk
Section titled “1. Get API access in Zendesk”- In Admin Center, go to Apps and integrations > APIs > Zendesk API.
- Add an API token and copy it. You can have more than one active token.
- Authenticate with Basic auth, using
{email_address}/tokenas the username and the token as the password. With curl that is-u [email protected]/token:YOUR_API_TOKEN.
Zendesk marks API tokens as deprecated and recommends OAuth access tokens instead. Both are described on Zendesk’s security and authentication page. If you use an OAuth access token, send it as Authorization: Bearer YOUR_ACCESS_TOKEN and the rest of this guide is the same.
2. Pull new tickets
Section titled “2. Pull new tickets”Use the cursor-based incremental ticket export. It returns every ticket that changed since a time you give it.
- First call:
GET /api/v2/incremental/tickets/cursor?start_time=UNIX_SECONDS. - Later calls:
GET /api/v2/incremental/tickets/cursor?cursor=AFTER_CURSOR, using theafter_cursorfrom the last response. - Keep requesting
after_urluntilend_of_streamistrue. Store the lastafter_cursorfor the next run.
curl -u "$ZENDESK_EMAIL/token:$ZENDESK_API_TOKEN" \ "https://YOUR_SUBDOMAIN.zendesk.com/api/v2/incremental/tickets/cursor.json?start_time=1760313600"The export doesn’t include comments. For each ticket, call list comments: GET /api/v2/tickets/{ticket_id}/comments?page[size]=100. Comments come back oldest first. Follow links.next while meta.has_more is true (pagination).
Rate limits, per Zendesk: the incremental export endpoints allow 10 requests per minute. Other Support API calls are limited per minute by plan (200 on Team, 400 on Professional, 700 on Enterprise). A 429 response carries a Retry-After header with the seconds to wait (rate limits).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Zendesk | Notes |
|---|---|---|
text | comment plain_body | The comment as plain text. Skip comments where public is false: those are internal notes. |
userId | ticket requester_id | Zendesk’s internal user id for the customer, on both their comments and the agents’ replies. |
incoming | comment author_id equals ticket requester_id | true for the requester’s comments, false for everyone else’s. |
sessionId | ticket id | Prefix it, for example zendesk-123. |
conversationId | ticket id | Same value as sessionId. |
dimensionlabs_timestamp | comment created_at | Convert with toMillis. |
platformJson | ticket via, status, tags, brand_id, organization_id, satisfaction_rating | organization_id lets you join to account data. |
Personal data. Don’t send the requester’s email or name. Comment text often holds names, emails and phone numbers: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// zendesk.mjs. Environment: ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_TOKEN, DL_API_KEY.import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const base = `https://${process.env.ZENDESK_SUBDOMAIN}.zendesk.com/api/v2`;const auth = 'Basic ' + Buffer.from(`${process.env.ZENDESK_EMAIL}/token:${process.env.ZENDESK_API_TOKEN}`).toString('base64');
// Retries Zendesk's rate limits (429) and server errors (5xx), honoring Retry-After.async function get(url) { for (let attempt = 0; ; attempt++) { const res = await fetch(url, { headers: { Authorization: auth } }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Zendesk returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Sends every public comment written at or after `since` (Unix ms). Returns the new `since`.export async function syncZendesk({ since, historical = false }) { const startTime = Math.min(Math.floor(since / 1000), Math.floor(Date.now() / 1000) - 120); // Zendesk wants a time in the past let url = `${base}/incremental/tickets/cursor.json?start_time=${startTime}`; let newest = since; for (;;) { const page = await get(url); for (const ticket of page.tickets) { let next = `${base}/tickets/${ticket.id}/comments.json?page[size]=100`; while (next) { const res = await get(next); for (const c of res.comments) { const ts = toMillis(c.created_at); if (!c.public || ts < since || !c.plain_body?.trim()) continue; await sendToDimensionLabs({ text: c.plain_body, userId: String(ticket.requester_id), incoming: c.author_id === ticket.requester_id, sessionId: `zendesk-${ticket.id}`, conversationId: `zendesk-${ticket.id}`, dimensionlabs_timestamp: ts, platformJson: { via: ticket.via?.channel, status: ticket.status, tags: ticket.tags, brand_id: ticket.brand_id, organization_id: ticket.organization_id, satisfaction_rating: ticket.satisfaction_rating?.score }, }, { historical: historical || ts < Date.now() - HOUR }); newest = Math.max(newest, ts); } next = res.meta?.has_more ? res.links.next : null; } } if (page.end_of_stream) return newest; url = page.after_url; }}Each run starts the export from since, so a comment at the boundary can be sent twice. That’s fine: duplicates are removed automatically.
5. Run it
Section titled “5. Run it”Save the code as zendesk.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncZendesk as sync } from './zendesk.mjs';.
- History first:
node run.mjs 2025-01-01sends every comment since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new comments to
/track, and they are enriched automatically every day. A comment 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.
You can also upload a file of exported tickets with one row per comment (check it with the file checker), then run a backfill.
Zendesk reference
Section titled “Zendesk reference”Checked against Zendesk’s documentation on 2026-10-13.