To integrate Replit with Zendesk, generate an API token from your Zendesk account settings, store it in Replit Secrets (lock icon π), and call the Zendesk Support API from your Python or Node.js backend to create tickets, manage users, update ticket fields, and receive real-time webhook events. Deploy with Autoscale for webhook endpoints or Reserved VM for scheduled ticket processing jobs.
Why Connect Replit to Zendesk?
Zendesk is the dominant customer support platform for small to enterprise businesses, and its comprehensive REST API makes it a powerful integration point for automation. Connecting Replit to Zendesk enables you to build custom workflows that extend Zendesk's native capabilities: auto-creating tickets from application events, enriching tickets with data from external systems, triggering escalations based on custom business logic, or building internal dashboards that pull ticket metrics into your preferred reporting tool.
The most common use cases involve bidirectional data flow. Your application creates Zendesk tickets when errors or user issues are detected, Zendesk webhooks notify your Replit server when ticket status changes so you can update your own database, or a Replit job processes tickets on a schedule to apply routing logic too complex for Zendesk's native automations.
Difference from Intercom: Zendesk is ticket-based with SLA tracking, formal escalation paths, and organization-level management. Intercom is conversation-first, optimized for real-time chat with product-led engagement. Zendesk's API reflects this structure: tickets have stages, assignees, priorities, and custom fields that map well to formal support process automation. Store Zendesk credentials in Replit Secrets (lock icon π in the sidebar) β the API token grants full access to your support data.
Integration method
You connect Replit to Zendesk by generating an API token from your Zendesk account's Admin Center, storing it with your email in Replit Secrets, and calling the Zendesk Support REST API from your server-side Python or Node.js code. The API supports creating and updating tickets, managing users and organizations, searching ticket queues, and receiving real-time webhook events when tickets change. Authentication uses HTTP Basic Auth with email/token credentials.
Prerequisites
- A Replit account with a Python or Node.js project created
- A Zendesk account (Support Team plan or higher for API token access)
- Admin access to your Zendesk instance to generate API tokens and configure webhooks
- Your Zendesk subdomain (the part before .zendesk.com in your URL, e.g., 'yourcompany')
- Node.js 18+ or Python 3.10+ (both available on Replit by default)
Step-by-step guide
Generate a Zendesk API Token
Generate a Zendesk API Token
Log in to your Zendesk instance and click the Admin Center icon (the cog or grid icon) in the left sidebar. Navigate to Apps and Integrations > APIs > Zendesk API. On the Settings tab, ensure 'Token Access' is enabled (toggle it on if it is off). Then click the 'Add API token' button. Enter a description like 'Replit Integration' and click 'Create'. Zendesk will display the token once β copy it immediately before closing the dialog. The token looks like a long alphanumeric string starting with a number like 6wiIBWbGkBMo1jRiknWC0m... You will authenticate using your Zendesk admin email address and the API token together. The Basic Auth format is: {email}/token:{api_token}. For example, if your email is admin@company.com and your token is abc123, the username for Basic Auth is admin@company.com/token:abc123. Also note your Zendesk subdomain from the URL bar β it is the part before .zendesk.com. If your Zendesk URL is yourcompany.zendesk.com, your subdomain is 'yourcompany'. The API base URL uses this subdomain.
Pro tip: Create a dedicated Zendesk agent account for API integrations (e.g., api-integration@yourcompany.com) rather than using your personal admin account's token. This makes it easier to manage permissions and audit API usage, and prevents the integration from breaking if personal credentials change.
Expected result: You have a Zendesk API token, your admin email address, and your Zendesk subdomain copied and ready to add to Replit Secrets.
Store Zendesk Credentials in Replit Secrets
Store Zendesk Credentials in Replit Secrets
Open your Replit project and click the lock icon π in the left sidebar to open the Secrets pane. Add the following secrets: - Key: ZENDESK_SUBDOMAIN β Value: your Zendesk subdomain (e.g., 'yourcompany', not the full URL) - Key: ZENDESK_EMAIL β Value: the email address of the Zendesk account that generated the token - Key: ZENDESK_API_TOKEN β Value: the API token you generated Click 'Add Secret' after each entry. The Base64-encoded credentials for Basic Auth will be constructed in code as: {email}/token:{api_token}. Access them in Python: os.environ['ZENDESK_SUBDOMAIN'] os.environ['ZENDESK_EMAIL'] os.environ['ZENDESK_API_TOKEN'] In Node.js: process.env.ZENDESK_SUBDOMAIN etc. Restart your Repl after adding secrets. Never hardcode these in source files β Zendesk API tokens grant broad access to all tickets, users, and organizations in your support system.
Pro tip: You can also use OAuth 2.0 for Zendesk API access if you are building a multi-tenant app where customers connect their own Zendesk accounts. For single-instance automation, API token authentication is simpler and sufficient.
Expected result: ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, and ZENDESK_API_TOKEN appear in the Replit Secrets pane and are accessible as environment variables.
Create and Manage Tickets with Python
Create and Manage Tickets with Python
The Zendesk API base URL format is https://{subdomain}.zendesk.com/api/v2. Authentication uses HTTP Basic Auth where the username is email/token:{api_token} and the password is any string β by convention, you pass the same email/token string or just the token as the password. The most common pattern is requests.get(url, auth=(f'{email}/token:{token}', '')). Tickets are the core object in Zendesk. A ticket has a requester (the customer), an assignee (the agent), a subject, a description (the first comment), a priority, a status, and custom fields. Creating a ticket requires at minimum a subject and a comment body. The Python module below provides a complete Zendesk client with ticket creation, updates, search, and comment management. Install requests with pip install requests.
1import os2import requests3from typing import Optional45SUBDOMAIN = os.environ["ZENDESK_SUBDOMAIN"]6EMAIL = os.environ["ZENDESK_EMAIL"]7TOKEN = os.environ["ZENDESK_API_TOKEN"]8BASE_URL = f"https://{SUBDOMAIN}.zendesk.com/api/v2"910# Zendesk Basic Auth: email/token:api_token as username11AUTH = (f"{EMAIL}/token:{TOKEN}", "")1213HEADERS = {14 "Content-Type": "application/json",15 "Accept": "application/json"16}1718def create_ticket(19 subject: str,20 body: str,21 requester_email: str,22 requester_name: str = "",23 priority: str = "normal", # low, normal, high, urgent24 tags: list = None,25 custom_fields: list = None26) -> dict:27 """28 Create a new support ticket.29 Returns the created ticket object.30 """31 payload = {32 "ticket": {33 "subject": subject,34 "comment": {"body": body},35 "requester": {36 "email": requester_email,37 "name": requester_name or requester_email38 },39 "priority": priority,40 "tags": tags or [],41 "custom_fields": custom_fields or []42 }43 }44 response = requests.post(f"{BASE_URL}/tickets.json", json=payload, auth=AUTH, headers=HEADERS)45 response.raise_for_status()46 return response.json()['ticket']4748def get_ticket(ticket_id: int) -> dict:49 """Retrieve a ticket by ID."""50 response = requests.get(f"{BASE_URL}/tickets/{ticket_id}.json", auth=AUTH, headers=HEADERS)51 response.raise_for_status()52 return response.json()['ticket']5354def update_ticket(55 ticket_id: int,56 status: str = None,57 priority: str = None,58 assignee_email: str = None,59 internal_note: str = None,60 tags: list = None61) -> dict:62 """Update ticket fields and optionally add an internal note."""63 ticket_update = {}64 if status:65 ticket_update['status'] = status # open, pending, hold, solved, closed66 if priority:67 ticket_update['priority'] = priority68 if assignee_email:69 ticket_update['assignee_email'] = assignee_email70 if tags:71 ticket_update['tags'] = tags72 if internal_note:73 ticket_update['comment'] = {74 'body': internal_note,75 'public': False # False = internal note, True = public reply76 }77 response = requests.put(78 f"{BASE_URL}/tickets/{ticket_id}.json",79 json={'ticket': ticket_update},80 auth=AUTH,81 headers=HEADERS82 )83 response.raise_for_status()84 return response.json()['ticket']8586def search_tickets(query: str, sort_by: str = "created_at", sort_order: str = "desc") -> list:87 """88 Search tickets using Zendesk search syntax.89 Examples:90 - 'status:open priority:high'91 - 'requester:user@example.com status:new'92 - 'created>2026-03-01 status:open'93 """94 params = {95 'query': f'type:ticket {query}',96 'sort_by': sort_by,97 'sort_order': sort_order98 }99 response = requests.get(f"{BASE_URL}/search.json", params=params, auth=AUTH, headers=HEADERS)100 response.raise_for_status()101 return response.json().get('results', [])102103def add_public_comment(ticket_id: int, comment: str) -> dict:104 """Add a public reply to a ticket (visible to the requester)."""105 return update_ticket(ticket_id, internal_note=None)106 # Use update_ticket with public: True comment instead107108def bulk_update_tickets(ticket_ids: list, updates: dict) -> dict:109 """110 Update multiple tickets at once.111 updates: {status, priority, assignee_email, tags, ...}112 """113 payload = {'ticket': updates, 'ticket_ids': ticket_ids}114 response = requests.put(115 f"{BASE_URL}/tickets/update_many.json",116 json=payload,117 auth=AUTH,118 headers=HEADERS119 )120 response.raise_for_status()121 return response.json()122123# Example usage124if __name__ == "__main__":125 # Create a test ticket126 ticket = create_ticket(127 subject="Payment processing error for order #12345",128 body="Customer reported that their payment failed during checkout. Order ID: 12345. Error code: CARD_DECLINED.",129 requester_email="customer@example.com",130 requester_name="Jane Smith",131 priority="high",132 tags=["payment", "checkout", "urgent"]133 )134 print(f"Ticket created: #{ticket['id']} β {ticket['subject']}")135136 # Add an internal note137 updated = update_ticket(138 ticket['id'],139 internal_note="Checked payment logs. Card was flagged by fraud detection. Forwarding to billing team."140 )141 print(f"Ticket updated: status={updated['status']}")142143 # Search for open high-priority tickets144 urgent_tickets = search_tickets("status:open priority:high")145 print(f"Open high-priority tickets: {len(urgent_tickets)}")Pro tip: Use the Zendesk search API for ticket queries β it is far more powerful than listing endpoints and supports Zendesk's full search syntax including field matching, date ranges, and tag filtering. The query syntax is documented at developer.zendesk.com/api-reference/ticketing/ticket-management/search/.
Expected result: Running the Python script creates a test ticket, adds an internal note, and searches for open high-priority tickets, printing the results.
Build a Zendesk Webhook Receiver with Node.js
Build a Zendesk Webhook Receiver with Node.js
Zendesk can send HTTP POST requests to your Replit server when tickets are created, updated, or solved β enabling real-time integration with external systems. You configure webhooks in Zendesk Admin Center > Apps and Integrations > Webhooks, and then connect them to Zendesk Triggers or Automations that define when the webhook fires. The Node.js server below receives Zendesk webhook payloads, verifies requests using a webhook secret, and processes ticket events. It includes a helper to update the ticket with enrichment data from an external system β the key pattern for CRM-enriched ticket workflows. Deploy this server on Replit Autoscale and use the public URL as the webhook endpoint URL in Zendesk.
1const express = require('express');2const axios = require('axios');3const crypto = require('crypto');45const app = express();6app.use(express.json());78const SUBDOMAIN = process.env.ZENDESK_SUBDOMAIN;9const EMAIL = process.env.ZENDESK_EMAIL;10const TOKEN = process.env.ZENDESK_API_TOKEN;11const WEBHOOK_SECRET = process.env.ZENDESK_WEBHOOK_SECRET || '';12const BASE_URL = `https://${SUBDOMAIN}.zendesk.com/api/v2`;1314// Zendesk uses email/token:{api_token} as Basic Auth username15const authHeader = 'Basic ' + Buffer.from(`${EMAIL}/token:${TOKEN}`).toString('base64');16const zdHeaders = {17 'Authorization': authHeader,18 'Content-Type': 'application/json',19 'Accept': 'application/json'20};2122function verifyWebhookSignature(req) {23 if (!WEBHOOK_SECRET) return true; // Skip verification if no secret configured24 const signature = req.headers['x-zendesk-webhook-signature'];25 const timestamp = req.headers['x-zendesk-webhook-signature-timestamp'];26 const body = JSON.stringify(req.body);27 const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);28 hmac.update(timestamp + body);29 const expected = hmac.digest('base64');30 return signature === expected;31}3233async function getTicket(ticketId) {34 const res = await axios.get(`${BASE_URL}/tickets/${ticketId}.json`, { headers: zdHeaders });35 return res.data.ticket;36}3738async function addInternalNote(ticketId, note) {39 const res = await axios.put(`${BASE_URL}/tickets/${ticketId}.json`, {40 ticket: {41 comment: { body: note, public: false }42 }43 }, { headers: zdHeaders });44 return res.data.ticket;45}4647async function updateTicketFields(ticketId, updates) {48 const res = await axios.put(`${BASE_URL}/tickets/${ticketId}.json`, {49 ticket: updates50 }, { headers: zdHeaders });51 return res.data.ticket;52}5354// POST /webhook β receive Zendesk trigger events55app.post('/webhook', async (req, res) => {56 // Verify webhook signature57 if (!verifyWebhookSignature(req)) {58 console.error('Webhook signature verification failed');59 return res.status(401).json({ error: 'Invalid signature' });60 }6162 // Respond quickly to Zendesk (must respond within 5 seconds)63 res.json({ received: true });6465 // Process the event asynchronously66 const payload = req.body;67 const ticketId = payload.ticket_id || payload.id;68 const eventType = payload.event_type || 'update';6970 console.log(`Zendesk webhook: ${eventType} on ticket #${ticketId}`);7172 if (ticketId) {73 try {74 // Example: enrich ticket with CRM data on creation75 if (eventType === 'create' || payload.status === 'new') {76 const ticket = await getTicket(ticketId);77 const requesterEmail = ticket.via?.source?.from?.address || '';7879 // Here you would look up the customer in your CRM80 // const crmData = await lookupCustomer(requesterEmail);81 const crmNote = `Customer lookup: ${requesterEmail}\nAccount tier: [fetched from CRM]\nContract value: [fetched from CRM]`;8283 await addInternalNote(ticketId, crmNote);84 console.log(`Enriched ticket #${ticketId} with CRM data`);85 }86 } catch (err) {87 console.error(`Failed to process webhook for ticket #${ticketId}:`, err.message);88 }89 }90});9192// GET /tickets/search β search tickets93app.get('/tickets/search', async (req, res) => {94 const { q } = req.query;95 if (!q) return res.status(400).json({ error: 'q query parameter required' });9697 try {98 const response = await axios.get(`${BASE_URL}/search.json`, {99 headers: zdHeaders,100 params: { query: `type:ticket ${q}` }101 });102 res.json({103 total: response.data.count,104 tickets: response.data.results105 });106 } catch (error) {107 res.status(500).json({ error: error.response?.data || error.message });108 }109});110111app.listen(3000, '0.0.0.0', () => {112 console.log('Zendesk integration server running on port 3000');113});Pro tip: Zendesk webhook delivery times out after 5 seconds. Always respond to the webhook request immediately (as shown with res.json({received: true})) and process the event asynchronously in a separate function. Long-running operations should be queued rather than processed inline.
Expected result: POST /webhook receives Zendesk events, verifies the signature, responds immediately, and processes ticket enrichment asynchronously.
Common use cases
Automatic Ticket Creation on Application Error
When your application detects a critical error β a payment failure, an API integration timeout, or a user-reported issue β a Replit backend automatically creates a Zendesk ticket with the error details, user information, and relevant logs attached, ensuring the support team is notified without manual escalation.
Build a Flask endpoint that receives error webhook payloads from your application monitoring tool, creates a high-priority Zendesk ticket with the error message and user ID, and assigns it to the Level 2 support group using the Zendesk API with credentials from Replit Secrets.
Copy this prompt to try it in Replit
CRM-Enriched Ticket Handler
When Zendesk sends a webhook notification that a new ticket is created, a Replit server looks up the customer in the CRM using the ticket's email address, retrieves their account tier and contract details, and updates the Zendesk ticket with CRM data as internal notes and custom field values, helping agents respond with full context.
Write a Node.js Express endpoint that receives Zendesk new ticket webhooks, queries Salesforce for the customer's account tier using the email from the ticket, and updates the Zendesk ticket with the account information as an internal comment using the Zendesk API.
Copy this prompt to try it in Replit
SLA Breach Alert System
A Replit scheduled job runs every hour to query open Zendesk tickets nearing their SLA deadline, compares breach risk against current time, and sends Slack alerts with ticket URLs to the assigned agents and their managers β providing early warning before tickets actually breach SLA.
Create a Python script that queries Zendesk for all open tickets with due dates in the next two hours using the Zendesk search API, filters by priority level, and sends a formatted Slack notification with ticket IDs and customer names for each at-risk ticket.
Copy this prompt to try it in Replit
Troubleshooting
API returns 401 Unauthorized on all requests
Cause: The authentication credentials are incorrectly formatted. Zendesk requires the email address in the format email/token:{api_token} as the Basic Auth username, not just the API token alone.
Solution: Verify that the auth tuple is (f'{email}/token:{api_token}', '') β the literal string '/token:' must be between the email and the token. The password field should be an empty string.
1import base6423# Python: requests library format4AUTH = (f"{os.environ['ZENDESK_EMAIL']}/token:{os.environ['ZENDESK_API_TOKEN']}", "")56# Node.js: manual Base64 format7const auth = Buffer.from(8 `${process.env.ZENDESK_EMAIL}/token:${process.env.ZENDESK_API_TOKEN}`9).toString('base64');10const authHeader = `Basic ${auth}`;Ticket creation returns 422 Unprocessable Entity with 'requester email' error
Cause: The requester email is invalid, not formatted correctly, or a user with that email already exists with a different name causing a conflict.
Solution: Verify the requester email is a valid email format. If you are creating tickets for existing users, you can use requester: {email: ...} and Zendesk will match the existing user. For new users, provide both email and name.
1# Correct ticket requester format2payload = {3 "ticket": {4 "subject": "Support request",5 "comment": {"body": "Ticket description"},6 "requester": {7 "email": "customer@example.com",8 "name": "Customer Name" # Required for new users9 }10 }11}Webhook events are not being received even though the webhook is configured in Zendesk
Cause: The webhook is not connected to a Trigger or Automation in Zendesk, the Replit server is not running (not deployed), or the webhook URL is the development URL rather than the deployed Autoscale URL.
Solution: Webhooks in Zendesk must be associated with a Trigger or Automation to fire. In Admin Center, go to Triggers or Automations and add an action to call your webhook. Ensure your Replit app is deployed (Autoscale) and use the production URL, not the development webview URL.
Search API returns no results despite visible tickets matching the query
Cause: The search query syntax is incorrect, or the 'type:ticket' prefix is missing. Zendesk's search endpoint searches all object types by default.
Solution: Always prefix search queries with 'type:ticket' to limit results to tickets. Verify the field names and values in your query match Zendesk's search syntax exactly (e.g., 'status:open' not 'status=open').
1# Correct: include type:ticket prefix2params = {'query': 'type:ticket status:open priority:high'}3# Wrong: missing type prefix (returns mixed object types)4# params = {'query': 'status:open priority:high'}Best practices
- Always store ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, and ZENDESK_API_TOKEN in Replit Secrets β the API token grants full access to all tickets and customer data
- Use the email/token:{api_token} format for Basic Auth β this is Zendesk-specific and different from standard Bearer token authentication
- Always add type:ticket prefix to search queries to avoid accidentally fetching users or organizations
- Respond to webhook requests within 5 seconds and process events asynchronously to avoid Zendesk marking your webhook as failed
- Use internal notes (public: false in the comment object) for adding machine-generated enrichment data to tickets rather than public replies that customers can see
- Implement webhook signature verification using HMAC-SHA256 with the webhook secret to ensure events actually come from Zendesk
- Use bulk ticket update endpoints when updating multiple tickets at once rather than looping through individual update calls to respect API rate limits
- Deploy on Replit Autoscale for webhook endpoints and use Reserved VM for scheduled SLA monitoring jobs that query tickets on a fixed interval
Alternatives
Intercom is chat-first with product tours and targeted messaging, making it a better choice for SaaS companies that want to blend support with in-app user engagement rather than pure ticket-based workflows.
LiveChat focuses on real-time customer support chat with a simpler API, making it a better fit for small businesses that primarily need live chat rather than the full ticket management and SLA tracking that Zendesk provides.
Sunshine Conversations is Zendesk's omnichannel messaging layer supporting WhatsApp, Messenger, and SMS, making it a better choice if your support channels extend beyond web and email into social messaging platforms.
Slack provides real-time internal communication with a developer-friendly API, making it better as a notification channel for Zendesk events rather than a replacement for ticket-based customer support.
Frequently asked questions
How do I connect Replit to Zendesk?
Generate an API token in Zendesk Admin Center > Apps and Integrations > Zendesk API, then add ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, and ZENDESK_API_TOKEN to Replit Secrets (lock icon π in the sidebar). Authenticate using HTTP Basic Auth with the username format: email/token:{api_token}.
Does Zendesk require OAuth for API access?
Not for single-user or service account access. Zendesk API tokens provide a simpler alternative to OAuth 2.0 β generate a token in account settings and use it with your email for Basic Auth. OAuth 2.0 is available and recommended for multi-tenant apps where users connect their own Zendesk accounts.
How do I store my Zendesk API credentials securely in Replit?
Click the lock icon π in the Replit sidebar to open Secrets and add ZENDESK_EMAIL, ZENDESK_API_TOKEN, and ZENDESK_SUBDOMAIN as separate secrets. Access them in Python with os.environ and in Node.js with process.env. Never hardcode them in source files.
Can I create Zendesk tickets automatically from my Replit app?
Yes. POST to /api/v2/tickets.json with a JSON body containing the subject, comment body, requester email, and optional priority and tags. The ticket is immediately created and visible in your Zendesk agent interface. You can also set custom field values if your Zendesk instance uses them.
How do I receive real-time Zendesk ticket events in my Replit app?
In Zendesk Admin Center, create a Webhook with your Replit server's URL as the endpoint. Then create a Trigger or Automation that calls the webhook when specific events occur (e.g., ticket created, status changed to solved). Implement the POST endpoint in your Replit server to receive and process the JSON payload.
What deployment type should I use on Replit for Zendesk integrations?
Use Autoscale deployment for webhook receiver servers that need a stable public URL for Zendesk to call. Use Reserved VM for continuously running processes or scheduled jobs that monitor ticket queues, send SLA alerts, or run batch ticket updates on a regular schedule.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation