---
title: "How to Migrate from Kayako to Zendesk: The Complete Technical Guide"
slug: how-to-migrate-from-kayako-to-zendesk-the-complete-technical-guide
date: 2026-07-06
author: Raaj
categories: [Migration Guide, Zendesk]
excerpt: "Step-by-step guide to migrating from Kayako to Zendesk. Covers API extraction, the archived ticket trap, object mapping, rate limits, and ETL process."
tldr: "A Kayako to Zendesk migration requires the &archived=1 API parameter to avoid silently losing closed tickets. Use Zendesk's Ticket Import API, not the standard Tickets API. Expect 1–3 weeks DIY or 3–5 days managed."
canonical: https://clonepartner.com/blog/how-to-migrate-from-kayako-to-zendesk-the-complete-technical-guide/
---

# How to Migrate from Kayako to Zendesk: The Complete Technical Guide


A **Kayako to Zendesk migration** is a data-model translation from Kayako's conversation-centric architecture to Zendesk's discrete ticket model. There is no native migration path between these platforms — no import wizard, no built-in connector. The realistic path is Kayako API extraction (or MySQL database dump) plus Zendesk's Ticket Import API for loading.

> [!NOTE]
> **Quick Answer**
> This is a moderately complex migration. The single biggest risk is silently losing archived tickets — Kayako's default API endpoint omits cases closed more than 30 days ago unless you append `&archived=1`. Kayako Macros, SLAs, Triggers, Automations, and Views cannot be migrated programmatically and must be rebuilt manually in Zendesk. Expect 1–3 weeks of engineering work for a DIY approach, or 3–5 business days with a managed service. Teams with fewer than 10K tickets and no custom fields can run this in-house. Teams with 100K+ tickets, attachments, and custom field mappings should use a managed migration service.

## Why Teams Move from Kayako to Zendesk

The most common reasons driving this migration:

- **Integration ecosystem.** Zendesk's marketplace lists 1,500+ integrations (verify against [Zendesk Marketplace](https://www.zendesk.com/marketplace/) for current count). Kayako lacks an app marketplace and relies on Zapier or Tray.io for third-party connections.
- **Omnichannel gaps.** Kayako's native channels are limited to email, social media, and messaging — no native voice or SMS. Zendesk provides unified omnichannel ticketing across email, chat, voice, SMS, and social.
- **Reporting limitations.** Kayako offers only four out-of-the-box dashboards, and custom reports must be exported to CSV. Zendesk Explore provides customizable, real-time analytics with pre-built and custom dashboards.
- **AI investment.** Zendesk has invested in AI Agents, Intelligent Triage, and Copilot (launched 2023–2024). Kayako's AI capabilities are limited by comparison.
- **Kayako Classic end-of-life risk.** Kayako Classic runs on end-of-life PHP 7.x (PHP 7.4 EOL was November 2022) and is in maintenance mode with no new features planned.

## The 3 Architectural Differences That Make This Non-Trivial

1. **Conversation vs. ticket model.** Kayako organizes interactions as Cases (or Conversations) with threaded Messages. Zendesk uses discrete Tickets with a flat comment thread. Each Kayako Case becomes one Zendesk Ticket, but the internal message threading does not carry over as a native concept.

2. **Teams vs. Groups.** Kayako's "Teams" (called "Departments" in Kayako Classic) map to Zendesk "Groups," but the assignment logic and routing rules are structurally different and must be rebuilt.

3. **Automation architecture.** Kayako uses Triggers, SLAs, and Monitors. Zendesk uses Triggers, Automations, Macros, and SLA Policies. There is no 1:1 mapping, and none of these can be migrated via API — they must be manually recreated.

For a broader pre-migration framework, see the [Zendesk Migration Checklist](https://clonepartner.com/blog/blog/zendesk-migration-checklist/). If you're also evaluating Zendesk against alternatives, see [Freshdesk vs Help Scout vs Zendesk](https://clonepartner.com/blog/blog/freshdesk-vs-help-scout-vs-zendesk-the-operations-leads-guide/).

## Kayako to Zendesk Object Mapping

The object mapping below covers both Kayako Cloud ("New Kayako") and Kayako Classic. Use this as your field-level migration spec.

| Kayako Object | Zendesk Object | Notes / Caveats |
|---|---|---|
| Cases (or Conversations) | Tickets | 1:1. Use Ticket Import API to preserve `created_at` timestamps. Store Kayako case ID in `external_id` and a visible custom field for agent lookup. |
| Messages (case replies) | Ticket Comments | Each message becomes a comment. Set `public: true` or `false` to match original visibility. Kayako "Messages" and "Posts" refer to the same underlying data — Messages are the API resource, Posts are the endpoint path. |
| Notes (internal) | Private Comments | Kayako Notes map to Zendesk comments with `public: false`. Notes may live outside the plain posts feed — audit note endpoints separately. |
| Users (customers) | Users (end-users) | Create before tickets. Match on email address. Zendesk requires unique emails; Kayako allows duplicates. |
| Agents / Staff | Users (agents) | Must exist in Zendesk before ticket import. License implications — each agent consumes a paid seat. |
| Organizations | Organizations | 1:1 mapping. Create before users. Configure domain mapping for automatic future user association. |
| Teams / Departments | Groups | Rebuild assignment rules manually after migration. |
| Tags | Tags | Direct mapping, but audit for naming conflicts and character restrictions (Zendesk tags are lowercase, no spaces). |
| Custom Fields | Custom Fields | Create in Zendesk first. Map field IDs. Picklist values must match exactly or the API rejects the ticket payload. |
| Attachments | Attachments | Upload via Zendesk Uploads API (`/api/v2/uploads`), then reference tokens in comment payloads. |
| SLAs | SLA Policies | **Cannot be migrated.** Must be rebuilt manually in Zendesk. |
| Triggers / Automations | Triggers / Automations | **Cannot be migrated.** Must be rebuilt manually. |
| Macros | Macros | **Cannot be migrated.** Must be rebuilt. |
| Help Center Articles | Help Center Articles | Separate migration via Help Center API. Kayako Classic's two-level hierarchy (Category → Article) must be adapted to Zendesk Guide's three-level hierarchy (Category → Section → Article). See the Help Center migration section below for details on images, metadata, and SEO redirects. |
| Satisfaction Ratings | Satisfaction Ratings | Can be set via Ticket Import API if `score` and `comment` data is available. |
| Collaborators | No clean equivalent | Requires a product decision, not just a field map. Options: add as CC'd users, store in a custom field, or convert to followers. |

### What Has No Clean Equivalent in Zendesk?

- **Kayako Journeys** (customer activity timelines) have no Zendesk equivalent. You can approximate them by adding context to ticket tags or custom fields.
- **Kayako Messenger conversations** (real-time chat history) do not map natively to Zendesk Chat transcripts. These must be imported as tickets with a tag like `source_kayako_messenger`.
- **Kayako Views** must be recreated manually as Zendesk Views with equivalent filter logic.
- **Historical SLA metrics and ticket performance data** are not imported by Zendesk. Imported tickets do not run triggers, and ticket metrics such as first reply time and first resolution time are not backfilled. Snapshot your legacy reporting before cutover. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/6696005837082-How-can-I-migrate-information-from-another-platform-into-Zendesk))

### Field Mapping: Where DIY Migrations Fail

Zendesk's drop-down, multi-select, and checkbox fields generate tags that business rules depend on. Zendesk does **not** let you change a custom field's type after creation. Map Kayako option values to Zendesk tag-backed option values before loading any ticket history, or you risk a full re-import after a schema fix. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/4408838961562-About-custom-fields-and-custom-field-types))

If a Kayako picklist value does not exist in the Zendesk destination field, the API rejects the entire ticket payload with a `422 Unprocessable Entity` response. The error message will reference the invalid field value, but in bulk imports this can be difficult to trace without structured logging.

For guidance on rebuilding automations after migration, see [Your Help Desk Data Migration's Secret Saboteur: Automations, Macros, and Workflows](https://clonepartner.com/blog/blog/how-to-migrate-automations-macros-workflows/).

## 3 Ways to Export Data from Kayako

There are three viable extraction methods. Each has different trade-offs for completeness, speed, and technical complexity.

### Method 1: Kayako REST API

**How it works:** Extract data using `GET /api/v1/cases.json`, `GET /api/v1/users.json`, and `GET /api/v1/organizations.json` endpoints. Paginate through results using `offset` and `limit` parameters. ([developer.kayako.com](https://developer.kayako.com/api/v1/cases/cases/))

**Key constraints:**
- Default pagination returns 10 results per page. Maximum is 100 per page using `?limit=100`.
- **The archived ticket trap:** The default `/api/v1/cases.json` endpoint excludes cases closed more than 30 days ago. You must append `&archived=1`. Missing this parameter is the single most common cause of data loss in Kayako migrations. ([help.kayako.com](https://help.kayako.com/article/48375-retrieving-all-conversations-or-cases))
- Rate limits are not publicly documented for Kayako Cloud. The API returns HTTP 429 with a `Retry-After` header when limits are exceeded. Kayako Classic (self-hosted) has no rate limit.
- **Authentication differs by version:** Kayako Cloud uses Basic Auth with agent/admin credentials. Kayako Classic uses API key-based authentication — you must pass the API key and a secret key as request parameters (`apikey` and `salt`/`signature`).
- Use the staff-facing `cases` endpoint when you need internal metadata; the `conversations` endpoint is intentionally narrower and excludes internal notes and some metadata fields.

**Kayako Classic API differences:** Kayako Classic uses a different base URL structure (`/api/index.php?/`) and XML-formatted responses by default (JSON available via `Accept` header). Endpoint paths differ — e.g., `/Tickets/Ticket` instead of `/api/v1/cases`. If migrating from Classic, consult the [Kayako Classic REST API documentation](https://classic.kayako.com/article/1524-kayako-rest-api) separately.

**When to use it:** Best for incremental extraction, delta syncs, and instances under 100K cases.

**Complexity:** Medium

### Method 2: MySQL Database Dump

**How it works:** Request a full backup from Kayako's Infrastructure team. They provide the data as a MySQL dump file — CSV format is not available. For Kayako Classic (self-hosted), you can run `mysqldump` directly against your own database. ([help.kayako.com](https://help.kayako.com/article/48696-requesting-a-data-dump-or-backup-for-your-kayako-instance))

**Key constraints:**
- Only up-to-date backups are available. You cannot request a backup from a specific date in the past.
- Download links expire within 1–7 days, depending on the expiration you select when submitting the request.
- You must specify whether you want database only, attachments only, or database with attachments.
- The requester must be a recognized billing contact.
- Parsing the raw MySQL schema requires database expertise. Key tables: `kayako_tickets`, `kayako_ticketposts`, `kayako_users`, `kayako_ticketcustomfieldvalues` (Classic); table names differ in Kayako Cloud dumps.

**When to use it:** Best for full historical extraction, especially when you need 100% of all records including edge cases the API might miss. Preferred for large, archived-heavy, attachment-heavy instances.

**Complexity:** Low (extraction), High (parsing and transformation)

### Method 3: Third-Party Tool or Managed Service

**Third-party tools** like Help Desk Migration (Relokia) provide a GUI-based mapping wizard that connects to both Kayako and Zendesk via API. They use per-record pricing (typically $1–$10 per 100 records depending on volume tier — check current pricing at [helpdeskMigration.com](https://www.helpdeskmigration.com/)), offer limited control over transformation logic, and may not handle deeply nested custom fields or multi-brand scenarios. Best for small-to-medium instances under 50K tickets with standard data models.

**Managed migration services** like ClonePartner handle the full extraction, transformation, mapping, and loading using custom ETL infrastructure. Best for enterprise volumes, complex fields, and zero-downtime requirements.

### Approach Comparison

| Approach | Best For | Scalability | Complexity | Risk Level |
|---|---|---|---|---|
| Kayako REST API | Delta syncs, selective extraction | Medium–High | Medium | Medium (archived ticket trap) |
| MySQL Database Dump | Full historical migration | High | Low extraction / High transform | Low (most complete) |
| Third-Party Tool | Small-to-medium, standard data | Low–Medium | Low | Low–Medium |
| Managed Service (custom ETL) | Enterprise, complex fields, zero-downtime | High | Handled for you | Low |

## Kayako API Limitations and the Archived Ticket Trap

This section covers the specific API constraints that cause extraction failures.

### The `&archived=1` Parameter

Kayako's `/api/v1/cases.json` endpoint excludes cases that were closed more than 30 days ago by default. To retrieve all historical cases, append `&archived=1`:

```bash
GET https://DOMAIN.kayako.com/api/v1/cases?limit=100&offset=0&archived=1
```

If you omit this parameter, your migration will silently drop every closed ticket older than 30 days. There is no warning or error — the API simply returns fewer results. ([help.kayako.com](https://help.kayako.com/article/48375-retrieving-all-conversations-or-cases))

> [!WARNING]
> If you start with Kayako `conversations` instead of `cases`, or you forget `archived=1`, you can finish the project with a clean-looking Zendesk instance and still be missing years of historical support data.

### Pagination Limits

Kayako's API defaults to 10 results per page. The maximum is 100 results per page via the `limit` parameter. For an instance with 200K cases, you'll need a minimum of 2,000 API calls just for case metadata — before fetching messages, notes, or attachments for each case.

```bash
# Paginate through all cases
GET /api/v1/cases?limit=100&offset=0&archived=1
GET /api/v1/cases?limit=100&offset=100&archived=1
GET /api/v1/cases?limit=100&offset=200&archived=1
# ... continue until offset >= total_count
```

> [!WARNING]
> Kayako's `total_count` can shift between pages if cases are created or updated during extraction. Treat any API export as a point-in-time snapshot and run a delta sync before cutover.

### Attachment Errors

Kayako enforces a 20MB attachment limit per post. When downloading attachments via the API, you may encounter a `402 LICENSE_LIMIT_REACHED` error for oversized files. Your extraction script must catch this error and log it for manual follow-up rather than halting the entire migration. ([help.kayako.com](https://help.kayako.com/article/48344-error-when-downloading-attachments-license-limit-reached))

### Custom Fields and Posts

Kayako's custom field options are not returned by default in API responses. Append `?include=*` to the field endpoint to retrieve picklist values and their locale-specific labels:

```bash
GET /api/v1/cases/fields/{field_id}?include=*
```

For posts, the default response excludes activities. Use `filters=ALL` to get the complete timeline:

```bash
GET /api/v1/cases/{case_id}/posts.json?filters=ALL
```

If your instance used conversation, requester, or organization notes heavily, audit those note endpoints separately — notes may live outside the plain posts feed. ([developer.kayako.com](https://developer.kayako.com/api/v1/cases/notes/))

## Zendesk Import Constraints and Rate Limits

The Zendesk side of the migration has its own constraints that directly affect throughput and data integrity.

### Which API Endpoint to Use

Zendesk provides a dedicated **Ticket Import API** at `POST /api/v2/imports/tickets` designed specifically for migrations. Do not use the standard Tickets API — it triggers business rules, sends notifications, and does not let you set historical timestamps. ([developer.zendesk.com](https://developer.zendesk.com/api-reference/ticketing/tickets/ticket_import/))

The Ticket Import API provides capabilities the standard API does not:

1. **Historical timestamps.** You can set `created_at`, `updated_at`, and `solved_at` on imported tickets and individual comments. Timestamps must be in ISO 8601 format (e.g., `2023-06-15T14:30:00Z`). If the format is invalid, Zendesk returns a `422 Unprocessable Entity` error with a message referencing the invalid timestamp field.
2. **Multiple comments per ticket.** You can include the full comment history in a single import call.
3. **Archive immediately.** The `archive_immediately=true` parameter sends closed tickets directly to the archive, bypassing the normal ticket lifecycle.
4. **No notifications.** No emails are sent to CC'd users on import.
5. **No triggers fire** on imported tickets unless subsequently updated.

For bulk operations, use `POST /api/v2/imports/tickets/create_many` which accepts up to 100 tickets per request.

### Common Ticket Import API Errors

| Error | Cause | Fix |
|---|---|---|
| `422` — "RequesterID: is not a valid value" | The `requester_id` references a user that doesn't exist in Zendesk | Create the user first, or check your user ID mapping table |
| `422` — "Created at: is not a valid datetime" | Timestamp not in ISO 8601 format or contains an invalid date | Validate and reformat all timestamps before import |
| `422` — "Field: is not a valid value" | A custom field picklist value doesn't exist in Zendesk | Add the missing option to the custom field, or map to an existing value |
| `422` — "Email: has already been taken" | Duplicate user email during user creation | Query Zendesk for the existing user and use their ID |
| `429` — Rate limited | Too many API requests | Read `Retry-After` header, implement exponential backoff |
| `413` — Request entity too large | Payload exceeds Zendesk's request size limit | Split into smaller batches; reduce comments per ticket |

### Rate Limits by Plan

| Zendesk Plan | Rate Limit (req/min) |
|---|---|
| Team | 200 |
| Growth | 400 |
| Professional | 400 |
| Enterprise | 700 |
| Enterprise Plus / High Volume Add-on | 2,500 |

These are **account-level limits**, not per-agent. Every integration, webhook, and agent UI action shares the same bucket. During a migration, you are competing with live agent activity for API capacity. ([developer.zendesk.com](https://developer.zendesk.com/api-reference/introduction/rate-limits/))

**Sandbox rate limits:** Zendesk sandbox environments have lower rate limits than production (typically 200 req/min regardless of plan). Your pilot migration in a sandbox may run significantly slower than production — plan throughput testing accordingly.

Monitor the `X-Rate-Limit-Remaining` and `Retry-After` response headers. Implement exponential backoff when you receive a 429 response.

### Important Import Constraints

- **SLAs and metrics are not calculated for imported tickets.** Add a tag like `migrated_from_kayako` to exclude imported tickets from performance reports. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/6696005837082-How-can-I-migrate-information-from-another-platform-into-Zendesk))
- **Ticket IDs are not preserved.** Zendesk assigns new IDs. Maintain an external mapping table (`kayako_case_id → zendesk_ticket_id`) for cross-referencing.
- **Attachments must be uploaded separately** via the Uploads API to get a token, then referenced in the comment payload. Zendesk's file size limit is 50MB. ([developer.zendesk.com](https://developer.zendesk.com/documentation/ticketing/managing-tickets/adding-ticket-attachments-with-the-api/))
- **Comment character limit:** 65,535 characters per comment, 5,000 total comments per ticket. Very long Kayako threads can hit these hard limits. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/4408820879258-Is-there-a-character-limit-on-ticket-comments))
- **Zendesk's data importer** (for CSV) handles users and organizations but not ticket history. It is not enabled by default — validate this setting before cutover week. CSV imports support files up to 1GB with a recommended maximum of 500,000 rows. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/4408893496218-Bulk-importing-user-data-with-the-data-importer))
- **Search indexing can lag** a few minutes after bulk load. Wait for indexing before running UI-based UAT. ([developer.zendesk.com](https://developer.zendesk.com/api-reference/ticketing/ticket-management/search/))
- **Bulk jobs return a `job_status` object.** Poll this rather than assuming a 201 means data is queryable. Background jobs are capped at 30 queued or running. ([developer.zendesk.com](https://developer.zendesk.com/api-reference/ticketing/ticket-management/job_statuses/))

### Throughput Math

On an Enterprise plan (700 req/min), each ticket import with attachments requires approximately 2–4 API calls (1 for the ticket + 1–3 for attachment uploads). Realistic throughput: **175–350 tickets per minute**, or roughly **10,000–20,000 tickets per hour**. A 200K-ticket migration takes 10–20 hours of continuous import time at this rate.

On an Enterprise Plus plan with the High Volume API add-on (2,500 req/min), throughput increases to approximately 625–1,250 tickets per minute, or **37,500–75,000 tickets per hour** — reducing a 200K-ticket migration to 3–6 hours.

> [!TIP]
> If your source has very long tickets, create one imported ticket with the historical thread baked into ordered comments. Do not create the ticket and then update it hundreds of times — Zendesk limits updates to 30 per 10 minutes per user per ticket.

## Step-by-Step Migration Process

The correct order-of-operations prevents broken relationships. Import entities in this sequence.

### Step 1: Extract and Load Organizations

Organizations must exist before users can be assigned to them.

```python
# Extract from Kayako
GET /api/v1/organizations?limit=100&offset=0

# Load into Zendesk
POST /api/v2/organizations/create_or_update
# Use name as the deduplication key
```

### Step 2: Extract and Load Users

Users must exist before tickets can reference them as requesters or assignees.

```python
# Extract from Kayako
GET /api/v1/users?limit=100&offset=0&include=*

# Load into Zendesk
POST /api/v2/users/create_or_update
# Match on email address; map organization_id
# IMPORTANT: Disable welcome emails before import
```

Zendesk strictly enforces email RFC standards (RFC 5321/5322). If Kayako contains users with invalid emails (e.g., `user@domain` without a TLD, addresses with spaces, or non-ASCII characters), Zendesk will reject the user creation. Your ETL pipeline must sanitize email strings or assign placeholder addresses (e.g., `kayako-user-{id}@placeholder.invalid`) to preserve the historical record.

Kayako allows multiple user records with the same email — Zendesk does not. Your script must deduplicate before import. The Zendesk API returns `422 Unprocessable Entity` for duplicate emails. Catch this error, query Zendesk for the existing user ID via `GET /api/v2/users/search?query=email:{email}`, and update your local mapping table.

### Step 3: Create Groups and Custom Fields

Map Kayako Teams (or Departments) to Zendesk Groups. Create all custom ticket fields in Zendesk before importing tickets, noting the new field IDs. Store the mapping of Kayako IDs to Zendesk IDs in a local database or key-value store — you'll need these for every ticket payload.

**Field type mapping considerations:**

| Kayako Field Type | Zendesk Equivalent | Caveats |
|---|---|---|
| Text | Text | Direct mapping |
| Select (single) | Drop-down | Tag values must match exactly |
| Multi-select | Multi-select | Available on Professional+ plans only |
| Checkbox | Checkbox | Maps to boolean |
| Radio buttons | Drop-down | Kayako radio → Zendesk drop-down; test tag generation |
| Linked select (cascading) | No native equivalent | Flatten to multiple independent drop-downs, or use conditional fields with a third-party app |
| File | No field-level equivalent | Store as attachments on the ticket |

### Step 4: Extract All Cases Including Archived

```python
import requests
import time
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("kayako_extract")

def extract_all_cases(domain, auth):
    cases = []
    offset = 0
    while True:
        url = f"https://{domain}.kayako.com/api/v1/cases"
        params = {"limit": 100, "offset": offset, "archived": 1}
        
        try:
            resp = requests.get(url, params=params, auth=auth, timeout=30)
        except requests.exceptions.RequestException as e:
            logger.error(f"Request failed at offset {offset}: {e}")
            time.sleep(10)
            continue
        
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 60))
            logger.warning(f"Rate limited. Waiting {wait}s.")
            time.sleep(wait)
            continue
        
        if resp.status_code != 200:
            logger.error(f"Unexpected status {resp.status_code} at offset {offset}")
            break
        
        data = resp.json()
        batch = data.get("data", [])
        cases.extend(batch)
        logger.info(f"Extracted {len(cases)} / {data.get('total_count', '?')} cases")
        
        if offset + 100 >= data.get("total_count", 0):
            break
        offset += 100
    
    logger.info(f"Extraction complete: {len(cases)} cases total")
    return cases
```

### Step 5: Extract Messages, Notes, and Attachments Per Case

For each case, fetch messages and notes separately:

```bash
GET /api/v1/cases/{case_id}/messages?include=*
GET /api/v1/cases/{case_id}/notes
```

Download all attachments, handling `402 LICENSE_LIMIT_REACHED` errors for files exceeding 20MB. Log failed downloads for manual follow-up rather than halting the pipeline.

### Step 6: Transform and Load Tickets

Map Kayako case data to the Zendesk Ticket Import API payload format:

```python
STATUS_MAP = {
    "NEW": "new",
    "OPEN": "open",
    "PENDING": "pending",
    "COMPLETED": "solved",
    "CLOSED": "closed",
    # Kayako Classic statuses
    "In Progress": "open",
    "On Hold": "hold",
}

def map_status(kayako_status):
    return STATUS_MAP.get(kayako_status, "open")

def build_zendesk_ticket(kayako_case, messages, notes, user_map, group_map, field_map):
    comments = []
    
    # Add messages as public comments
    for msg in sorted(messages, key=lambda m: m["created_at"]):
        comment = {
            "author_id": user_map.get(msg["creator"]["id"]),
            "created_at": msg["created_at"],
            "html_body": msg.get("body_html", msg.get("body_text", "")),
            "public": True
        }
        # Truncate if exceeding Zendesk's 65,535 char limit
        if len(comment["html_body"]) > 65535:
            comment["html_body"] = comment["html_body"][:65500] + "\n\n[Truncated — original exceeded 65,535 character limit]"
        comments.append(comment)
    
    # Add notes as private comments
    for note in sorted(notes, key=lambda n: n["created_at"]):
        comments.append({
            "author_id": user_map.get(note["user"]["id"]),
            "created_at": note["created_at"],
            "value": note.get("body_text", ""),
            "public": False
        })
    
    # Map custom fields
    custom_fields = []
    for kf_id, kf_value in kayako_case.get("custom_fields", {}).items():
        zd_field_id = field_map.get(kf_id)
        if zd_field_id:
            custom_fields.append({"id": zd_field_id, "value": kf_value})
    
    ticket = {
        "ticket": {
            "external_id": f"kayako-{kayako_case['id']}",
            "subject": kayako_case.get("subject", "(No subject)"),
            "requester_id": user_map.get(kayako_case["requester"]["id"]),
            "group_id": group_map.get(
                kayako_case.get("assigned_team", {}).get("id")
            ),
            "created_at": kayako_case["created_at"],
            "updated_at": kayako_case["updated_at"],
            "status": map_status(kayako_case.get("status", {}).get("label", "OPEN")),
            "tags": ["migrated_from_kayako"],
            "custom_fields": custom_fields,
            "comments": comments
        }
    }
    
    return ticket

# POST to Zendesk Ticket Import API
# POST /api/v2/imports/tickets?archive_immediately=true
```

### Step 7: Upload Attachments

For each comment with attachments, upload to Zendesk first, then include the upload token:

```bash
# Step 1: Upload file
POST /api/v2/uploads?filename=report.pdf
Content-Type: application/binary
# Response: {"upload": {"token": "abc123"}}

# Step 2: Reference token in comment
{"comment": {"body": "See attached", "uploads": ["abc123"]}}
```

### Step 8: Handle Inline Images

Kayako stores inline images in two ways:
1. **Embedded URLs** pointing to the Kayako instance (e.g., `https://yourdomain.kayako.com/api/v1/cases/{id}/attachments/{id}/url`)
2. **Base64-encoded content** embedded directly in HTML body

After migration, Kayako-hosted URLs will break when Kayako is decommissioned. Your ETL pipeline must:

1. Parse HTML bodies for `<img>` tags with Kayako-hosted `src` attributes
2. Download each image from Kayako
3. Upload to Zendesk via the Uploads API
4. Replace the `src` URL in the HTML body with the Zendesk-hosted URL

```python
import re
from urllib.parse import urlparse

def rewrite_inline_images(html_body, kayako_domain, zendesk_upload_func):
    """Find Kayako-hosted images and re-upload to Zendesk."""
    img_pattern = re.compile(
        r'<img[^>]+src=["\'](' + re.escape(kayako_domain) + r'[^"\']+)["\']',
        re.IGNORECASE
    )
    for match in img_pattern.finditer(html_body):
        kayako_url = match.group(1)
        filename = urlparse(kayako_url).path.split("/")[-1] or "inline_image.png"
        zendesk_url = zendesk_upload_func(kayako_url, filename)
        if zendesk_url:
            html_body = html_body.replace(kayako_url, zendesk_url)
    return html_body
```

### Step 9: Run Delta Sync and Cut Over

After the full historical migration, run a delta sync to capture any tickets created or updated during the migration window. Use Kayako's `updated_at` filter to fetch only cases modified since the initial extraction timestamp. Switch email routing from Kayako to Zendesk. Do not shut down Kayako until Zendesk has operated successfully for at least 14 days.

### Step 10: Validate Record Counts and Run QA

After import, reconcile counts between source and target. See the [Post-Migration QA Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/) for detailed tests to run.

If your script fails mid-run, see [Help Desk Data Migration Failed? The Engineer's Rescue Guide](https://clonepartner.com/blog/blog/help-desk-data-migration-failed-the-engineers-rescue-guide/).

## Email Threading and Channel Continuity

When a customer replies to an old Kayako email notification after cutover, Zendesk must recognize the reply and route it to the correct ticket. This depends on email threading headers.

**How it works:** Email clients use `Message-ID`, `In-Reply-To`, and `References` headers (RFC 2822) to thread conversations. Kayako's outbound emails contain Kayako-generated `Message-ID` headers. Zendesk's inbound email processing uses `In-Reply-To` and `References` to match incoming replies to existing tickets.

**The problem:** If a customer replies to an old Kayako email, the `In-Reply-To` header references a Kayako `Message-ID` that Zendesk has never seen. Zendesk will create a new ticket instead of appending to the migrated ticket.

**Mitigation options:**
1. **Forward mapping:** Set up email forwarding from your old Kayako support address to Zendesk. Zendesk will create new tickets for old-thread replies — train agents to merge these manually during the first 30–60 days.
2. **Customer notification:** Proactively email customers before cutover, asking them to start new threads for new issues rather than replying to old emails.
3. **Mail rules:** Create a Zendesk trigger that detects replies referencing old Kayako headers (by checking for specific email patterns) and tags them for agent review.

## Help Center / Knowledge Base Migration

Migrating Kayako's Help Center to Zendesk Guide requires separate handling from ticket migration.

### Hierarchy Mapping

- **Kayako Classic:** Category → Article (two levels)
- **Kayako Cloud:** Category → Section → Article (three levels, matching Zendesk)
- **Zendesk Guide:** Category → Section → Article (three levels)

For Kayako Classic migrations, create a placeholder Section (e.g., "General") under each Category to house articles that were directly under a Category.

### Migration Checklist for KB Content

1. **Article metadata:** Preserve `created_at`, `updated_at`, author, and status (draft/published) via the Help Center API (`POST /api/v2/help_center/sections/{id}/articles`).
2. **Embedded images:** Re-upload images to Zendesk and update `src` URLs in article HTML, just as with ticket inline images.
3. **Internal links:** Update links between articles to point to new Zendesk Guide URLs.
4. **SEO redirects:** Set up 301 redirects from old Kayako Help Center URLs (e.g., `help.yourdomain.com/article/12345`) to new Zendesk Guide URLs. Implement via DNS redirect rules, web server configuration, or a redirect app.
5. **Multilingual content:** If using Kayako's localization, map language variants to Zendesk Guide's translation model (`POST /api/v2/help_center/articles/{id}/translations`).
6. **Attachments on articles:** Download and re-upload to Zendesk using the Article Attachments API.

## Data Residency and Compliance

For enterprise migrations, data handling during the ETL process is a compliance concern.

- **Data residency:** Zendesk offers data center locations in the US, EU (Frankfurt), and Australia. Confirm your Zendesk instance's data residency matches your compliance requirements before beginning data transfer. Kayako Cloud data is hosted in AWS US-East by default.
- **GDPR considerations:** The migration ETL pipeline constitutes data processing. If you store intermediate data (e.g., a local database of extracted tickets), ensure it is encrypted at rest and deleted after migration is complete. Document the migration in your Records of Processing Activities (ROPA).
- **PII handling:** Ticket bodies and user records contain PII. If using a third-party tool or managed service, ensure they have a Data Processing Agreement (DPA) in place. Verify their data retention and deletion policies.
- **Right to be forgotten:** If any customers submitted GDPR deletion requests in Kayako, verify those users are not re-imported from a pre-deletion database dump.

## Timeline and Phases [For PMs]

A typical Kayako to Zendesk migration takes **1–3 weeks** for a DIY approach and **3–5 business days** with a managed service.

| Phase | Duration | Dependencies |
|---|---|---|
| Discovery and data audit | 2–3 days | Access to Kayako admin, Zendesk sandbox |
| Field mapping and custom field setup | 1–2 days | Stakeholder sign-off on field mapping |
| Script development and testing | 3–5 days | Engineering resource, API credentials |
| Pilot migration (subset) | 1 day | Zendesk sandbox (note: reduced rate limits) |
| UAT and stakeholder validation | 1–2 days | CS team review, agent spot-checks |
| Full migration run | 1–2 days | Depends on volume and Zendesk plan tier |
| Delta sync and cutover | 4–8 hours | Scheduled maintenance window |
| Post-migration validation | 1 day | QA checklist |

**Critical dependency:** Zendesk environment must be fully configured (custom fields created, data importer enabled) before script development begins. Freeze Zendesk field design before loading history — custom field types cannot be changed after creation. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/4408886624410-Understanding-how-creating-deactivating-or-deleting-ticket-fields-impacts-tickets))

### Risk Register

| Risk | Likelihood | Mitigation |
|---|---|---|
| Archived tickets silently dropped | High (if `&archived=1` is missed) | Validate total_count before and after extraction; compare against Kayako dashboard metrics |
| Zendesk rate limiting during load | Medium | Implement exponential backoff; consider High Volume API add-on ($$$); schedule migration during off-hours to reduce contention with agent traffic |
| Attachment download failures (402 error) | Medium | Catch and log errors; route to manual review queue; count affected tickets |
| User deduplication conflicts | Medium | Match on email; deduplicate before import; handle multi-identity users by merging to primary email |
| Custom field picklist mismatches | Low–Medium | Validate picklist values before import; field types are immutable after creation |
| Oversized threads hitting Zendesk limits | Low | Pre-check comment count (<5,000) and character length (<65,535) before import; truncate or split if necessary |
| Inline images break post-migration | Medium | Re-host images on Zendesk; rewrite HTML `src` attributes in ETL pipeline |
| Old email thread replies create new tickets | Medium | Set up forwarding; train agents on merging; notify customers before cutover |

### Cutover Strategy

**Recommended: Incremental (delta sync) approach.** Run the full historical migration first, then perform a delta sync of new and updated tickets during the migration window. This eliminates the need for extended downtime.

Big-bang cutovers (moving everything over a single weekend) are high-risk. Use them only when volume is small and open-ticket count is low.

For a detailed breakdown, see [Zero-Downtime Help Desk Data Migration](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/).

## What Customers and Agents Notice [For Customer Success]

Done correctly, customers notice nothing. Done poorly, they notice everything.

**What should be invisible:**
- All historical tickets and conversation threads searchable in Zendesk under the customer's profile.
- Agent names and response history preserved in comment attribution.
- Timestamps reflecting original dates, not import dates.

**What will change:**
- **Ticket ID numbers will be different.** If customers reference old Kayako ticket IDs, add the original ID as a custom field or tag. Zendesk can list tickets by `external_id` via API, but agents also need a searchable custom field, subject prefix, or tag for quick UI lookup. ([developer.zendesk.com](https://developer.zendesk.com/api-reference/ticketing/tickets/tickets/))
- **Ticket URLs will change.** If you have a customer-facing help center, set up 301 redirects from Kayako URLs.
- **CSAT survey links from Kayako will stop working.** Notify customers that historical survey links are deprecated.
- **Historical SLA and performance dashboards will change** since imported tickets don't carry metrics. Add the `migrated_from_kayako` tag and exclude it from performance views.
- **Old email thread replies may create new tickets** instead of appending to migrated tickets (see Email Threading section above).

**Change management recommendations:**
- Notify customers 1–2 weeks before cutover that support is moving to a new platform.
- Provide agents with a 1-page reference mapping old Kayako workflows to new Zendesk equivalents.
- Staff a dedicated escalation channel for the first 48 hours post-cutover.
- Communicate who owns hypercare for the first 2–5 business days.
- Run a 30-minute training session for agents on Zendesk UI differences — especially searching by external ID and using the new Views.

## Edge Cases and Known Limitations

These are the failure modes seen most frequently in Kayako-to-Zendesk migrations:

- **Archived cases silently omitted.** The default API call excludes tickets closed more than 30 days ago unless `&archived=1` is appended. ([help.kayako.com](https://help.kayako.com/article/48375-retrieving-all-conversations-or-cases))

- **Duplicate users.** Kayako allows multiple user records with the same email. Zendesk does not. Your script must deduplicate before import — the Zendesk API returns 422 for duplicate emails.

- **Invalid emails.** Zendesk strictly enforces email RFC standards. Users with malformed emails (missing TLDs, invalid characters) will be rejected. Sanitize email strings or assign placeholder addresses.

- **Inline images in messages.** Kayako stores inline images as embedded URLs pointing to the Kayako instance (or as base64-encoded content). After migration, these URLs break unless you re-host the images on Zendesk and update the HTML body's `src` tags. See Step 8 above for implementation.

- **Kayako Classic two-level KB hierarchy.** Kayako Classic allows articles directly under Categories without Folders. Zendesk Guide requires three levels (Category → Section → Article). Create placeholder Sections for orphaned articles.

- **Multi-brand instances.** Kayako's brand-level data separation may not map cleanly to Zendesk's multi-brand architecture. In Zendesk, each brand has its own subdomain, Help Center, and email addresses, but shares a single ticket pool. Each brand needs its own extraction and mapping pass. Ensure tickets are tagged or routed to the correct Zendesk brand during import using the `brand_id` field on the ticket payload.

- **Custom field type mismatches.** Kayako supports field types that don't exist in Zendesk (and vice versa). Audit field types before migration. Zendesk does not allow changing a field's type after creation. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/4408886624410-Understanding-how-creating-deactivating-or-deleting-ticket-fields-impacts-tickets))

- **`total_count` drift.** Kayako's `total_count` response field can change between paginated API calls if records are created or modified during extraction. Treat extraction as a point-in-time snapshot and reconcile with a delta sync.

- **Kayako posts omit activities by default.** Use `filters=ALL` to get complete timeline data. ([developer.kayako.com](https://developer.kayako.com/api/v1/cases/cases/))

- **Zendesk comment and thread limits are hard limits.** 65,535 characters per comment and 5,000 comments per ticket. Route oversized threads to a manual review queue. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/4408820879258-Is-there-a-character-limit-on-ticket-comments))

- **Zendesk search indexing lags.** After bulk import, search may take several minutes to index new records. Wait for indexing before UI-based validation.

- **Timezone mismatches.** Kayako may store timestamps in the server's local timezone (especially Classic). Zendesk expects UTC. Verify timezone handling in your ETL pipeline to avoid shifted timestamps.

## Validation and Testing

Never trust a "200 OK" response as proof of data integrity. Run these checks before declaring the migration complete:

1. **Record-count reconciliation.** Compare total Organizations, Users, Tickets, and Comments between Kayako and Zendesk. Run a SQL `COUNT()` on your Kayako database (or use API `total_count` with `&archived=1`) and compare to Zendesk Search API results or the `GET /api/v2/tickets/count` endpoint. Allow for a small delta if the delta sync hasn't run yet.

2. **Random sample audit.** Pull 50 random tickets across different statuses (open, closed, pending) and verify: correct requester, correct assignee/group, all comments present in order, correct timestamps, attachments accessible, private notes stayed private, and inline images render correctly.

3. **Custom field validation.** For each custom field, verify that values transferred correctly — especially picklist/dropdown fields where Kayako option IDs may not match Zendesk option IDs. Query 10 tickets per custom field type to confirm.

4. **Attachment spot-check.** Download 20 random attachments from migrated tickets and verify they open correctly and are not corrupted 0-byte files. Pay special attention to attachments near the 20MB Kayako limit.

5. **Search verification.** Search for 10 known tickets by subject line and requester email in Zendesk to confirm indexing. Wait for search indexing to complete (typically 2–5 minutes after bulk import) before running this test.

6. **Agent UAT.** Have 2–3 agents work their normal workflow in Zendesk for a day using real migrated tickets. Collect feedback on missing data or broken references. Provide agents with a feedback form covering: ticket history completeness, search functionality, custom field accuracy, and attachment accessibility.

7. **Poll job statuses.** For bulk imports, confirm all background jobs completed successfully before starting validation. ([developer.zendesk.com](https://developer.zendesk.com/api-reference/ticketing/ticket-management/job_statuses/))

8. **Email threading test.** Reply to an old Kayako notification email and verify whether Zendesk creates a new ticket (expected) or correctly routes it. Document the behavior for agent training.

For a comprehensive testing framework, see the [Post-Migration QA Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

**Rollback plan:** True rollback is traffic rollback, not mass deletion. Keep inbound email and channel routing on Kayako until the final delta passes UAT. If go-live fails, keep agents in Kayako, fix the mapping, and rerun against a fresh Zendesk sandbox. Do not shut down Kayako until Zendesk has operated successfully for at least 14 days.

## Build In-House vs. Use a Managed Service

### When In-House Makes Sense

- Fewer than 10K tickets with no custom fields.
- You have a developer with Python/Node experience who can dedicate 2–3 weeks.
- You're comfortable with the risk of re-doing the migration if the script misses edge cases.
- Your data model is standard — no multi-brand, no cascading custom fields, minimal attachments.

### When In-House Doesn't Make Sense

- More than 50K tickets, especially with attachments and custom fields.
- You need zero-downtime cutover with delta sync.
- You don't have engineering bandwidth to build, test, and maintain a one-time migration script.
- You've already attempted a DIY migration and hit rate limits, missing data, or broken relationships.
- You have compliance requirements (GDPR, SOC 2) that require documented data handling procedures during migration.

The hidden cost of a DIY migration is not the initial script — it's the debugging. When 2% of tickets fail silently due to a 402 attachment error or a duplicate user conflict, the engineering team spends days tracing individual failures. Mapping design, retry logic, attachment handling, batch orchestration, exception queues, and revalidation after every schema change add up fast. Based on typical engineering rates, a 3-week DIY migration at $150/hr engineering cost runs $18,000–$27,000 in labor — often exceeding managed service pricing for equivalent scope.

If a DIY script fails mid-migration, see [Help Desk Data Migration Failed? The Engineer's Rescue Guide](https://clonepartner.com/blog/blog/help-desk-data-migration-failed-the-engineers-rescue-guide/).

### ClonePartner

ClonePartner has completed 1,500+ data migrations including complex helpdesk moves with custom field mappings, attachment handling, and multi-brand architectures. For Kayako-to-Zendesk specifically, we handle the `&archived=1` extraction automatically, run delta syncs so your team never stops working, and validate record counts at every stage.

> Need to migrate from Kayako to Zendesk without losing archived tickets, attachments, or custom field data? ClonePartner runs the full extraction, transformation, and load — including delta syncs for zero-downtime cutover. Book a 30-minute scoping call to get a timeline and fixed quote.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## FAQ

### How long does a Kayako to Zendesk migration take?

A typical migration takes 1–3 weeks with an in-house engineering team, or 3–5 business days with a managed service. The primary variables are ticket volume, attachment count, number of custom fields requiring transformation, and your Zendesk plan's API rate limit (200–2,500 req/min depending on plan tier).

### Can I migrate Kayako to Zendesk without losing data?

Yes, but only if your extraction includes the `&archived=1` parameter when calling the Kayako Cases API. Without it, all tickets closed more than 30 days ago are silently excluded. Attachments over 20MB per post may also fail with a 402 error and require manual handling. Inline images hosted on Kayako URLs will break unless re-uploaded to Zendesk during migration. ([help.kayako.com](https://help.kayako.com/article/48375-retrieving-all-conversations-or-cases))

### What data cannot be migrated from Kayako to Zendesk?

Kayako Macros, Triggers, SLA Policies, Automations, and Views cannot be migrated programmatically and must be manually rebuilt in Zendesk. Kayako Journeys (customer activity timelines) have no Zendesk equivalent. Ticket ID numbers are not preserved — Zendesk assigns new IDs. Imported tickets do not carry ticket metrics like first reply time or resolution time. Email threading from old Kayako conversations does not transfer — replies to old Kayako emails will create new Zendesk tickets. ([support.zendesk.com](https://support.zendesk.com/hc/en-us/articles/6696005837082-How-can-I-migrate-information-from-another-platform-into-Zendesk))

### Is there a native Kayako-to-Zendesk migration tool?

No. Neither Kayako nor Zendesk provides a native migration tool between the two platforms. Zendesk's built-in CSV import handles users and organizations but not full ticket history with comments and attachments. The Ticket Import API (`POST /api/v2/imports/tickets`) is the correct endpoint for migration.

### Should I use the Kayako API or request a MySQL dump?

Use the API when you need controlled extraction, interval syncs, or a final delta before cutover. Use a MySQL dump when completeness and volume matter more than convenience — especially for archived or attachment-heavy instances. Kayako delivers dumps in MySQL format only (no CSV), and download links expire within 1–7 days. For many migrations, the optimal approach is to use the MySQL dump for the initial bulk load and the API for the final delta sync. ([help.kayako.com](https://help.kayako.com/article/48696-requesting-a-data-dump-or-backup-for-your-kayako-instance))

### How do I handle Kayako Classic vs. Kayako Cloud differences?

Kayako Classic uses a different API structure (XML-based, API key authentication, different endpoint paths like `/Tickets/Ticket` instead of `/api/v1/cases`). Classic's database schema also differs from Cloud. If migrating from Classic, use a MySQL dump for extraction rather than the API — Classic's API is less well-documented and has known inconsistencies with archived data retrieval.

### What about GDPR compliance during migration?

The migration ETL pipeline constitutes data processing under GDPR. Store intermediate extracted data encrypted at rest, delete it after migration is complete, and document the migration in your Records of Processing Activities. If using a third-party tool or managed service, ensure a Data Processing Agreement is in place. Verify that GDPR deletion requests processed in Kayako are honored — do not re-import deleted users from a pre-deletion database dump.

## Frequently asked questions

### How long does a Kayako to Zendesk migration take?

A typical migration takes 1–3 weeks with an in-house engineering team, or 3–5 business days with a managed service. The primary variables are ticket volume, attachment count, and number of custom fields requiring transformation.

### Can I migrate Kayako to Zendesk without losing data?

Yes, but only if your extraction includes the &archived=1 parameter when calling the Kayako Cases API. Without it, all tickets closed more than 30 days ago are silently excluded. Attachments over 20MB per post may also fail with a 402 error and require manual handling.

### What data cannot be migrated from Kayako to Zendesk?

Kayako Macros, Triggers, SLA Policies, Automations, and Views cannot be migrated programmatically and must be manually rebuilt in Zendesk. Kayako Journeys have no Zendesk equivalent. Ticket ID numbers are not preserved — Zendesk assigns new IDs. Imported tickets do not carry ticket metrics like first reply time or resolution time.

### Is there a native Kayako-to-Zendesk migration tool?

No. Neither Kayako nor Zendesk provides a native migration tool between the two platforms. Zendesk's built-in CSV import handles users and organizations but not full ticket history with comments and attachments. The Ticket Import API (POST /api/v2/imports/tickets) is the correct endpoint for migration.

### Should I use the Kayako API or request a MySQL dump?

Use the API when you need controlled extraction, interval syncs, or a final delta before cutover. Use a MySQL dump when completeness and volume matter more than convenience — especially for archived or attachment-heavy instances. Kayako delivers dumps in MySQL format only (no CSV), and download links expire within 1–7 days.
