# How to Integrate Replit with Zendesk

- Tool: Replit
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: March 2026

## TL;DR

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.

## Before you start

- 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

### 1. 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.

**Expected result:** You have a Zendesk API token, your admin email address, and your Zendesk subdomain copied and ready to add to Replit Secrets.

### 2. 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.

**Expected result:** ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, and ZENDESK_API_TOKEN appear in the Replit Secrets pane and are accessible as environment variables.

### 3. 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.

```
import os
import requests
from typing import Optional

SUBDOMAIN = os.environ["ZENDESK_SUBDOMAIN"]
EMAIL = os.environ["ZENDESK_EMAIL"]
TOKEN = os.environ["ZENDESK_API_TOKEN"]
BASE_URL = f"https://{SUBDOMAIN}.zendesk.com/api/v2"

# Zendesk Basic Auth: email/token:api_token as username
AUTH = (f"{EMAIL}/token:{TOKEN}", "")

HEADERS = {
    "Content-Type": "application/json",
    "Accept": "application/json"
}

def create_ticket(
    subject: str,
    body: str,
    requester_email: str,
    requester_name: str = "",
    priority: str = "normal",  # low, normal, high, urgent
    tags: list = None,
    custom_fields: list = None
) -> dict:
    """
    Create a new support ticket.
    Returns the created ticket object.
    """
    payload = {
        "ticket": {
            "subject": subject,
            "comment": {"body": body},
            "requester": {
                "email": requester_email,
                "name": requester_name or requester_email
            },
            "priority": priority,
            "tags": tags or [],
            "custom_fields": custom_fields or []
        }
    }
    response = requests.post(f"{BASE_URL}/tickets.json", json=payload, auth=AUTH, headers=HEADERS)
    response.raise_for_status()
    return response.json()['ticket']

def get_ticket(ticket_id: int) -> dict:
    """Retrieve a ticket by ID."""
    response = requests.get(f"{BASE_URL}/tickets/{ticket_id}.json", auth=AUTH, headers=HEADERS)
    response.raise_for_status()
    return response.json()['ticket']

def update_ticket(
    ticket_id: int,
    status: str = None,
    priority: str = None,
    assignee_email: str = None,
    internal_note: str = None,
    tags: list = None
) -> dict:
    """Update ticket fields and optionally add an internal note."""
    ticket_update = {}
    if status:
        ticket_update['status'] = status  # open, pending, hold, solved, closed
    if priority:
        ticket_update['priority'] = priority
    if assignee_email:
        ticket_update['assignee_email'] = assignee_email
    if tags:
        ticket_update['tags'] = tags
    if internal_note:
        ticket_update['comment'] = {
            'body': internal_note,
            'public': False  # False = internal note, True = public reply
        }
    response = requests.put(
        f"{BASE_URL}/tickets/{ticket_id}.json",
        json={'ticket': ticket_update},
        auth=AUTH,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()['ticket']

def search_tickets(query: str, sort_by: str = "created_at", sort_order: str = "desc") -> list:
    """
    Search tickets using Zendesk search syntax.
    Examples:
      - 'status:open priority:high'
      - 'requester:user@example.com status:new'
      - 'created>2026-03-01 status:open'
    """
    params = {
        'query': f'type:ticket {query}',
        'sort_by': sort_by,
        'sort_order': sort_order
    }
    response = requests.get(f"{BASE_URL}/search.json", params=params, auth=AUTH, headers=HEADERS)
    response.raise_for_status()
    return response.json().get('results', [])

def add_public_comment(ticket_id: int, comment: str) -> dict:
    """Add a public reply to a ticket (visible to the requester)."""
    return update_ticket(ticket_id, internal_note=None)
    # Use update_ticket with public: True comment instead

def bulk_update_tickets(ticket_ids: list, updates: dict) -> dict:
    """
    Update multiple tickets at once.
    updates: {status, priority, assignee_email, tags, ...}
    """
    payload = {'ticket': updates, 'ticket_ids': ticket_ids}
    response = requests.put(
        f"{BASE_URL}/tickets/update_many.json",
        json=payload,
        auth=AUTH,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()

# Example usage
if __name__ == "__main__":
    # Create a test ticket
    ticket = create_ticket(
        subject="Payment processing error for order #12345",
        body="Customer reported that their payment failed during checkout. Order ID: 12345. Error code: CARD_DECLINED.",
        requester_email="customer@example.com",
        requester_name="Jane Smith",
        priority="high",
        tags=["payment", "checkout", "urgent"]
    )
    print(f"Ticket created: #{ticket['id']} — {ticket['subject']}")

    # Add an internal note
    updated = update_ticket(
        ticket['id'],
        internal_note="Checked payment logs. Card was flagged by fraud detection. Forwarding to billing team."
    )
    print(f"Ticket updated: status={updated['status']}")

    # Search for open high-priority tickets
    urgent_tickets = search_tickets("status:open priority:high")
    print(f"Open high-priority tickets: {len(urgent_tickets)}")
```

**Expected result:** Running the Python script creates a test ticket, adds an internal note, and searches for open high-priority tickets, printing the results.

