---
title: "Zoho Desk to Puzzel Case Management Migration: Technical Guide"
slug: zoho-desk-to-puzzel-case-management-migration-technical-guide
date: 2026-08-17
author: Rishabh
categories: [Zoho Desk, Migration Guide, Help Desk]
excerpt: "Technical guide to migrating from Zoho Desk to Puzzel Case Management. Covers API extraction, credit limits, data model mapping, and cutover process."
tldr: "Zoho Desk CSV exports strip threads and attachments — extract via API, map Departments to Teams and Custom Fields to Form Fields, and budget 2–4 weeks."
canonical: https://clonepartner.com/blog/zoho-desk-to-puzzel-case-management-migration-technical-guide/
---

# Zoho Desk to Puzzel Case Management Migration: Technical Guide


# Zoho Desk to Puzzel Case Management Migration: Technical Guide

> [!NOTE]
> **TL;DR: Zoho Desk → Puzzel Case Management Migration**
>
> A Zoho Desk to Puzzel Case Management migration is a data-model translation, not a CSV copy job. Zoho Desk's built-in CSV export captures ticket metadata but strips threads, comments, and attachments — you must extract through the Zoho Desk REST API (`/api/v1/tickets/`) or use Zoho's full backup feature where available. On the Puzzel side, tickets are loaded through API ticket channels with Basic Token or OAuth authentication, using tenant-specific Swagger-documented endpoints. Zoho Desk's Departments, Custom Fields, Contacts, and Accounts do not map 1:1 to Puzzel's Teams, Form Fields, Contacts, and Organisations — each requires structural mapping decisions. Automation rules, SLAs, and workflow macros cannot be migrated programmatically. A typical mid-size migration (10,000–50,000 tickets) takes 2–4 weeks.

## What Is a Zoho Desk to Puzzel Case Management Migration?

A **Zoho Desk to Puzzel Case Management migration** moves tickets, contacts, accounts, custom fields, tags, conversation threads, internal notes, and attachments from Zoho Desk into Puzzel Case Management while preserving message history, timestamps, and relational integrity.

Puzzel Case Management is a case and ticket management tool that provides contact centres and helpdesks with the ability to manage written interactions in a secure and efficient way. Each email in the queue is converted into a support ticket, assigned a unique case/ticket ID, and then filtered, categorised, and distributed to the right team or agent. It is part of the broader Puzzel CX ecosystem — designed for contact centres that need skill-based routing, SLA tracking, and multi-channel queuing across email, SMS, and API channels.

Teams move from Zoho Desk to Puzzel for several reasons: consolidating ticketing into an existing Puzzel Contact Centre deployment, moving to a platform built for contact-centre-scale routing, or replacing a Zoho stack with a vendor that better fits European data residency requirements.

Because the two systems use fundamentally different data models — Zoho Desk is a department-based ticketing system; Puzzel is a skill-based, queue-driven contact centre platform — a direct CSV import is impossible. You must extract the data programmatically, transform it to fit Puzzel's schema, and load it via API.

## Is There a Direct Zoho Desk to Puzzel Migration Tool?

Plan for custom work. The public marketplaces and support pages for Help Desk Migration and Import2 both list Zoho Desk as a source, but neither publicly lists Puzzel Case Management as a target as of mid-2025. That does not prove no third party can do it, but it means this is not in the same category as connector-based migrations to Zendesk or Freshdesk.

> [!WARNING]
> If someone proposes this as a pure no-code migration, ask one question early: **how are Zoho threads, Zoho comments, and Zoho attachment binaries being preserved and loaded into Puzzel's ticket timeline?** If the answer is vague, stop there.

## Why CSV Exports Are Not Enough

Zoho Desk offers a built-in export under **Setup → Data Administration → Export**. You can export tickets using this path, with a cap of 3,000 records per export run. Reports can export up to 20,000 records. But there is a hard limitation that makes CSV unsuitable for full migration:

> While exporting a report you can export a maximum of 20,000 records. But such exports would not include the threads or comments. If you need an export that includes threads and comments you need to contact support@zohodesk.com for a backup extraction.

A ticket without its conversation history is metadata — not a migration. You lose staff replies, customer responses, private notes, and the full timeline that agents need when handling follow-ups. CSV exports are useful for quick audits and record-count validation, but they cannot serve as a migration source for any team that needs historical ticket context in Puzzel.

## Zoho Desk Extraction: Backup vs. API

### Decision Matrix: Which Path to Use

Before building your extraction pipeline, choose the right approach:

| Condition | Recommended Path |
|---|---|
| Ticket count < 50,000 and Zoho backup is enabled on your portal | Use Zoho backup as baseline, API for delta |
| Ticket count > 50,000 or backup not accessible | API with date-range partitioning |
| Plan is Free tier | API only (backup may not be available) |
| You need delta sync during live cutover period | API required regardless of backup use |
| Multi-department with different field schemas | API with per-department partitioning |
| Non-email channel tickets (chat, social, telephony) | API only — backup may not include all channel data shapes |

Use backup where you can. Use API where you must. In most migrations, you use both.

### When Zoho Backup Is the Right Baseline

Zoho's full **backup feature** is the best historical baseline when it is enabled in your portal. The backup can include tickets, threads, comments, time entries, and attachments via secure download links. Key constraints:

- Only the primary contact can create or download the backup
- Export limit is 10 million records
- The download link expires after 7 days
- Once a full backup is initiated, another full backup cannot be run for 30 days
- Scheduled backups begin as a full backup and later contain only new and modified data

One documentation wrinkle worth calling out: Zoho's main backup article describes the feature as available for paid editions, while a current FAQ states data backup is in Beta and may require a request to support. Verify what your portal actually exposes before you promise a backup-based runbook to the business.

### Where the REST API Fits

You still need the **Zoho REST API** for delta extraction during the migration window, attachment binary retrieval, and gap-filling — and as the primary path when backup is unavailable.

Zoho Desk uses a **REST API (v1)** with **OAuth 2.0** authentication. You must include your `orgId` header in every request.

Data-center routing is the caller's responsibility: EU organisations must use `desk.zoho.eu`, AU organisations `desk.zoho.com.au`, and so on. Getting the wrong regional endpoint returns authentication failures that look like credential errors — a common red herring during initial setup.

### Non-Email Channel Tickets

Zoho Desk accepts tickets from multiple channels: email, live chat, telephony, social media, and web forms. Each channel produces a different data shape:

- **Chat tickets** contain a transcript object, not threaded email messages — the structure is a flat array of chat events with sender role and timestamp
- **Telephony tickets** may contain call metadata (duration, recording URL, IVR path) rather than message content
- **Social tickets** (Twitter/Facebook integrations) contain platform-specific identifiers and may have character-length constraints on content that email tickets do not

When extracting, filter tickets by `channel` field and apply channel-specific transformation rules. Attempting to treat a chat transcript as an email thread will produce malformed content in Puzzel's message timeline.

## Zoho Desk API: Extraction Constraints

### API Credit System

Zoho Desk does not use a simple requests-per-minute rate limit. Instead, it uses a credit-based system where each API call costs a specific number of credits, and your daily credit pool depends on your edition and user count.

| Edition | Base Credits/Day | Per-User Credits | Max (with add-ons) |
|---|---|---|---|
| Free | 25,000 | — | 25,000 |
| Standard | 50,000 | 500/user | — |
| Professional | 75,000 | 500/user | 1,000,000 |
| Enterprise | 100,000 | 500/user | 2,000,000 |

API credits are tracked on a 24-hour cycle, from 00:00 to 23:59:59, based on the timezone of your data centre. These credits reset every 24 hours.

Key credit costs for migration operations:

- **List tickets:** 3 credits per call
- **Get single ticket:** 1 credit
- **List threads:** 1 credit
- **List comments:** 1 credit
- **Search:** 3 credits per call

Once your credits are exhausted, API calls fail with a `429 Too Many Requests` response and a `Retry-After` header until the next reset. Concurrency limits also apply — the maximum number of simultaneously active API calls ranges from 5 (Free) to 25 (Enterprise).

Zoho exposes credit usage and remaining credits in response headers. Your extraction scripts should pace based on credits consumed, not just request counts:

```python
# Pacing pattern for Zoho extraction workers
weight = int(resp.headers.get('X-Rate-Limit-Request-Weight-v3', 0))
remaining = int(resp.headers.get('X-Rate-Limit-Remaining-v3', 0))

if remaining < 500:
    sleep(30)
```

### Credit Budget Estimation

Each ticket with threads, comments, and attachments costs 4–10+ credits to extract fully. For 30,000 tickets:

- List all tickets: ~900 credits (300 pages × 3 credits)
- Threads per ticket: ~30,000 credits (1 credit × 30,000 tickets)
- Comments per ticket: ~30,000 credits
- Attachments per ticket: ~30,000 credits
- **Total: roughly 90,000–150,000 credits minimum**

On a Professional plan with 50 users (100,000 daily credits), extraction alone may take 2–3 calendar days. On a Standard plan with 50,000 daily credits, the same extraction takes 3–5 days.

### Pagination Ceiling

Pagination uses a 1-based `from` index with a maximum `limit` of 100 records per page. There is a hard ceiling most engineers miss on first implementation: the `from` parameter has a maximum value of 4,999. You can only paginate through the first 5,000 tickets in a single list-call sequence. To extract beyond 5,000 records, you must partition by date range, department, or status and run multiple paginated sequences. This is a documented design constraint in Zoho Desk's API, not a bug.

### Thread and Comment Extraction

The List ticket comments API (`/api/v1/tickets/{ticketId}/comments`) has a limit range of 1–100 with a default of 50. The List threads API (`/api/v1/tickets/{ticketId}/threads`) has a limit range of 1–200 with a default of 100.

For tickets with very long conversation histories (100+ messages), you must paginate within each ticket. The extraction pattern:

```python
# Full ticket extraction from Zoho Desk
for ticket in paginate_tickets(date_from, date_to):
    threads = paginate_all("/api/v1/tickets/{id}/threads", limit=200)
    comments = paginate_all("/api/v1/tickets/{id}/comments", limit=100)
    attachments = get("/api/v1/tickets/{id}/attachments")
    yield {
        "ticket": ticket,
        "threads": threads,
        "comments": comments,
        "attachments": [download(a) for a in attachments]
    }
```

> [!TIP]
> **Plan extraction across multiple 24-hour credit windows.** Start over a weekend if possible. Use date-range partitioning to avoid the `from` parameter's 4,999-record ceiling, and build in exponential backoff for the undocumented per-minute throttle. Users report receiving HTTP 429 "The maximum number of API calls that can be made per minute has been exceeded" errors during bulk sync jobs even before hitting the daily credit limit.

### Attachment Handling: Cloud Storage Links

Zoho Desk Enterprise integrations can store attachments in connected cloud storage — Google Drive, Dropbox, OneDrive. In these configurations, the Zoho attachment record contains a reference URL to the external storage provider, not a binary hosted on Zoho's servers.

Attempting to download these as standard attachments will return a redirect or permission error. You must:
1. Identify attachments where the `href` field points to an external domain rather than `desk.zoho.com`
2. Use the appropriate cloud storage API (Google Drive API, Dropbox API) with valid credentials to download the binary
3. Re-upload to Puzzel as a standard attachment

Build your attachment audit to flag external-storage references before extraction begins.

## Puzzel Case Management API: Loading Constraints

On the Puzzel side, tickets are created through **API ticket channels**. An API ticket channel is a distinct inbound channel type in Puzzel's routing model — separate from email channels, which are associated with a mailbox address. API channels accept HTTP POST requests and route resulting tickets through the same queue and skill-based assignment logic as email tickets, but without an associated email address. This means:

- Tickets created via API channel are not linked to any inbound mailbox
- Replies sent from the API channel go out via Puzzel's configured outbound email, not the ticket's original inbound address (unless you explicitly configure the reply-to)
- Authentication is scoped to the channel — a Basic Token or OAuth Token associated with that specific channel configuration

To configure: go to **Settings → Ticket Channels → API**, create a new channel, select an authentication level (Global or Organisation-scoped), and choose your Token Type (Basic Token or OAuth Token).

The API documentation is powered by Swagger UI, accessible from the Help menu, and is tenant-specific — your instance's endpoints and schemas are documented against your actual configuration. This means rate limits, payload schemas, and available form fields will differ between Puzzel tenants. Always test against your specific Swagger environment before finalising your loader.

### Puzzel API Rate Limits and Error Codes

Puzzel's public documentation does not publish explicit per-minute or per-day rate limits for the Case Management API as of mid-2025 — this is a gap you should resolve with your Puzzel account team before building your loader. Based on observed behaviour in API channel integrations:

- Sustained high-volume POSTing (>10 requests/second) triggers HTTP 429 responses
- Error payloads return a JSON body with a `code` field and `message` field
- Common error codes encountered during migration loads:

| HTTP Status | Likely Cause | Resolution |
|---|---|---|
| 400 Bad Request | Missing required field, invalid enum value, malformed JSON | Check field schema against your tenant's Swagger spec |
| 401 Unauthorized | Token expired, wrong auth scope, IP not whitelisted | Verify token, check IP whitelist in channel config |
| 404 Not Found | Referenced Team, Form, Category, or Organisation does not exist | Pre-create all structural elements before loading tickets |
| 413 Payload Too Large | Attachment exceeds size limit or ticket body too large | Split attachments, strip signatures from historical messages |
| 429 Too Many Requests | Rate limit exceeded | Implement exponential backoff; confirm rate limits with Puzzel support |
| 500 Internal Server Error | Malformed HTML in ticket body, invalid character encoding | Sanitise HTML content; normalize to UTF-8 before posting |

Confirm specific rate limits with your Puzzel account team before designing your loader's concurrency model.

### What Puzzel's API Supports for Import

Based on the Puzzel API documentation and the Digital Marketplace service listing, the API supports:

- Creating tickets with content, subject, and metadata
- Assigning Teams, Categories, Forms, and Form Fields
- Tagging tickets
- Associating tickets with Organisations
- Adding notes (public and private) — when adding a note to a ticket using the API, it is possible to add a flag to mark it as public or private; when a note is marked private, it is not exposed when retrieving ticket content via an API channel
- Uploading attachments
- Reply and Inbound Message endpoints for reconstructing conversation threads

### Handling Existing Tickets in Your Puzzel Tenant

If your Puzzel tenant already has live tickets before migration begins, you must resolve several collision risks:

**ID namespace**: Zoho Desk ticket IDs (numeric) will not match Puzzel's internal ticket IDs. Maintain a mapping table in your staging database — `zoho_ticket_id → puzzel_ticket_id` — from the moment of first creation. Never rely on sequential IDs matching.

**Deduplication**: If any tickets were manually re-created in Puzzel before migration, your loader must check for duplicates. Use a combination of subject, contact email, and creation timestamp as a composite deduplication key — no single field is reliable alone.

**Contact collision**: If contacts already exist in Puzzel (created by agents manually or imported earlier), your contact creation step must use upsert logic rather than blind insert. Email address is the safe primary deduplication key for contacts.

**Organisation collision**: Same logic applies for Organisations. Query existing organisations by name before attempting to create.

### What Cannot Be Migrated Programmatically

- **Business Rules / Event Rules** — Puzzel's automated workflows are configured through the admin UI, not importable via API
- **SLA policies** — must be manually recreated to match your Zoho Desk SLA targets
- **Email templates and canned responses** — recreated manually through the admin interface
- **Agent routing assignments** — skill-based routing rules must be reconfigured in Puzzel's settings
- **Knowledge Base articles** — Puzzel's Customer Hub is a separate paid add-on and does not accept API-driven article import
- **Zoho Desk macros and blueprints** — these have no programmatic export and must be rebuilt as Business Rules in Puzzel

### Attachment and Network Constraints

Puzzel blocks many executable and script-like file extensions. Size guidance is 50MB for incoming email and 10MB for outgoing email. Encoding adds roughly 30%, and emails over 40MB can fail delivery and leave the ticket in `ERROR` status. Build a pre-flight report that flags oversized attachments and blocked extensions before your pilot load.

Puzzel supports IP whitelisting that can prevent non-whitelisted IP addresses from requesting an API OAuth token. If your loader runs from elastic cloud IPs or a CI runner, settle this before your dress rehearsal by either fixing the loader's egress IP or adding a static NAT.

## Data Model Mapping: Zoho Desk → Puzzel

This is where most migration projects succeed or fail. The two systems organise ticket metadata differently.

| Zoho Desk Concept | Puzzel Equivalent | Notes |
|---|---|---|
| **Departments** | **Teams** | 1:1 is typical, but only if the department represents routing ownership. If it represents a product line, map to Category or Form instead |
| **Contacts** | **Contacts** | Direct mapping; email is the natural dedupe key |
| **Accounts** | **Organisations** | Optional in Puzzel. Only model explicitly if account structure drives SLA, reporting, or ownership |
| **Custom Fields** | **Form Fields** | Zoho's per-department fields → Puzzel's Form Fields. Types may not map 1:1 (e.g., Zoho multi-select may require text or separate categories) |
| **Categories (Zoho)** | **Categories (Puzzel)** | Both use hierarchical categories, but structures differ — Zoho uses Classification, so you may need to flatten or remap |
| **Tags** | **Tags** | Direct mapping after normalization |
| **Ticket Threads** | **Messages** | Zoho separates threads (email replies) from comments (internal notes). In Puzzel, inbound/outbound messages and notes appear on the same ticket timeline |
| **Comments** | **Notes** | Private comments → private notes; public comments → public notes |
| **Attachments** | **Attachments** | Download from Zoho, re-upload to Puzzel via multipart/form-data |
| **Ticket Status** | **Status** | Zoho uses Open, On Hold, Escalated, Closed, plus custom statuses. Puzzel has its own status model — map carefully |
| **Priority** | **Priority** | Usually maps directly (Low, Medium, High, Urgent) — verify both systems' definitions |
| **Agents** | **Users** | Create users in Puzzel first; map by email address |
| **SLAs** | **SLAs** | Manual recreation required |
| **Workflows/Macros** | **Business Rules** | Manual recreation required |
| **Chat transcripts** | **Notes or Messages** | Chat tickets have no direct equivalent — flatten as a timestamped note block or series of inbound/outbound messages |
| **Telephony metadata** | **Custom Form Fields** | Call duration, recording URL, IVR path — store as Form Fields if needed for reporting |
| **Parent/Child tickets** | **Linked tickets or tags** | Puzzel does not have a native parent-child ticket hierarchy equivalent to Zoho's. Use tags or a custom Form Field to preserve the relationship reference |

> [!WARNING]
> **Zoho Desk Custom Fields are scoped per department.** Two departments can have different custom fields with the same name but different types. When flattening into Puzzel's Form Fields (which are global to a Form), you must resolve naming collisions and type mismatches before loading. Run a cross-department field audit before writing transformation code.

### Ticket Relationships: Parent-Child and Linked Tickets

Zoho Desk supports parent-child ticket relationships and linked tickets (tickets linked by association, not hierarchy). Puzzel Case Management does not have a native equivalent to Zoho's parent-child ticket structure.

Options for preserving this relationship:
1. **Tag-based reference**: Add tags like `parent:ZD-12345` and `child:ZD-12346` to both tickets during import
2. **Form Field reference**: Create a custom Form Field `Related Ticket` and populate it with the Zoho ticket ID of the related case
3. **Note annotation**: Prepend a structured note to each ticket: `[Linked ticket: ZD-12345 — migrated relationship, not enforced in Puzzel]`

Option 1 is the lowest-friction implementation. Option 2 is better if agents need to search or filter by relationship. Option 3 provides the clearest audit trail.

### Multi-Language and Multi-Timezone Portals

Zoho Desk supports multi-language portals where ticket content, auto-responses, and field labels may be configured per language. Puzzel Case Management's language support is configured separately and does not auto-detect ticket language for routing.

Key concerns for multi-language Zoho portals:
- Zoho stores the ticket's portal language in the `language` field — extract and preserve this
- Auto-response templates in Zoho are language-specific; recreate equivalent templates in Puzzel's email templates
- Field labels in Zoho may be translated per language; Puzzel Form Field names are defined once — use canonical English names and document the translation mapping separately

Zoho stores timestamps in the portal's configured timezone. Puzzel expects UTC. Convert **all** timestamps during transformation — getting this wrong means ticket timelines are off by hours, thread interleaving breaks chronologically, and SLA calculations in Puzzel produce incorrect results for historical tickets.

### Forms and Form Fields in Puzzel

Puzzel Case Management placeholders include `{{form:name}}` for the selected form, `{{form_field:FORM_FIELD_NAME}}` for specific form field values, and `{{category:CATEGORY_NAME}}` for category-specific values. Forms and Form Fields are first-class objects — they drive templates, automation, and reporting.

Use **categories** for short picklists that drive routing, assignment, or reporting. Use **forms and form fields** for case-specific intake data that agents need to see but that should not overload your routing model. Plan your Forms before importing a single ticket — changing Form Field structures after loading 30,000 tickets means re-processing everything.

### Agent Mapping and Orphaned Records

One of the most common failure points is mapping inactive agents. If a ticket in Zoho Desk is assigned to an agent who left the company three years ago, that agent will not have an active seat in Puzzel. Attempting to load a case assigned to an unmapped or inactive user ID causes the API to reject the request.

The fix:
1. Create a "Legacy Agent" or "System User" account in Puzzel
2. Map all inactive Zoho agents to this single Puzzel account
3. Prepend the original agent's name to the first historical note in the case (e.g., `[Originally assigned to: John Doe]`)

Zoho Desk also allows tickets without contacts in some configurations. Puzzel expects every ticket to be associated with a contact or organisation. Build a catch-all contact (e.g., "Unknown Contact") for unlinked tickets and flag them for agent review post-migration.

## Step-by-Step Migration Process

### Step 1: Audit Your Zoho Desk Data

