The real problem
Why this is harder than it looks
Zendesk's REST API is clean and the documentation is thorough. You can build a working single-account prototype in an hour. The complexity arrives when you try to support real users in a multi-tenant product — and it's different from most connectors because Zendesk doesn't use OAuth 2.0 by default.
Zendesk authenticates via API token — a credential that is account-scoped, not user-scoped. That means a single token can impersonate any verified user on the entire Zendesk instance. In a multi-tenant product where each of your customers has their own Zendesk subdomain, you're now managing a credential vault: one (subdomain + email + API token) triple per customer, each pointing to a different base URL. There's no OAuth redirect flow to standardize the collection of these credentials — you have to build a UI that prompts each customer for their subdomain and token, store those credentials encrypted, and route every API call to the right subdomain. Get the routing wrong and you'll make API calls against the wrong customer's instance with no helpful error message.
Beyond credential collection, the operational problems compound: tokens don't expire, which sounds convenient until a customer rotates their Zendesk API token and your calls start returning 401s with no automated recovery path. You need revocation detection, re-credentialing flows, and per-account status tracking — all without the OAuth lifecycle hooks that other connectors provide. And if a customer has IP allowlists configured in their Zendesk Admin Center, your server IPs need to be explicitly permitted or calls will fail silently after successful credential setup.
Scalekit handles credential storage, per-tenant subdomain routing, and account status tracking. Your agent names a tool and passes parameters. The plumbing is not your problem.
Capabilities
What your agent can do with Zendesk
Once connected, your agent has 14 pre-built tools covering the Zendesk support workflow end-to-end:
- Create, read, and update tickets: open new tickets with priority, type, and assignee; fetch ticket details with sideloaded data; update status, routing, and tags
- Search and list tickets: full Zendesk search syntax with sorting and pagination; list tickets from any view or queue
- Reply to tickets and add internal notes: post public replies or agent-only internal notes to any ticket by ID
- Read ticket comments: fetch the full conversation thread including attachments and author metadata
- Manage users and organizations: create and look up end-users, agents, and admins; list and retrieve organizations
- Inspect groups and views: list agent groups and saved ticket views to understand routing and queue structure
Setup context
What we're building
This guide connects a support assistant agent to Zendesk — helping agents triage tickets, post replies, escalate issues, and look up customer records without leaving your product.
🤖
Example agent
Support assistant triaging tickets, posting replies, and managing Zendesk queues on behalf of each support rep
🔐
Auth model
API Key — each customer supplies their Zendesk subdomain, email, and API token. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Zendesk secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
npm install @scalekit-sdk/node
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENV_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const actions = scalekit.actions;
Already have credentials?
Connected Accounts
2 Per-User Auth: Creating connected accounts
Zendesk uses API Key authentication — there is no OAuth redirect flow. Each user's connected account is provisioned directly with their Zendesk subdomain, email address, and API token. The identifier is any unique string from your system.
In the Scalekit dashboard, go to Agent Auth → Connected Accounts for your Zendesk connection and click Add account. Supply the user's identifier, their Zendesk domain (e.g. yourcompany.zendesk.com), email address, and API token. Scalekit stores the credentials encrypted and routes every API call to the correct subdomain automatically.
response = actions.get_or_create_connected_account(
connection_name="zendesk",
identifier="user_zd_123" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: ACTIVE — credentials were supplied at account creation
const response = await actions.getOrCreateConnectedAccount({
connectionName: "zendesk",
identifier: "user_zd_123" // your internal user ID
});
const connectedAccount = response.connectedAccount;
console.log(`Status: ${connectedAccount.status}`);
// Status: ACTIVE — credentials were supplied at account creation
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Credential handling
3 Credential management
Because Zendesk uses API keys rather than OAuth, there is no user-facing authorization step. Credentials are entered once in the Scalekit dashboard (or via API) per connected account. Scalekit stores them encrypted and uses them on every tool call.
Credential storage is automatic
Once a connected account is provisioned with Zendesk credentials, Scalekit stores them in its encrypted
vault and the account is immediately ACTIVE. Every tool call is routed to the correct Zendesk subdomain
using the stored credentials — your agent code never touches them. If credentials become invalid (e.g. the
customer rotates their API token), the account moves to REVOKED. Check account.status before critical
operations and surface a re-credentialing prompt.
Generating a Zendesk API token
In Zendesk Admin Center, go to Apps and integrations → APIs → Zendesk API. Under Settings, enable Token
access, then click Add API token. Copy the token immediately — it is only shown once. The token authenticates
as the email address supplied alongside it, so use an account with the appropriate permissions for the
operations your agent will perform.
Calling Zendesk
4 Calling Zendesk: What your agent writes
With the connected account active, your agent calls Zendesk actions using execute_tool. Name the tool, pass parameters. Scalekit handles credential retrieval, subdomain routing, and response parsing.
Search tickets
Use Zendesk's native search syntax — the same strings you'd type in the Zendesk search bar. Filter by status, assignee, tag, date, or any ticket property. Returns up to 1000 results with pagination.
result = actions.execute_tool(
identifier="user_zd_123",
tool_name="zendesk_search_tickets",
tool_input={
"query": "type:ticket status:open assignee:me priority:urgent",
"sort_by": "updated_at",
"sort_order": "desc",
"per_page": 25
}
)
# Returns list of matching tickets with id, subject, status, priority, assignee
const result = await actions.executeTool({
identifier: "user_zd_123",
toolName: "zendesk_search_tickets",
toolInput: {
"query": "type:ticket status:open assignee:me priority:urgent",
"sort_by": "updated_at",
"sort_order": "desc",
"per_page": 25
}
});
// Returns list of matching tickets with id, subject, status, priority, assignee
Create a ticket
Open a new support ticket. Only comment_body is required — all other fields are optional and can be set at creation or updated later.
result = actions.execute_tool(
identifier="user_zd_123",
tool_name="zendesk_ticket_create",
tool_input={
"subject": "Login broken after SSO change",
"comment_body": "Customer reports they cannot log in after the SSO migration on March 28. Affects 3 accounts.",
"priority": "high",
"type": "problem",
"assignee_email": "tier2@support.example.com",
"tags": ["sso", "login", "escalated"]
}
)
# Returns: { "id": 9821, "subject": "...", "status": "new", ... }
const result = await actions.executeTool({
identifier: "user_zd_123",
toolName: "zendesk_ticket_create",
toolInput: {
"subject": "Login broken after SSO change",
"comment_body": "Customer reports they cannot log in after the SSO migration on March 28. Affects 3 accounts.",
"priority": "high",
"type": "problem",
"assignee_email": "tier2@support.example.com",
"tags": ["sso", "login", "escalated"]
}
});
// Returns: { "id": 9821, "subject": "...", "status": "new", ... }
Reply to a ticket
Post a public reply or internal note. Set public to false for internal notes visible only to agents — useful when your agent is summarizing context or escalation notes before a human takes over.
# Public reply to the customer
result = actions.execute_tool(
identifier="user_zd_123",
tool_name="zendesk_ticket_reply",
tool_input={
"ticket_id": 9821,
"body": "Thanks for reaching out. Our team is investigating and will follow up within 2 hours.",
"public": True
}
)
# Internal note for the agent queue
result = actions.execute_tool(
identifier="user_zd_123",
tool_name="zendesk_ticket_reply",
tool_input={
"ticket_id": 9821,
"body": "Escalated to Tier 2. Root cause likely the SSO config change deployed March 28 — see incident #4401.",
"public": False
}
)
// Public reply to the customer
const reply = await actions.executeTool({
identifier: "user_zd_123",
toolName: "zendesk_ticket_reply",
toolInput: {
"ticket_id": 9821,
"body": "Thanks for reaching out. Our team is investigating and will follow up within 2 hours.",
"public": true
}
});
// Internal note for the agent queue
const note = await actions.executeTool({
identifier: "user_zd_123",
toolName: "zendesk_ticket_reply",
toolInput: {
"ticket_id": 9821,
"body": "Escalated to Tier 2. Root cause likely the SSO config change deployed March 28 — see incident #4401.",
"public": false
}
});
Update a ticket
Change ticket status, reassign, update priority, or apply tags in a single call. Only the fields supplied are changed — all others remain untouched.
result = actions.execute_tool(
identifier="user_zd_123",
tool_name="zendesk_ticket_update",
tool_input={
"ticket_id": 9821,
"status": "pending",
"priority": "urgent",
"assignee_email": "oncall@support.example.com",
"tags": ["sso", "login", "escalated", "p1"]
}
)
const result = await actions.executeTool({
identifier: "user_zd_123",
toolName: "zendesk_ticket_update",
toolInput: {
"ticket_id": 9821,
"status": "pending",
"priority": "urgent",
"assignee_email": "oncall@support.example.com",
"tags": ["sso", "login", "escalated", "p1"]
}
});
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to look up or act on; Scalekit handles credential routing on every invocation. No credential plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
zd_tools = get_tools(
connection_name="zendesk",
identifier="user_zd_123"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a support assistant. Use the available tools to help manage Zendesk tickets, reply to customers, and look up user and organization information."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), zd_tools, prompt)
result = AgentExecutor(agent=agent, tools=zd_tools).invoke({
"input": "Find all urgent open tickets assigned to me, summarize them, and add an internal note to each flagging the SLA deadline"
})
import { ChatAnthropic } from "@langchain/anthropic";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { getTools } from "@scalekit-sdk/langchain";
const zdTools = getTools({
connectionName: "zendesk",
identifier: "user_zd_123"
});
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a support assistant. Use the available tools to help manage Zendesk tickets, reply to customers, and look up user and organization information."],
new MessagesPlaceholder("chat_history", true),
["human", "{input}"],
new MessagesPlaceholder("agent_scratchpad"),
]);
const agent = await createToolCallingAgent({
llm: new ChatAnthropic({ model: "claude-sonnet-4-6" }),
tools: zdTools,
prompt
});
const result = await AgentExecutor.fromAgentAndTools({
agent,
tools: zdTools
}).invoke({
input: "Find all urgent open tickets assigned to me, summarize them, and add an internal note to each flagging the SLA deadline"
});
Other frameworks supported
Tool reference
All 14 Zendesk tools
Grouped by object and capability. Your agent calls tools by name — no API wrappers to write.
Create a new ticket with subject, description, priority (urgent/high/normal/low), type, assignee email, status, and tags
Retrieve a specific ticket by ID. Supports sideloading related users, groups, and organizations in one request
Update any writable ticket field — status, priority, assignee, group, subject, or tags. Only supplied fields are changed
Post a public reply or internal note to a ticket. Set public to false for agent-only notes
zendesk_ticket_comments_list
List all comments and internal notes on a ticket including body, author, timestamps, and attachments
List tickets with sorting by created_at, updated_at, priority, or status. Supports pagination
Search tickets using Zendesk's full search syntax — filter by assignee, tag, date, status, type, and more. Returns up to 1000 results
Create a new user (end-user, agent, or admin) with name, email, phone, organization association, and verified status
Retrieve a specific user by ID including name, email, role, organization, and account status
List users filtered by role (end-user, agent, admin) with sorting and pagination
Retrieve a specific organization by ID including name, domain names, tags, notes, and shared ticket settings
zendesk_organizations_list
List all organizations in the account with pagination support
List all agent groups in the account — used for understanding ticket routing and queue structure
List saved ticket views filtered by access level (personal, shared, account) with sorting and pagination
Connector notes
Zendesk-specific behavior
API tokens are account-scoped, not user-scoped
A Zendesk API token paired with an email address authenticates as that user and can perform any action
that user is permitted to do. Unlike OAuth tokens, API tokens do not expire automatically — but they can
be manually revoked or rotated in Zendesk Admin Center. If a customer rotates their token, the connected
account will move to REVOKED. Surface a re-credentialing flow rather than returning a generic error.
Every customer has a different subdomain
Zendesk API calls go to https://{subdomain}.zendesk.com/api/v2/. Each of your customers operates on their
own subdomain — there is no shared base URL. Scalekit routes each tool call to the correct subdomain based
on the credentials stored for that connected account. Ensure the domain entered at account setup is the
full Zendesk domain, not a custom or host-mapped subdomain.
tags on zendesk_ticket_update replace, not append
The tags field in zendesk_ticket_update replaces the ticket's entire tag set. To add a tag without
removing existing ones, first fetch the current tags with zendesk_ticket_get, merge your new tag into
the list, and pass the full merged array to the update call.
Infrastructure decision
Why not build this yourself
The Zendesk API is documented. Credential storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Per-customer subdomain routing — every API call must go to the right customer's Zendesk instance, with the right credentials, with no shared base URL
PROBLEM 02
API tokens don't expire but they do get rotated — you need revocation detection and re-credentialing flows without the OAuth lifecycle hooks other connectors provide
PROBLEM 03
Per-user credential isolation in a multi-tenant system — one customer's Zendesk token must never route calls to another customer's instance
PROBLEM 04
Encrypted credential storage, account status tracking, and a UI for collecting customer API tokens at onboarding — all before you've written a single ticket operation
That's one connector. Your agent product will eventually need Salesforce, Gmail, GitHub, Linear, and whatever else your customers ask for. Each has its own auth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.