Zendesk
"Zendesk: Support API, tickets, users, custom fields, triggers, Web Widget, webhooks, OAuth, Node SDK"
Zendesk is an enterprise customer service platform built around a structured ticketing system. Integration should treat tickets as the atomic unit of work — every customer interaction maps to a ticket with defined lifecycle states, assignees, and SLA policies. Design integrations that automate ticket creation and routing, enrich tickets with external data via custom fields, and use triggers and webhooks to keep systems in sync. The Support API follows REST conventions with cursor-based pagination for large datasets. Always authenticate with OAuth tokens for production deployments, implement incremental exports for data synchronization, and respect rate limits to avoid throttling. ## Key Points - Use `create_or_update` endpoints for users and organizations to avoid duplicate records and simplify sync logic. - Prefer cursor-based incremental exports over search for bulk data synchronization — they are faster and not subject to the 1000-result search limit. - Set custom fields via their numeric field ID, not name. Cache the field ID mapping at startup with `GET /ticket_fields.json`. - Implement rate limit handling by reading `X-Rate-Limit` and `Retry-After` headers. Zendesk allows 700 requests per minute on professional plans. - Use internal notes (non-public comments) to log automated actions so agents see the audit trail without customers receiving noise. - Batch ticket updates with `update_many` when modifying more than 5 tickets to reduce API calls. - Always paginate with cursor-based pagination (`page[after]`) instead of offset pagination for large result sets. - **Using the search API for data synchronization** — Search results are eventually consistent and capped at 1000 results. Use incremental exports for reliable sync. - **Creating tickets without a requester** — Anonymous tickets cannot be tracked or followed up on. Always associate a requester by email or user ID. - **Storing API tokens in client-side code** — Zendesk tokens grant full API access. Always proxy requests through your backend. - **Ignoring job status for bulk operations** — Bulk updates return a job ID, not immediate results. Poll the job status endpoint to confirm completion before proceeding. - **Setting priorities without SLA policies** — Priority values are meaningless without corresponding SLA targets. Configure SLAs in Zendesk admin before using priority fields programmatically.
skilldb get customer-support-services-skills/ZendeskFull skill: 332 linesZendesk Integration
Core Philosophy
Zendesk is an enterprise customer service platform built around a structured ticketing system. Integration should treat tickets as the atomic unit of work — every customer interaction maps to a ticket with defined lifecycle states, assignees, and SLA policies. Design integrations that automate ticket creation and routing, enrich tickets with external data via custom fields, and use triggers and webhooks to keep systems in sync. The Support API follows REST conventions with cursor-based pagination for large datasets. Always authenticate with OAuth tokens for production deployments, implement incremental exports for data synchronization, and respect rate limits to avoid throttling.
Setup
Configure the Node client for API access:
import axios, { AxiosInstance } from "axios";
interface ZendeskConfig {
subdomain: string;
email: string;
token: string;
}
function createZendeskClient(config: ZendeskConfig): AxiosInstance {
const baseURL = `https://${config.subdomain}.zendesk.com/api/v2`;
return axios.create({
baseURL,
auth: {
username: `${config.email}/token`,
password: config.token,
},
headers: { "Content-Type": "application/json" },
timeout: 15000,
});
}
const zd = createZendeskClient({
subdomain: process.env.ZENDESK_SUBDOMAIN!,
email: process.env.ZENDESK_EMAIL!,
token: process.env.ZENDESK_API_TOKEN!,
});
// Verify connection
async function verifyConnection(): Promise<void> {
const { data } = await zd.get("/users/me.json");
console.log(`Connected as: ${data.user.name} (${data.user.email})`);
}
Embed the Web Widget on your frontend:
declare global {
interface Window {
zE: (...args: unknown[]) => void;
zESettings: Record<string, unknown>;
}
}
function initZendeskWidget(key: string): void {
window.zESettings = {
webWidget: {
color: { theme: "#1f73b7" },
contactForm: {
fields: [{ id: "description", prefill: { "*": "" } }],
},
helpCenter: { suppress: false },
chat: { suppress: false },
},
};
const script = document.createElement("script");
script.id = "ze-snippet";
script.src = `https://static.zdassets.com/ekr/snippet.js?key=${key}`;
script.async = true;
document.head.appendChild(script);
}
function identifyZendeskUser(name: string, email: string): void {
window.zE("webWidget", "identify", { name, email });
}
function openZendeskWidget(): void {
window.zE("webWidget", "open");
}
function prefillTicketForm(subject: string, description: string): void {
window.zE("webWidget", "prefill", {
name: { value: "", readOnly: false },
email: { value: "", readOnly: false },
description: { value: `**${subject}**\n\n${description}` },
});
}
Key Techniques
Ticket Management
interface TicketCreate {
subject: string;
description: string;
requesterId?: number;
assigneeId?: number;
priority?: "urgent" | "high" | "normal" | "low";
tags?: string[];
customFields?: Array<{ id: number; value: string }>;
}
async function createTicket(ticket: TicketCreate): Promise<number> {
const { data } = await zd.post("/tickets.json", {
ticket: {
subject: ticket.subject,
comment: { body: ticket.description },
requester_id: ticket.requesterId,
assignee_id: ticket.assigneeId,
priority: ticket.priority ?? "normal",
tags: ticket.tags ?? [],
custom_fields: ticket.customFields ?? [],
},
});
return data.ticket.id;
}
async function updateTicket(
ticketId: number,
updates: { status?: string; priority?: string; tags?: string[]; comment?: string; internal?: boolean }
): Promise<void> {
const payload: Record<string, unknown> = {};
if (updates.status) payload.status = updates.status;
if (updates.priority) payload.priority = updates.priority;
if (updates.tags) payload.tags = updates.tags;
if (updates.comment) {
payload.comment = { body: updates.comment, public: !(updates.internal ?? false) };
}
await zd.put(`/tickets/${ticketId}.json`, { ticket: payload });
}
async function bulkUpdateTickets(ticketIds: number[], updates: Record<string, unknown>): Promise<string> {
const ids = ticketIds.join(",");
const { data } = await zd.put(`/tickets/update_many.json?ids=${ids}`, { ticket: updates });
return data.job_status.id;
}
async function searchTickets(query: string, page: number = 1): Promise<{ tickets: unknown[]; hasMore: boolean }> {
const { data } = await zd.get("/search.json", {
params: { query: `type:ticket ${query}`, page, per_page: 100 },
});
return { tickets: data.results, hasMore: data.next_page !== null };
}
User and Organization Management
interface ZendeskUser {
name: string;
email: string;
role?: "end-user" | "agent" | "admin";
organizationId?: number;
userFields?: Record<string, unknown>;
}
async function createOrUpdateUser(user: ZendeskUser): Promise<number> {
const { data } = await zd.post("/users/create_or_update.json", {
user: {
name: user.name,
email: user.email,
role: user.role ?? "end-user",
organization_id: user.organizationId,
user_fields: user.userFields ?? {},
},
});
return data.user.id;
}
async function findUserByEmail(email: string): Promise<number | null> {
const { data } = await zd.get("/search.json", {
params: { query: `type:user email:${email}` },
});
return data.results.length > 0 ? data.results[0].id : null;
}
async function createOrganization(name: string, domains: string[]): Promise<number> {
const { data } = await zd.post("/organizations.json", {
organization: { name, domain_names: domains },
});
return data.organization.id;
}
Webhook Handler
import crypto from "node:crypto";
import type { Request, Response } from "express";
function verifyZendeskWebhook(req: Request, secret: string): boolean {
const signature = req.headers["x-zendesk-webhook-signature"] as string;
const timestamp = req.headers["x-zendesk-webhook-signature-timestamp"] as string;
if (!signature || !timestamp) return false;
const payload = timestamp + JSON.stringify(req.body);
const expected = crypto.createHmac("sha256", secret).update(payload).digest("base64");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
interface ZendeskWebhookPayload {
ticketId: number;
ticketStatus: string;
ticketPriority: string;
requesterEmail: string;
assigneeName?: string;
latestComment?: string;
tags: string;
}
function handleZendeskWebhook(req: Request, res: Response): void {
if (!verifyZendeskWebhook(req, process.env.ZENDESK_WEBHOOK_SECRET!)) {
res.status(401).json({ error: "Invalid signature" });
return;
}
const payload = req.body as ZendeskWebhookPayload;
if (payload.ticketPriority === "urgent") {
console.log(`URGENT ticket #${payload.ticketId} from ${payload.requesterEmail}`);
// Trigger PagerDuty or Slack alert
}
if (payload.ticketStatus === "solved") {
console.log(`Ticket #${payload.ticketId} solved by ${payload.assigneeName}`);
// Update internal tracking system
}
res.status(200).json({ received: true });
}
Incremental Export for Data Sync
async function incrementalTicketExport(startTime: number): Promise<{
tickets: unknown[];
nextStartTime: number;
hasMore: boolean;
}> {
const { data } = await zd.get("/incremental/tickets/cursor.json", {
params: { start_time: startTime },
});
return {
tickets: data.tickets,
nextStartTime: data.end_time,
hasMore: !data.end_of_stream,
};
}
async function fullIncrementalSync(since: number): Promise<unknown[]> {
const allTickets: unknown[] = [];
let startTime = since;
let hasMore = true;
while (hasMore) {
const batch = await incrementalTicketExport(startTime);
allTickets.push(...batch.tickets);
startTime = batch.nextStartTime;
hasMore = batch.hasMore;
if (hasMore) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
return allTickets;
}
OAuth Authentication Flow
import type { Request, Response } from "express";
function getZendeskOAuthURL(subdomain: string, clientId: string, redirectUri: string): string {
const params = new URLSearchParams({
response_type: "code",
redirect_uri: redirectUri,
client_id: clientId,
scope: "read write",
});
return `https://${subdomain}.zendesk.com/oauth/authorizations/new?${params}`;
}
async function exchangeOAuthCode(
subdomain: string,
code: string,
clientId: string,
clientSecret: string,
redirectUri: string
): Promise<string> {
const { data } = await axios.post(`https://${subdomain}.zendesk.com/oauth/tokens`, {
grant_type: "authorization_code",
code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
scope: "read write",
});
return data.access_token;
}
Best Practices
- Use
create_or_updateendpoints for users and organizations to avoid duplicate records and simplify sync logic. - Prefer cursor-based incremental exports over search for bulk data synchronization — they are faster and not subject to the 1000-result search limit.
- Set custom fields via their numeric field ID, not name. Cache the field ID mapping at startup with
GET /ticket_fields.json. - Implement rate limit handling by reading
X-Rate-LimitandRetry-Afterheaders. Zendesk allows 700 requests per minute on professional plans. - Use internal notes (non-public comments) to log automated actions so agents see the audit trail without customers receiving noise.
- Batch ticket updates with
update_manywhen modifying more than 5 tickets to reduce API calls. - Always paginate with cursor-based pagination (
page[after]) instead of offset pagination for large result sets.
Anti-Patterns
- Using the search API for data synchronization — Search results are eventually consistent and capped at 1000 results. Use incremental exports for reliable sync.
- Creating tickets without a requester — Anonymous tickets cannot be tracked or followed up on. Always associate a requester by email or user ID.
- Storing API tokens in client-side code — Zendesk tokens grant full API access. Always proxy requests through your backend.
- Ignoring job status for bulk operations — Bulk updates return a job ID, not immediate results. Poll the job status endpoint to confirm completion before proceeding.
- Setting priorities without SLA policies — Priority values are meaningless without corresponding SLA targets. Configure SLAs in Zendesk admin before using priority fields programmatically.
- Overusing triggers for integration logic — Zendesk triggers are limited and hard to debug. Use webhooks to push events to your own services where you have full control over business logic.
Install this skill directly: skilldb add customer-support-services-skills
Related Skills
Crisp
"Crisp: live chat widget, chatbot scenarios, CRM contacts, campaigns, helpdesk, REST API, JavaScript SDK, webhooks"
Drift
"Drift: conversational marketing, live chat, chatbots, meeting scheduling, contact management, REST API, webhooks, JavaScript SDK"
Freshdesk
"Freshdesk: ticket management, contact CRUD, automations, canned responses, REST API v2, webhooks, satisfaction surveys"
Help Scout
"Help Scout: conversation management, mailbox API, customer profiles, Beacon widget, webhooks, Docs knowledge base, REST API v2"
Intercom
"Intercom: Messenger widget, conversations API, custom bots, product tours, help center, user/company data, events, webhooks, Node SDK"
LiveChat
"LiveChat: real-time chat, ticket system, agent management, chat archives, customer SDK, REST API v3, webhooks, rich messages"