### 4. 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.

```
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const SUBDOMAIN = process.env.ZENDESK_SUBDOMAIN;
const EMAIL = process.env.ZENDESK_EMAIL;
const TOKEN = process.env.ZENDESK_API_TOKEN;
const WEBHOOK_SECRET = process.env.ZENDESK_WEBHOOK_SECRET || '';
const BASE_URL = `https://${SUBDOMAIN}.zendesk.com/api/v2`;

// Zendesk uses email/token:{api_token} as Basic Auth username
const authHeader = 'Basic ' + Buffer.from(`${EMAIL}/token:${TOKEN}`).toString('base64');
const zdHeaders = {
  'Authorization': authHeader,
  'Content-Type': 'application/json',
  'Accept': 'application/json'
};

function verifyWebhookSignature(req) {
  if (!WEBHOOK_SECRET) return true; // Skip verification if no secret configured
  const signature = req.headers['x-zendesk-webhook-signature'];
  const timestamp = req.headers['x-zendesk-webhook-signature-timestamp'];
  const body = JSON.stringify(req.body);
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  hmac.update(timestamp + body);
  const expected = hmac.digest('base64');
  return signature === expected;
}

async function getTicket(ticketId) {
  const res = await axios.get(`${BASE_URL}/tickets/${ticketId}.json`, { headers: zdHeaders });
  return res.data.ticket;
}

async function addInternalNote(ticketId, note) {
  const res = await axios.put(`${BASE_URL}/tickets/${ticketId}.json`, {
    ticket: {
      comment: { body: note, public: false }
    }
  }, { headers: zdHeaders });
  return res.data.ticket;
}

async function updateTicketFields(ticketId, updates) {
  const res = await axios.put(`${BASE_URL}/tickets/${ticketId}.json`, {
    ticket: updates
  }, { headers: zdHeaders });
  return res.data.ticket;
}

// POST /webhook — receive Zendesk trigger events
app.post('/webhook', async (req, res) => {
  // Verify webhook signature
  if (!verifyWebhookSignature(req)) {
    console.error('Webhook signature verification failed');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Respond quickly to Zendesk (must respond within 5 seconds)
  res.json({ received: true });

  // Process the event asynchronously
  const payload = req.body;
  const ticketId = payload.ticket_id || payload.id;
  const eventType = payload.event_type || 'update';

  console.log(`Zendesk webhook: ${eventType} on ticket #${ticketId}`);

  if (ticketId) {
    try {
      // Example: enrich ticket with CRM data on creation
      if (eventType === 'create' || payload.status === 'new') {
        const ticket = await getTicket(ticketId);
        const requesterEmail = ticket.via?.source?.from?.address || '';

        // Here you would look up the customer in your CRM
        // const crmData = await lookupCustomer(requesterEmail);
        const crmNote = `Customer lookup: ${requesterEmail}\nAccount tier: [fetched from CRM]\nContract value: [fetched from CRM]`;

        await addInternalNote(ticketId, crmNote);
        console.log(`Enriched ticket #${ticketId} with CRM data`);
      }
    } catch (err) {
      console.error(`Failed to process webhook for ticket #${ticketId}:`, err.message);
    }
  }
});

// GET /tickets/search — search tickets
app.get('/tickets/search', async (req, res) => {
  const { q } = req.query;
  if (!q) return res.status(400).json({ error: 'q query parameter required' });

  try {
    const response = await axios.get(`${BASE_URL}/search.json`, {
      headers: zdHeaders,
      params: { query: `type:ticket ${q}` }
    });
    res.json({
      total: response.data.count,
      tickets: response.data.results
    });
  } catch (error) {
    res.status(500).json({ error: error.response?.data || error.message });
  }
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Zendesk integration server running on port 3000');
});
```

**Expected result:** POST /webhook receives Zendesk events, verifies the signature, responds immediately, and processes ticket enrichment asynchronously.

## 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

## 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

### 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.

Prompt example:

```
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.
```

## 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.

```
import base64

# Python: requests library format
AUTH = (f"{os.environ['ZENDESK_EMAIL']}/token:{os.environ['ZENDESK_API_TOKEN']}", "")

# Node.js: manual Base64 format
const auth = Buffer.from(
  `${process.env.ZENDESK_EMAIL}/token:${process.env.ZENDESK_API_TOKEN}`
).toString('base64');
const 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.

```
# Correct ticket requester format
payload = {
    "ticket": {
        "subject": "Support request",
        "comment": {"body": "Ticket description"},
        "requester": {
            "email": "customer@example.com",
            "name": "Customer Name"  # Required for new users
        }
    }
}
```

### 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').

```
# Correct: include type:ticket prefix
params = {'query': 'type:ticket status:open priority:high'}
# Wrong: missing type prefix (returns mixed object types)
# params = {'query': 'status:open priority:high'}
```

## 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.

---

Source: https://www.rapidevelopers.com/replit-integration/zendesk
© RapidDev — https://www.rapidevelopers.com/replit-integration/zendesk