Before writing any code, run an audit:
- Total ticket count by department, status, and channel (email, chat, telephony, social)
- Tickets with >100 threads (these hit pagination boundaries)
- Custom fields in use per department (identify naming collisions and type mismatches)
- Attachment sizes (flag anything over Puzzel's per-file limits)
- Attachment types (identify blocked extensions early)
- Attachments stored in external cloud storage (Google Drive, Dropbox links)
- Contact and Account count, including orphan records (tickets with no contact)
- Inline images in ticket bodies (Base64 encoded images break during transfer)
- Parent-child and linked ticket relationships
- Multi-language configuration and which languages are in use
- Inactive agents assigned to open or recent tickets

Use Zoho Desk's built-in reporting or the `/api/v1/ticketsCountByFieldValues` endpoint to get fast counts without burning excessive credits.

### Step 2: Build the Structural Mapping

Create a mapping document that covers:
- Department → Team assignments
- Custom Field → Form Field translations (with type coercions documented explicitly)
- Status mapping (including any custom statuses — document the business logic behind each)
- Category hierarchy mapping (Zoho classification → Puzzel category tree)
- Agent email → Puzzel User mapping (flag inactive agents)
- Organisation/Account mapping
- Parent-child relationship preservation strategy
- Channel-specific data shape handling (chat, telephony, social)

Get stakeholder sign-off on this document before proceeding. Mapping disagreements discovered during production loading cost days.

### Step 3: Configure Puzzel Before Loading

In Puzzel Case Management, pre-create in this order (dependencies matter):

1. **Teams** — tickets reference Teams; Teams must exist first
2. **Categories and sub-categories** — must exist before ticket categorisation
3. **Forms and Form Fields** — must exist before ticket form data is assigned
4. **Tags** — create all tags present in Zoho Desk
5. **Organisations** — must exist before tickets reference them
6. **Users/Agents** — must exist before tickets are assigned; create the "Legacy Agent" catch-all
7. **API ticket channel** — configure with appropriate authentication; note the channel ID for your loader

Record the Puzzel IDs of every pre-created object. Your transformation layer will need these to construct valid API payloads.

### Step 4: Extract from Zoho Desk

Build your extraction pipeline:

1. **Use Zoho backup as the historical baseline** when available — tickets, threads, comments, and attachment references
2. **Partition by date and department** to stay under the 5,000-record `from` parameter ceiling when using the API
3. **Identify channel type per ticket** and apply channel-specific extraction logic for chat, telephony, and social tickets
4. **Fetch threads and comments per ticket** — this is the bulk of your credit spend
5. **Download attachment binaries** to local or cloud storage, keyed by ticket ID; handle external storage (Google Drive, Dropbox) separately with their respective APIs
6. **Store raw JSON** for every extracted record — you will need it for debugging transformation issues
7. **Respect the per-minute throttle** — implement exponential backoff, as users report 429 errors during bulk sync before hitting the daily credit ceiling

### Step 5: Transform and Load

The transformation layer handles:

- **Chronological interleaving**: Combine Zoho Threads (email replies) and Comments (internal notes) into a single chronological array sorted by `createdTime`. This ensures internal notes appear in correct context alongside customer emails when agents read the historical case in Puzzel.
- **Channel-specific reshaping**: Flatten chat transcripts into timestamped note blocks; convert telephony metadata into Form Field values.
- **HTML content sanitisation**: Zoho Desk threads contain HTML email content with inline images, CSS, and signature blocks. Normalise to UTF-8 and strip malformed HTML before posting — Puzzel returns HTTP 500 on some malformed HTML payloads.
- **Inline image extraction**: Images pasted directly into Zoho Desk email bodies (Base64 encoded) break during transfer. Write a parser to extract Base64 strings, convert them to standard attachments, and replace inline `<img>` tags with text links or hosted attachment references.
- **Field value mapping**: Convert Zoho Desk field values to Puzzel equivalents — status, priority, and all custom fields need explicit translation tables.
- **Attachment upload**: Upload binaries via multipart/form-data; capture returned Puzzel attachment IDs to link to the correct case messages.
- **Timestamp conversion**: Convert all Zoho timestamps from portal timezone to UTC before writing to Puzzel.

```json
// Example: Transformed ticket payload for Puzzel API
{
  "subject": "Order #12345 delivery issue",
  "content": "Original message content...",
  "team": "Customer Service",
  "priority": "High",
  "status": "Closed",
  "categories": {
    "QueryType": "Complaint",
    "Product": "Shipping"
  },
  "formFields": {
    "OrderNumber": "12345",
    "Region": "EMEA"
  },
  "tags": ["escalated", "shipping"],
  "organisation": "Acme Corp"
}
```

### Step 6: Validate and Run Delta Sync

After the initial production load:
- Compare ticket counts: source vs. target, broken down by department and status
- Spot-check 50–100 tickets across different departments, date ranges, statuses, and channel types
- Verify thread chronological order and completeness for tickets with >20 messages
- Confirm attachments are accessible and not broken (including previously external-storage attachments)
- Validate that custom field values landed in the correct Form Fields
- Verify parent-child relationship tags or Form Fields are correctly populated
- Check that UTC timestamps produce correct display times in Puzzel's UI

Run a **delta sync** to capture tickets created or updated in Zoho Desk between extraction start and cutover. Use the `modifiedTimeRange` parameter:

```
GET /api/v1/tickets?modifiedTimeRange=2025-09-01T00:00:00Z,2025-09-05T23:59:59Z
```

Your delta script must:
1. Check the mapping table (stored in your staging database) for whether the ticket already exists in Puzzel
2. If it does not exist, create it as a new case via the Puzzel API
3. If it does exist, fetch threads and comments from Zoho, compare against what was already loaded, and append only the new messages
4. For any ticket that changed status, send an explicit status update API call to Puzzel — status changes do not propagate automatically

> [!WARNING]
> **Status overrides during delta sync**: If a ticket was "Open" during the initial extraction but "Closed" during the delta period, your script must explicitly send a status update API call to Puzzel. Do not assume status changes propagate automatically from the initial ticket record.

### Step 7: Cutover

- Update DNS records and email forwarding rules to route incoming support emails to Puzzel's email queues
- Disable Zoho Desk's inbound email processing
- Run a final delta sync
- Verify new tickets are flowing correctly into Puzzel's queues
- Lock user access in Zoho Desk to prevent agents from accidentally replying in the wrong system
- Keep Zoho Desk in read-only mode for 2–4 weeks as a reference fallback
- Run a final validation script verifying ticket counts, open case statuses, category assignments, and agent assignments match between staging data and Puzzel

## Common Failure Modes

The following failure modes are documented with resolution steps, not just identification:

**Credit exhaustion mid-extraction.** On a Standard plan with 50,000 daily credits and 50 users, extracting 20,000 tickets with full threads exhausts the daily budget in 4–8 hours depending on thread density. Resolution: Plan extraction across multiple credit windows, purchase add-on credits before starting, or upgrade the plan temporarily. Use the `X-Rate-Limit-Remaining-v3` header to pause workers before exhaustion rather than waiting for 429 errors.

**Pagination ceiling hit unexpectedly.** Engineers who test with small datasets miss the `from` parameter's 4,999 ceiling entirely — it only manifests at scale. Resolution: Always partition by date range or department regardless of expected volume. Do not build an extractor that assumes unlimited sequential pagination.

**Orphaned contacts blocking ticket creation.** Zoho Desk allows tickets without contacts in some configurations; Puzzel requires a contact or organisation reference. Resolution: Create a catch-all contact ("Unknown Contact") in Puzzel during pre-configuration, and flag all tickets mapped to this contact for agent review post-migration.

**HTML content causing HTTP 500 on Puzzel load.** Specific patterns in Zoho Desk email bodies — malformed HTML, unsupported CSS properties, non-UTF-8 character encoding — cause Puzzel's API to return 500 errors with minimal diagnostic information. Resolution: Run an HTML sanitisation pass using a library like `bleach` (Python) or `sanitize-html` (Node.js) before loading. Normalise all content to UTF-8. Test with your 50 most complex HTML tickets in the pilot load, not after production.

**Form Field type mismatches.** A Zoho multi-select dropdown has no direct equivalent in every Puzzel Form Field type. Resolution: Concatenate multi-select values into a delimited text field, or create multiple boolean Form Fields (one per option). Document the transformation rule in your mapping document before loading.

**External attachment downloads failing.** Tickets where Zoho attachments reference Google Drive or Dropbox return redirects rather than binaries. Resolution: Build attachment type detection into your extractor before attempting downloads. Use the appropriate cloud storage API with valid OAuth credentials.

**Parent-child relationships lost silently.** Zoho Desk's ticket hierarchy has no Puzzel equivalent, and a naive migration will lose these relationships without error — tickets will load successfully but the relationship context will be gone. Resolution: Audit parent-child relationships during Step 1 and implement one of the three preservation strategies documented in the Data Model Mapping section.

**Non-email channel data shape mismatch.** Chat transcripts loaded as email threads produce garbled or malformed message timelines in Puzzel. Resolution: Filter by `channel` field during extraction and apply channel-specific transformation rules. Flatten chat transcripts to timestamped note blocks.

**Treating CSV export as the primary archive.** CSV exports do not include threads or comments. This mistake causes first-run migrations to succeed technically (tickets load) but fail operationally (agents have no conversation history). There is no remediation except re-extracting from the API or requesting a Zoho backup.

## Timeline and Resource Estimates

For a mid-size migration (10,000–50,000 tickets with full thread history):

| Phase | Duration | Key Activities |
|---|---|---|
| Planning & Mapping | 3–4 days | Data audit (all channels), field mapping, relationship mapping, stakeholder sign-off |
| Puzzel Configuration | 2–3 days | Teams, Categories, Forms, Form Fields, API channel setup, Legacy Agent creation |
| Extraction | 3–5 days | API extraction across credit windows, cloud attachment retrieval, raw JSON storage |
| Transformation & Test Load | 3–5 days | Field mapping, HTML sanitisation, channel reshaping, test imports, validation |
| Production Load & Delta Sync | 2–3 days | Full load, incremental sync, spot checks across all validation dimensions |
| Cutover & Validation | 2–3 days | Email redirect, final delta, read-only period, agent confirmation |
| **Total** | **15–23 days** | |

Smaller migrations (<5,000 tickets, email channel only) can compress to 1–2 weeks. Larger migrations (100,000+) typically take 4–6 weeks, primarily gated by Zoho Desk's API credit limits. Multi-channel migrations (including chat, telephony, or social tickets) add 3–5 days for channel-specific transformation work regardless of ticket volume.

## Complexity Thresholds: When to Add Engineering Resources

This migration is manageable for an engineering team with API integration experience and 2–3 weeks of dedicated time. Objective criteria that increase complexity:

- **50,000+ tickets**: Credit math becomes a multi-week extraction project; consider purchasing add-on credits or a temporary plan upgrade
- **Multiple departments with different custom field schemas**: Naming collision resolution requires cross-functional stakeholder time, not just engineering time
- **Non-email channels in use** (chat, telephony, social): Each channel type requires separate transformation logic
- **Parent-child ticket relationships** at scale: Relationship preservation adds validation complexity proportional to the number of linked tickets
- **Multi-language portal configuration**: Template recreation and field label mapping require localisation decisions
- **Puzzel tenant already has live tickets**: Deduplication and ID mapping logic adds significant complexity to the loader
- **Zero-downtime requirement**: Delta sync orchestration with status reconciliation is non-trivial to implement correctly
- **Puzzel's Swagger API endpoints have tenant-specific undocumented behaviors**: Require direct investigation with Puzzel support during development

---

Related technical guides: [Zoho Desk to Freshdesk migration](https://clonepartner.com/blog/blog/zoho-desk-to-freshdesk-migration-the-complete-technical-guide/), [Zoho Desk to Zendesk migration](https://clonepartner.com/blog/blog/zoho-desk-to-zendesk-migration-the-ctos-guide/), [HappyFox to Puzzel migration](https://clonepartner.com/blog/blog/happyfox-to-puzzel-case-management-migration-technical-guide/).

> Need help migrating from Zoho Desk to Puzzel Case Management? Our engineers handle the extraction, transformation, and loading — including field mapping, API credit management, channel-specific data reshaping, and zero-downtime cutover. Book a 30-minute call to scope your migration.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I use Zoho Desk CSV export to migrate to Puzzel Case Management?

No. Zoho Desk's built-in CSV export only captures ticket metadata — it strips all threads, comments, and attachments. You must use the Zoho Desk REST API or Zoho's full backup feature to extract complete conversation history for migration.

### How long does a Zoho Desk to Puzzel migration take?

A mid-size migration (10,000–50,000 tickets) typically takes 2–4 weeks, including planning, extraction, transformation, test loading, production loading, and cutover. Extraction time is often gated by Zoho Desk's daily API credit limits.

### What are Zoho Desk API rate limits for migration?

Zoho Desk uses a credit-based system. Base daily credits range from 25,000 (Free) to 100,000 (Enterprise), plus 500 per paid user. Listing tickets costs 3 credits per call; getting a single ticket costs 1 credit. There is also a 5,000-record pagination ceiling and undocumented per-minute throttles that return HTTP 429.

### How do Zoho Desk custom fields map to Puzzel Case Management?

Zoho Desk Custom Fields map to Puzzel Form Fields. Zoho scopes custom fields per department while Puzzel Form Fields are global to a Form, so you need to resolve naming collisions and type mismatches during mapping before loading any tickets.

### Can I migrate Zoho Desk automations and SLAs to Puzzel?

No. Zoho Desk workflows, macros, blueprints, and SLA policies cannot be migrated programmatically. Business Rules, SLAs, and Event Rules must be manually recreated in Puzzel's admin interface.
