Deskpro to Zoho Desk Migration: A Technical Guide
A technical guide to migrating from Deskpro to Zoho Desk. Covers API constraints, object mapping, credit limits, attachment handling, and edge cases.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Deskpro to Zoho Desk Migration: A Technical Guide
Migrating from Deskpro to Zoho Desk has no built-in path. Zoho Desk's native migration utility (Zwitch) supports Freshdesk and Zendesk, but Deskpro isn't on the list. That means every Deskpro-to-Zoho migration is either a CSV export/import, a custom API pipeline, a third-party tool, or a managed migration service.
If you treat this as a simple CSV exercise, you will orphan ticket threads, lose inline attachments, and break historical SLA reporting. This guide covers the real technical constraints, object mapping, and architecture decisions you'll face.
If you've already worked through a Zoho Desk migration from another platform, some patterns will be familiar — see our Zendesk to Zoho Desk migration guide for comparison.
Why Teams Migrate from Deskpro to Zoho Desk
The most common triggers:
- Cost consolidation. Teams already on Zoho CRM, Zoho Projects, or Zoho One want a single vendor for helpdesk. Zoho Desk pricing: $7/user/month (Express), $14 (Standard), $23 (Professional), $40 (Enterprise). Deskpro's agent-based pricing starts at $29/agent/month (Team) and $59/agent/month (Professional). For a 20-agent team, that's $460–$800/month on Zoho Desk vs. $580–$1,180/month on Deskpro.
- Simpler administration. Deskpro is powerful but configuration-heavy. Zoho Desk's opinionated defaults appeal to teams that don't want to maintain a custom helpdesk build.
- Ecosystem fit. Zoho Desk natively connects to Zoho CRM, Zoho Analytics, and Zoho SalesIQ with pre-built bidirectional sync. If those tools are already in your stack, the integration story is stronger than Deskpro's webhook-based approach.
- Multi-brand consolidation. Zoho Desk Enterprise supports multi-department and multi-brand help centers (up to 15 departments), which can replace separate Deskpro instances for agencies managing multiple clients.
Core Architecture Differences: Deskpro vs Zoho Desk
Before mapping fields, understand the structural gaps.
| Concept | Deskpro | Zoho Desk |
|---|---|---|
| Ticket model | Tickets with messages (replies), notes, logs | Tickets with threads (replies), comments |
| User model | "People" (users/contacts) + Organizations | Contacts + Accounts (companies) |
| Agent model | Agents with teams and departments | Agents with teams and departments |
| Custom fields | Per-ticket, per-user, per-organization | Per-ticket, per-contact, per-account |
| Knowledge base | KB with categories, articles, downloads, news | Knowledge Base with categories, sections, articles |
| Automation | Triggers, escalations, SLAs, macros | Workflows, assignment rules, SLAs, macros, Blueprints |
| Custom modules | Not natively supported | Enterprise only: max 10 modules, 5 custom lookups per module. Cannot be deleted after creation. (help.zoho.com) |
| Chat/messaging | Built-in live chat | Zoho SalesIQ integration (separate product) |
| API version | REST API v2 (/api/v2/) |
REST API v1 (/api/v1/) |
| API rate limiting | Per-key hourly/daily limits (configurable; cloud defaults ~500 requests/minute) | Credit-based daily system (see credit table below) + 15 requests/minute per org on Free plan, higher on paid |
The biggest structural difference: Deskpro uses "People" as a unified contact record with Organizations as a grouping layer. Zoho Desk separates Contacts and Accounts and links them relationally. A Deskpro person can belong to multiple organizations; Zoho Desk Contacts typically have a single primary Account unless you enable the multiple-accounts feature.
Ticket messages in Deskpro map to threads in Zoho Desk, but internal notes map to comments — and the API endpoints for each are different. Deskpro's department structure does not map 1:1 with Zoho Desk's department isolation, requiring careful permission mapping.
Zoho Desk's published standard modules do not include CRM objects like Leads or Opportunities. If Deskpro has sales-style data in custom fields, route it to Zoho CRM or, if the edition allows, a carefully designed Enterprise custom module. (help.zoho.com)
Migration Approaches Compared
1. Native CSV Export/Import
How it works: Export CSVs from Deskpro's Report Builder (DPQL queries) and upload them via Zoho Desk's Setup → Data Administration → Import Data.
When to use it: Under 5,000 tickets with minimal custom fields and no attachment requirements.
Pros:
- No code required
- Good enough for a quick proof of concept
Cons:
- Deskpro's Report Builder limits output to approximately 55 fields per query and caps result volume. Split large pulls into ID ranges after about 2,500 tickets. (support.deskpro.com)
- Zoho Desk CSV import does not preserve ticket threads, comments, or attachments — you get flat records only
- No relationship linking (ticket-to-contact-to-account chains break)
- Zoho's CSV import caps at 10,000 rows or 30 MB per file and runs in GMT, not the importing user's timezone (help.zoho.com)
- No way to set
createdTimeor preserve original ticket IDs via CSV - CSV import is only available on Professional and Enterprise plans via the UI
Complexity: Low | Scalability: Small datasets only | Risk: High data loss on threads and attachments
2. API-Based Migration (Deskpro API → Zoho Desk API)
How it works: Extract data from Deskpro's REST API v2, transform it in staging storage, and write it into Zoho Desk's REST API v1.
When to use it: Any migration over 5,000 tickets, or when you need full-fidelity transfer of threads, attachments, custom fields, and relationships.
Pros:
- Full control over every object and field
- Can preserve thread order, timestamps, and attachment binaries
- Handles custom field mapping and picklist value translation
- Can be run incrementally (delta sync during cutover window)
Cons:
- Requires development effort (Python, Node.js, or similar) — typically 2–4 weeks for a production-quality pipeline
- Must handle Zoho Desk's API credit system and Deskpro's rate limits
- Attachment migration is a two-step process on both sides (blob download from Deskpro, attachment upload to Zoho)
Typical throughput: On a Professional plan with 10 agents (80,000 credits/day), expect roughly 10,000–15,000 tickets/day including threads and attachments (assuming ~5–7 credits per ticket with threads). A 50K-ticket migration runs approximately 4–5 days of continuous processing.
Complexity: High | Scalability: Enterprise-grade | Risk: Low if properly tested
3. Third-Party Migration Tools
Tools like Help Desk Migration and Migration Wizard offer automated wizards for moving data into Zoho Desk. Zoho's own Zwitch runs in two phases, recommends a sample migration first, and can move agents, accounts, contacts, products, tickets, threads, comments, and attachments — but it only supports Freshdesk and Zendesk as source platforms, not Deskpro. (help.zoho.com)
When to use it: Mid-size migrations where you want automation but don't have developer bandwidth.
Pros:
- No-code interface
- Built-in field mapping and validation
- Trial migration typically included
Cons:
- Limited customization for complex field transformations
- Per-record pricing can be expensive at scale (Help Desk Migration charges roughly $40–$70 per 1,000 records depending on volume)
- May not support all Deskpro custom field types or attachment edge cases
- You're dependent on the vendor's mapping logic — hard to debug when things break
Complexity: Low–Medium | Scalability: Medium | Risk: Medium
4. Custom ETL Pipelines
How it works: Extract directly from Deskpro's SQL database (if on-premise, typically MySQL/MariaDB with tables like tickets, ticket_messages, people, organizations) or API, stage raw data in a warehouse (Snowflake, BigQuery, PostgreSQL), transform via dbt or custom scripts, build crosswalk tables, and push to Zoho Desk in dependency order.
When to use it: Enterprise migrations involving millions of records, merging multiple helpdesks into one, or mixed destinations where some data belongs in Zoho Desk and some in Zoho CRM.
Pros:
- Handles massive scale and complex deduplication
- Full auditability and control
- Supports delta sync before cutover
- Direct SQL extraction from on-premise Deskpro bypasses API rate limits and the 1,000-result filter cap entirely
Cons:
- Requires dedicated data engineering infrastructure
- You are building and operating migration software, not just moving records
Complexity: High | Scalability: Enterprise | Risk: Low if well-engineered
5. Middleware Platforms (Zapier, Make, Zoho Flow)
When to use it: Ongoing sync of new tickets between systems during a transition period. Not for historical data migration.
Pros:
- Quick to set up for simple ticket creation triggers
- Can bridge the cutover gap
Cons:
- Not designed for bulk historical migration
- Per-task pricing makes large volumes prohibitively expensive (Zapier charges ~$0.01–$0.03 per task at scale)
- No batch operations — every record is a separate execution
- Poor handling of relational data (contact → account → ticket chains)
- Burns API credits rapidly
Complexity: Low | Scalability: Very limited | Risk: High for historical data
Migration Approach Comparison
| Approach | Full Fidelity | Attachments | Custom Fields | Scale | Complexity |
|---|---|---|---|---|---|
| CSV Export/Import | ❌ No threads | ❌ | Partial | < 5K tickets | Low |
| API-Based Pipeline | ✅ | ✅ | ✅ | Unlimited | High |
| Third-Party Tool | Partial | ✅ (varies) | Partial | 5K–100K | Low–Med |
| Custom ETL | ✅ | ✅ | ✅ | Enterprise | High |
| Middleware | ❌ | ❌ | Limited | < 1K | Low |
| Managed Migration Service | ✅ | ✅ | ✅ | Unlimited | Low (for you) |
Which Approach Fits Your Scenario?
- Small team, < 5K tickets, no attachments needed: CSV export/import or a third-party tool.
- Enterprise, 50K+ tickets, full history required: API-based pipeline or managed service. No shortcut.
- Low engineering bandwidth, attachments and custom fields matter: Third-party tool or managed service.
- Ongoing sync during transition: Middleware for new tickets, API pipeline for historical backfill.
When a Managed Migration Service Makes Sense
Building an API-based migration in-house is a significant time investment for a one-time operation. The scripts you write will be discarded the day after cutover.
DIY migrations fail in predictable ways:
- Broken relationships. You import contacts, then tickets, but the contact-to-account linkage doesn't carry over because IDs change between systems. Agents see orphaned tickets with no customer context.
- Silent data loss. CSV imports drop threads and attachments without warning. You don't discover this until an agent searches for a customer's history post-go-live.
- Rate limit surprises. A naive script that doesn't throttle will burn through Zoho Desk's daily API credits in the first hour and stall the migration for 24 hours.
- Hidden engineering cost. A "two-week" migration project quietly consumes 6–8 weeks of engineering time when you account for edge cases, retries, and validation.
Disclosure: ClonePartner is a migration services vendor. We've handled helpdesk migrations across platforms including Deskpro, Zoho Desk, Zendesk, Freshdesk, and Intercom — with 1,500+ completed migrations. We handle the relationship graph (organizations → contacts → tickets → threads → attachments), custom field translation, and Zoho Desk's API credit budgeting so your engineering team stays on product work. For complex Deskpro setups with heavy custom fields or multi-brand configurations, a managed approach typically cuts delivery time from weeks to days. That said, the technical guidance in this post applies regardless of whether you build in-house or use any migration service.
Pre-Migration Planning Checklist
Before writing any code or engaging any tool:
Data Audit
- Count tickets by status (open, closed, archived) — decide which statuses to migrate
- Count contacts and organizations
- Inventory all custom fields (ticket, user, organization) and their types (text, picklist, date, checkbox, numeric)
- Identify attachment volume (total count + total size in GB)
- Document all active triggers, escalations, and SLA policies
- List all departments and agent teams
- Export a sample of KB articles if knowledge base is in scope
- Identify any sales-style data (pipeline stages, account ownership, opportunities) hiding in Deskpro custom fields — decide now whether it lands in Zoho Desk, an Enterprise custom module, or Zoho CRM
- Calculate estimated API credit consumption: (tickets × ~5–7 credits) + (contacts × 1) + (accounts × 1) + (KB articles × 1)
Scope Definition
- Define the cutoff date — how far back does ticket history need to go?
- Decide whether to migrate closed/archived tickets or only active ones
- Identify unused custom fields and orphan contacts to exclude
- Determine whether KB articles, downloads, and news posts are in scope
- Document which Deskpro departments map to which Zoho Desk departments
Migration Strategy
- Big bang: Migrate everything over a weekend, cut over Monday morning. Works for < 50K tickets. Almost always preferable to avoid split-brain data issues.
- Phased: Migrate contacts and organizations first, then tickets in batches by department or date range. Reduces risk for larger datasets.
- Incremental: Run initial migration, then a delta sync during the transition period to catch new tickets created after the first pass. Usually the least disruptive for enterprise moves.
Risk Mitigation
- Take a full SQL backup of Deskpro (if on-premise) or request a complete data export from Deskpro support before beginning.
- For cloud instances, use Deskpro's Data Backup feature. Don't rely solely on API extraction as your backup.
Do not start writing migration scripts until you have a finalized field mapping document and a signed-off migration scope. Scope creep mid-migration is the #1 cause of project delays.
Data Model and Object Mapping
This is where most migrations succeed or fail. Zoho Desk requires strict parent-child relationships. If a Contact does not exist, you cannot assign a Ticket to them — the API will return a 422 Unprocessable Entity error with body {"errorCode":"INVALID_DATA","message":"contactId is invalid"}. (help.zoho.com)
Primary Object Mapping
| Deskpro Object | Zoho Desk Object | Notes |
|---|---|---|
| People (Users) | Contacts | 1:1 mapping. Email is the dedupe key in both systems. |
| Organizations | Accounts | 1:1 mapping. Name is the primary identifier. |
| Agents | Agents | Map by email. Agent must exist in Zoho Desk before ticket import. |
| Departments | Departments | Map by name. Must be pre-created in Zoho Desk. |
| Tickets | Tickets | Core object. Map status, priority, and department. |
| Ticket Messages | Threads | Each message becomes a thread entry. Distinguish public vs internal. |
| Ticket Notes | Comments | Deskpro agent notes → Zoho Desk comments (isPublic: false). |
| Attachments | Attachments | Must be downloaded from Deskpro blob storage and re-uploaded to Zoho. |
| KB Categories | KB Categories | Structural mapping. |
| KB Articles | KB Articles | HTML content transfer. Inline images need re-hosting. Article permissions/visibility must be set separately. |
| Labels/Tags | Tags | Direct mapping. Zoho Desk tags are limited to 50 characters per tag and 10 tags per ticket. |
| Custom Fields | Custom Fields | Must pre-create matching fields in Zoho Desk. Type compatibility matters. |
| Forum Topics | No equivalent | Zoho Desk's community forums have a separate data model with no import API. These cannot be migrated. |
Field-Level Mapping: Tickets
| Deskpro Field | Zoho Desk Field | Type | Transformation |
|---|---|---|---|
id |
cf_deskpro_id (custom) |
Integer → Text | Store as custom field for cross-reference; Zoho assigns its own ID |
ref |
cf_deskpro_ref (custom) |
String | Deskpro ticket reference number |
subject |
subject |
String | Truncate if exceeds 255 characters |
status |
status |
Picklist | Map: awaiting_agent → Open, awaiting_user → On Hold, resolved → Closed |
priority |
priority |
Picklist | Map: urgent → High, normal → Medium, low → Low |
department_id |
departmentId |
Lookup | Map Deskpro department ID → Zoho department ID |
agent_id |
assigneeId |
Lookup | Map Deskpro agent ID → Zoho agent ID |
person_id |
contactId |
Lookup | Map Deskpro person ID → Zoho contact ID |
person.email |
email (Contact) |
String | Dedupe key. Zoho requires lastName — if Deskpro only has a full name, split it. |
organization_id |
accountId |
Lookup | Map via contact's primary account |
date_created |
createdTime |
DateTime | ISO 8601. API only — not settable via CSV. |
date_resolved |
closedTime |
DateTime | ISO 8601 |
labels |
tag |
Array → String | Comma-separated in Zoho |
custom_fields [n] |
cf_{alias} |
Varies | Must match field type. Picklist values must exist in Zoho. |
Downloadable Field Mapping Template
Use this JSON structure as a starting template for your migration configuration:
{
"status_map": {
"awaiting_agent": "Open",
"awaiting_user": "On Hold",
"resolved": "Closed",
"closed": "Closed",
"hidden": "Closed"
},
"priority_map": {
"urgent": "High",
"high": "High",
"normal": "Medium",
"low": "Low"
},
"department_map": {
"1": "ZOHO_DEPT_ID_HERE",
"2": "ZOHO_DEPT_ID_HERE"
},
"agent_map": {
"deskpro_agent_email@example.com": "ZOHO_AGENT_ID_HERE"
},
"custom_field_map": {
"custom_field_1": {"zoho_alias": "cf_custom1", "type": "text"},
"custom_field_2": {"zoho_alias": "cf_priority_level", "type": "picklist"}
}
}Migration Order (Dependency Chain)
Zoho Desk requires referenced objects to exist before you can link them. Breaking this order creates orphaned records that are painful to fix:
- Departments — create or map first
- Agents — must be active in Zoho before ticket assignment
- Accounts (organizations) — create before contacts
- Contacts (people) — link to accounts via
accountId - Tickets — reference
contactId,departmentId,assigneeId - Threads and Comments — reference
ticketId - Attachments — reference
ticketIdorthreadId - KB Categories → KB Sections → KB Articles — maintain hierarchy
Migration Architecture: Extract → Transform → Load
Extraction from Deskpro
Deskpro provides a REST API v2 with endpoints for all primary objects:
GET /api/v2/tickets → paginated list of tickets
GET /api/v2/tickets/{id}/messages → messages for a ticket
GET /api/v2/people → contacts/users
GET /api/v2/organizations → organizations
GET /api/v2/agents → agents
GET /api/v2/blobs/{id} → attachment binary download
GET /api/v2/kb/articles → knowledge base articles
GET /api/v2/kb/categories → KB category hierarchy
Authentication: API key passed via Authorization: key {id}:{key} header. For migration, use a superuser API key to access all data regardless of agent permissions.
Pagination: Deskpro returns paginated responses with total_count, page, total_pages in the meta object. Default page size is 10; increase it with the count parameter (max 200). Known limitation: filtered queries (e.g., ?status=awaiting_agent) cap at 1,000 total results even if more exist. Extract without filters and filter locally. (support.deskpro.com)
Rate limits: Configurable per API key (hourly and daily). Cloud customers have a default global limit (typically ~500 requests/minute, though this varies by plan and can be raised by contacting Deskpro support). Handle HTTP 429 responses with exponential backoff — the response body includes a Retry-After header indicating the wait time in seconds.
Loading into Zoho Desk
Zoho Desk's REST API v1 handles all write operations:
POST /api/v1/accounts → create an account
POST /api/v1/contacts → create a contact
POST /api/v1/tickets → create a ticket
POST /api/v1/tickets/{id}/threads → add a thread to a ticket
POST /api/v1/tickets/{id}/comments → add a comment
POST /api/v1/tickets/{id}/attachments → upload attachment (multipart/form-data)
GET /api/v1/organizationFields → discover custom field API names
DELETE /api/v1/tickets/{id} → delete a ticket (for rollback)
Authentication: OAuth 2.0 with access tokens. Register your app at api-console.zoho.com. Access tokens expire every 60 minutes — your script must handle refresh automatically using the refresh token grant. Refresh tokens are valid indefinitely unless revoked.
Data-center routing is your responsibility. US orgs use desk.zoho.com, EU orgs use desk.zoho.eu, IN orgs use desk.zoho.in, AU orgs use desk.zoho.com.au, JP orgs use desk.zoho.jp. Hitting the wrong data center returns {"errorCode":"INVALID_OAUTH","message":"Invalid OAuth token"} — which looks like an auth error, not a routing error. Check your Zoho org's data center in Admin → Company Details.
API Credits: Zoho Desk uses a credit-based system. Each API call consumes 1 credit for simple GETs, 2 credits for POST/PUT/DELETE operations, and attachment uploads consume 5 credits.
| Edition | Base Credits/Day | Per Agent/Day | 10-Agent Total |
|---|---|---|---|
| Free | 5,000 | 0 | 5,000 |
| Express | 25,000 | 100 | 26,000 |
| Standard | 50,000 | 250 | 52,500 |
| Professional | 75,000 | 500 | 80,000 |
| Enterprise | 100,000 | 1,000 | 110,000 |
Credit budget example: A ticket with 4 threads, 1 comment, and 1 attachment costs approximately: 2 (ticket create) + 8 (4 thread creates × 2) + 2 (comment create) + 5 (attachment upload) = 17 credits. On a Professional plan with 10 agents (80,000 credits/day), that's ~4,700 fully-loaded tickets per day. Budget accordingly. If you exhaust credits, Zoho returns HTTP 429 with {"errorCode":"REQUEST_THROTTLED","message":"API credits exhausted"} — implement exponential backoff.
No bulk/batch API in Zoho Desk. Every record is a single API call. This makes large migrations slow and credit-intensive compared to platforms like Salesforce that offer bulk APIs.
Zoho Desk API request rate limit: Beyond the daily credit system, Zoho enforces per-minute request throttling. Paid plans allow approximately 25–30 requests per minute per org. Exceeding this triggers a 429 independent of credit availability.
Zoho Desk's paid plans (excluding Free and Trial) allow purchasing up to 5,000,000 additional credits per day. If your migration will run for several days at high volume, purchase add-on credits before you start via Setup → Developer Space → API.
Transformation Layer
Deskpro outputs ticket messages in HTML or rich text. Zoho Desk accepts rich text but the rendering engines differ — sanitize HTML payloads, particularly:
- Strip
<script>and<iframe>tags (Zoho will reject them) - Convert Deskpro's internal
<img>sources to re-hosted URLs - Normalize CSS class names (Deskpro's custom classes won't render in Zoho)
Map Deskpro agent IDs to newly created Zoho Desk agent IDs using the cross-reference table you build during loading. Use Zoho's Organization Fields API (GET /api/v1/organizationFields) to discover field API names (cf_* custom fields) from the target portal rather than hard-coding them. (support.zoho.com)
Step-by-Step Migration Process
Step 1: Extract Data from Deskpro
Pull all objects in dependency order. Store extracted data as JSON files or in a staging database. Never stream directly from Deskpro to Zoho — if the script fails, you lose your place.
import requests
import json
import time
DESKPRO_URL = "https://your-instance.deskpro.com/api/v2"
HEADERS = {"Authorization": "key 1:YOUR_API_KEY_HERE"}
def extract_all(endpoint, params=None):
results = []
page = 1
while True:
resp = requests.get(
f"{DESKPRO_URL}/{endpoint}",
headers=HEADERS,
params={**(params or {}), "page": page, "count": 200}
)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 60))
time.sleep(retry_after)
continue
resp.raise_for_status()
data = resp.json()
results.extend(data.get("data", []))
meta = data.get("meta", {}).get("pagination", {})
if page >= meta.get("total_pages", 1):
break
page += 1
time.sleep(0.2) # respect rate limits (~5 req/sec)
return results
# Extract in dependency order — no filters to avoid 1000-result cap
orgs = extract_all("organizations")
people = extract_all("people")
agents = extract_all("agents")
tickets = extract_all("tickets")
# Persist to disk for resumability
with open("extracted_tickets.json", "w") as f:
json.dump(tickets, f)For each ticket, make follow-up calls to extract messages and attachments:
for ticket in tickets:
tid = ticket["id"]
ticket["messages"] = extract_all(f"tickets/{tid}/messages")
# Download attachment blobs separately and save to disk
for msg in ticket["messages"]:
for attach in msg.get("attachments", []):
blob_resp = requests.get(
f"{DESKPRO_URL}/blobs/{attach['blob_id']}",
headers=HEADERS
)
with open(f"blobs/{attach['blob_id']}_{attach['filename']}", "wb") as f:
f.write(blob_resp.content)Step 2: Transform Data
Build ID mapping tables and translate field values:
STATUS_MAP = {
"awaiting_agent": "Open",
"awaiting_user": "On Hold",
"resolved": "Closed",
"closed": "Closed",
"hidden": "Closed",
}
PRIORITY_MAP = {
"urgent": "High",
"high": "High",
"normal": "Medium",
"low": "Low",
}
def split_name(full_name):
"""Zoho Desk requires lastName. Split Deskpro's full name field."""
parts = (full_name or "Unknown").strip().rsplit(" ", 1)
if len(parts) == 2:
return parts[0], parts[1]
return "", parts[0] # Use full string as lastName
def transform_ticket(ticket, contact_map, dept_map, agent_map):
return {
"subject": ticket["subject"][:255], # Zoho 255-char limit
"status": STATUS_MAP.get(ticket["status"], "Open"),
"priority": PRIORITY_MAP.get(ticket.get("priority"), "Medium"),
"departmentId": dept_map.get(ticket.get("department_id")),
"contactId": contact_map.get(ticket.get("person_id")),
"assigneeId": agent_map.get(ticket.get("agent_id")),
"createdTime": ticket.get("date_created"), # ISO 8601 — preserves historical date
"cf": {
"cf_deskpro_id": str(ticket["id"]),
"cf_deskpro_ref": ticket.get("ref"),
**transform_custom_fields(ticket.get("custom_fields", {})),
},
}Step 3: Load into Zoho Desk
Create records in dependency order. Build an ID cross-reference map as you go:
import shelve
ZOHO_URL = "https://desk.zoho.com/api/v1" # adjust for your data center
ZOHO_HEADERS = {
"Authorization": "Zoho-oauthtoken YOUR_ACCESS_TOKEN",
"orgId": "YOUR_ORG_ID",
"Content-Type": "application/json"
}
# Use shelve for persistent ID mapping (survives crashes)
id_map = shelve.open("migration_id_map")
def create_zoho_record(endpoint, payload, max_retries=3):
for attempt in range(max_retries):
resp = requests.post(
f"{ZOHO_URL}/{endpoint}",
headers=ZOHO_HEADERS,
json=payload
)
if resp.status_code == 429:
wait = min(300 * (2 ** attempt), 3600) # exponential backoff, max 1hr
time.sleep(wait)
continue
if resp.status_code == 401:
refresh_oauth_token() # handle token expiry
continue
if resp.status_code == 422:
# Log the exact error for debugging
error = resp.json()
logging.error(f"422 on {endpoint}: {error}")
# Common: {"errorCode":"INVALID_DATA","message":"contactId is invalid"}
return None
resp.raise_for_status()
return resp.json()
raise Exception(f"Max retries exceeded for {endpoint}")
# Create accounts first, then contacts, then tickets
for org in transformed_orgs:
deskpro_id = str(org["deskpro_id"])
if f"org_{deskpro_id}" in id_map:
continue # skip already-migrated (idempotent)
result = create_zoho_record("accounts", org["payload"])
if result:
id_map[f"org_{deskpro_id}"] = result["id"]Step 4: Migrate Threads and Attachments
For each migrated ticket, add threads (replies) and comments (internal notes):
for ticket in migrated_tickets:
zoho_ticket_id = id_map.get(f"ticket_{ticket['deskpro_id']}")
if not zoho_ticket_id:
continue
# Sort messages chronologically to preserve thread order
messages = sorted(ticket["messages"], key=lambda m: m["date_created"])
for msg in messages:
if msg.get("is_note"):
create_zoho_record(
f"tickets/{zoho_ticket_id}/comments",
{"content": msg["message_html"], "isPublic": False}
)
else:
create_zoho_record(
f"tickets/{zoho_ticket_id}/threads",
{
"content": msg["message_html"],
"direction": "in" if msg.get("person_id") else "out",
"fromEmailAddress": msg.get("email", ""),
}
)Thread-level timestamp preservation: Zoho Desk's thread creation API (POST /tickets/{id}/threads) does not accept a createdTime parameter. Threads are timestamped at the moment of API call. This means while ticket-level creation dates can be preserved, individual reply timestamps will reflect the migration date. Document this limitation for stakeholders who need exact thread chronology — the content field can include a header line like "Original reply date: 2023-06-15T14:30:00Z" as a workaround.
Step 5: Validate
Run automated checks after every batch:
- Compare record counts: source tickets vs. destination tickets
- Spot-check 5% of tickets for thread completeness (thread count match)
- Verify custom field values on a random sample of 50 tickets
- Confirm attachment download URLs are active in Zoho by issuing a HEAD request
- Verify that closed tickets remain closed and
createdTimematches source - Run a DPQL query in Deskpro and a Zoho Desk search for the same customer email — compare results side by side
Edge Cases and Failure Modes
Inline Images
Deskpro allows users to paste images directly into the ticket body. These are stored as base64 strings or internal URLs (e.g., https://your.deskpro.com/file.php/...). Zoho Desk requires you to:
- Download the image binary from Deskpro
- Upload via the
POST /api/v1/uploadsendpoint (returns a{token}) - Rewrite the
<img src="...">tag in the thread body to<img src="zoho-hosted-url">
Base64 images embedded in HTML must be extracted, saved as files, uploaded, and replaced with URLs. Failing to do this results in broken images that display as empty boxes.
Duplicate Contacts
Zoho Desk deduplicates contacts by email. If two Deskpro "People" share an email address, the second POST /api/v1/contacts call will return {"errorCode":"DUPLICATE_VALUE","message":"Email already exists"}. Pre-deduplicate in your transform layer by grouping Deskpro People by email and merging their metadata.
Deskpro's 1,000-Result Filter Limit
Filtered API queries in Deskpro (e.g., ?status=awaiting_agent) return a maximum of 1,000 results, even if more exist. The API does not indicate truncation — it simply returns 1,000 results as if that's the complete set. Extract without filters and apply filtering locally.
Multi-Organization Contacts
A Deskpro person can belong to multiple organizations. Zoho Desk Contacts typically have a single primary Account. Decide which Account becomes the primary in Zoho: options include using the most recent organization, the organization with the most tickets, or the first listed. If you enable the multiple-accounts feature in Zoho Desk (Settings → Contacts → Multiple Accounts), test carefully — imported account names can overwrite existing account context.
Missing Picklist Values
If a Deskpro custom field has a value that doesn't exist in the corresponding Zoho Desk picklist, the API returns {"errorCode":"INVALID_DATA","message":"cf_fieldname - Invalid picklist value"}. Pre-populate all picklist values in Zoho before migration by calling GET /api/v1/organizationFields to discover existing values, then adding missing ones via the Zoho Desk UI (picklist values cannot be added via API).
Orphan Tickets
Zoho Desk requires every ticket to have an associated contact. Old Deskpro tickets tied only to an email string (no matching People record) need either:
- A placeholder contact created with that email address and a
lastNameof "Unknown" - A pre-merge pass that matches email strings to existing People records before loading
HTML Content Sanitization
Deskpro stores ticket messages as HTML. Zoho Desk threads also accept HTML, but the rendering engines differ. Test a sample of 20–30 HTML-heavy tickets to catch formatting breaks — especially tables (Zoho strips some colspan/rowspan attributes), inline images (see above), and embedded iframes (Zoho strips these entirely for security).
Timestamp Preservation
When you create a ticket via the Zoho API, the createdTime defaults to the current timestamp. To preserve historical dates, explicitly pass the createdTime field in your JSON payload in ISO 8601 format (e.g., "2022-03-15T09:30:00.000Z"). This is only possible via API — CSV imports cannot set creation dates.
Important caveat: Thread-level timestamps cannot be set via API. Only ticket-level createdTime is preservable.
Zoho's native CSV import uses GMT, not the importing user's timezone. If you are validating timestamps or SLA-related dates after a CSV import, compare them after timezone conversion rather than by eye in the UI. (help.zoho.com)
Knowledge Base Article Migration
KB articles require special handling beyond ticket migration:
- Hierarchy: Deskpro KB categories → Zoho Desk KB categories → sections → articles. Map Deskpro's two-level hierarchy into Zoho's three-level structure.
- Inline images in articles: Same re-hosting process as ticket inline images (download, upload via
/uploads, rewritesrcattributes). - Article permissions: Deskpro's user group-based KB visibility doesn't map directly to Zoho Desk's visibility model (All Users, Registered Users, Agents Only). Map each article's visibility explicitly.
- API endpoints:
POST /api/v1/kbCategories,POST /api/v1/kbCategories/{id}/sections,POST /api/v1/kbSections/{id}/articles - Downloads and News: Deskpro's KB downloads and news posts have no equivalent in Zoho Desk. Archive or migrate to a file-sharing platform.
Limitations and Constraints
- Custom modules are Enterprise-only. Zoho Desk supports up to 10 custom modules with 5 custom lookup fields each, but only on the Enterprise plan. Custom modules cannot be deleted or have their storage mode changed after creation. Treat them as a deliberate schema choice. (help.zoho.com)
- Attachment size limits. Zoho Desk's attachment API limits vary by plan: Standard allows 15 MB per file, Professional and Enterprise allow 20 MB per file. Files exceeding this must be hosted externally (e.g., Zoho WorkDrive, AWS S3) and linked in the ticket body.
- Forum topics from Deskpro cannot be migrated. Zoho Desk's community forums have a separate data model with no import API.
- Zoho Desk Free and Express plans lack UI-based import. You must be on Professional or Enterprise to use the native import tool. API access is available on all plans but credit-constrained on lower tiers.
- Native CSV import only partially reverts. Zoho's import only reverts created records for a short window (approximately 30 minutes after import completion); updates are not reverted. Plan your rollback strategy accordingly.
- Thread timestamps cannot be preserved. The thread creation API does not accept historical
createdTime. Thread ordering is maintained by insertion order, but all threads will carry migration-date timestamps. - Bulk delete for rollback is limited. Zoho Desk does not offer a bulk delete API. To roll back, you must delete records one-by-one via
DELETE /api/v1/tickets/{id}at 2 credits per call. For 50K tickets, that's 100K credits — more than one day's allocation on most plans. Plan rollback credit budget separately, or use Zoho's "Move to Trash" feature in the UI (which supports multi-select but is limited to 50 records per batch in the interface).
Validation and Testing
Never execute a production migration without a test run.
Record Count Verification
| Object | Deskpro Count | Zoho Desk Count | Delta | Action |
|---|---|---|---|---|
| Organizations/Accounts | X | Y | X - Y | Investigate mismatches |
| People/Contacts | X | Y | X - Y | Check deduplication |
| Tickets | X | Y | X - Y | Check for API failures |
| Threads per ticket | Sample 50 | Sample 50 | — | Spot-check count match |
| Attachments | X | Y | X - Y | Re-upload failures |
Sampling Strategy
- Pull 50 random tickets across different departments and date ranges
- Compare every field value between source and target
- Verify thread ordering (chronological)
- Open attachments in Zoho to confirm they render correctly
- Verify inline images display properly in thread bodies
UAT Process
- Give 2–3 agents access to the new Zoho Desk instance with migrated data
- Have them search for known customers and verify history
- Have them attempt to reply to a migrated ticket
- Have them verify that custom field values display correctly
- Collect feedback over 48 hours before full cutover
Rollback Planning
- Do not decommission Deskpro until Zoho Desk has been validated in production for at least one week
- Keep Deskpro in read-only mode during the validation window
- If rollback is needed, the old system is still intact — no data was deleted
- If the production cutover fails, delete injected records from Zoho Desk via API (
DELETE /api/v1/tickets/{id},DELETE /api/v1/contacts/{id}). Budget 2 credits per delete — for 50K tickets + 20K contacts, that's 140K credits over ~2 days - Alternatively, Zoho support can perform a full organization data wipe for a clean restart, but this requires a support ticket and takes 24–48 hours
Post-Migration Tasks
Once the data is verified in Zoho Desk, the operational work begins.
- Rebuild automations. Deskpro triggers and escalations do not migrate. Recreate them as Zoho Desk workflows (Setup → Automation → Workflows), assignment rules, SLA policies, and Blueprints. Map each Deskpro trigger to its Zoho equivalent before cutover.
- Reconfigure SLAs. Zoho Desk's SLA engine works differently from Deskpro's. Deskpro calculates SLA based on ticket properties at creation; Zoho Desk recalculates on ticket updates. Map response and resolution targets manually and test with sample tickets.
- Set up integrations. If Deskpro was connected to Slack, Jira, or CRM tools via API, reconfigure those integrations with Zoho Desk's connectors or Zoho Flow. Key integrations: Zoho CRM sync (bidirectional), Slack notifications (via Zoho Desk Marketplace app), Jira (via Zoho Desk's built-in Jira integration).
- Train agents. Zoho Desk's UI and terminology differ from Deskpro. Plan a 1–2 hour hands-on session covering ticket views, macros, the knowledge base editor, and Zoho-specific concepts like Blueprints and Happiness Ratings.
- Update email routing. Configure Zoho Desk's email channel to receive support emails. Update your support@ MX records or forwarding rules. Set the reply-from address to match your existing support email to avoid confusing customers.
- Monitor for data gaps. Keep the Deskpro instance accessible in read-only mode for at least 30 days. Run daily reports for the first two weeks comparing ticket volumes, response times, and CSAT scores to pre-migration baselines.
For a deeper post-migration quality checklist, see our Post-Migration QA guide.
Best Practices
- Back up everything before you start. Export a full Deskpro database backup (or use their Data Backup feature for cloud instances). Don't rely solely on API extraction as your backup.
- Extract everything first. Never read from Deskpro and write to Zoho in the same function. Decouple extraction from loading. This prevents partial writes if either API goes down.
- Map agents early. Agent mapping is the most common point of failure. Ensure all Deskpro agents exist in Zoho Desk (with matching emails) before migrating tickets. Invite agents and have them accept before the migration run.
- Run a test migration first. Validate the entire pipeline end-to-end with 100 tickets (including tickets with attachments, custom fields, and multiple threads) before committing to a full run. This catches field mapping errors, picklist mismatches, and authentication issues early.
- Use incremental validation. Validate after each batch (accounts, contacts, tickets, threads) rather than waiting until the end.
- Log every API call and response. When (not if) something fails, your logs are the only way to identify which records need re-processing without re-running the entire migration. Include the Deskpro source ID, Zoho response code, and response body in every log entry.
- Budget API credits conservatively. Calculate your expected credit consumption before starting. Use the formula:
(tickets × 17) + (contacts × 2) + (accounts × 2) + (KB_articles × 2)as a rough upper bound. If your Zoho plan's daily credits won't cover the migration, purchase add-on credits in advance. - Build idempotent processes. Use the persistent ID cross-reference map to skip already-migrated records on retry. Make retries object-aware so a single bad attachment does not poison an entire batch.
- Schedule migrations during off-peak hours. Zoho's API performance can vary during business hours (US Pacific time). Running migrations during evening/weekend hours in your Zoho data center's timezone reduces timeout risk.
Production Script Structure
Here's the high-level architecture for a production migration script:
import logging
import shelve
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
class DeskproExtractor:
"""Handles paginated extraction from Deskpro API v2."""
def __init__(self, base_url, api_key):
self.base_url = base_url
self.headers = {"Authorization": f"key {api_key}"}
def extract(self, endpoint, params=None) -> list:
"""Paginate through all results. Extract unfiltered to avoid 1000-result cap.
Uses count=200 (max page size). Handles 429 with Retry-After header."""
...
def extract_ticket_messages(self, ticket_id) -> list:
return self.extract(f"tickets/{ticket_id}/messages")
def download_blob(self, blob_id) -> bytes:
"""Download attachment binary from /api/v2/blobs/{id}."""
...
class ZohoDeskLoader:
"""Handles OAuth2 auth, credit tracking, and writes to Zoho Desk API v1."""
def __init__(self, client_id, client_secret, refresh_token, org_id, dc="com"):
self.base_url = f"https://desk.zoho.{dc}/api/v1"
self.org_id = org_id
self.credits_used = 0
self.daily_credit_limit = 80000 # set based on your plan
def create(self, endpoint, payload) -> dict:
"""POST with retry on 429, token refresh on 401, error logging on 422."""
self.credits_used += 2
if self.credits_used >= self.daily_credit_limit * 0.95:
logging.warning(f"Approaching daily credit limit: {self.credits_used}")
...
def upload_attachment(self, ticket_id, file_bytes, filename) -> dict:
"""Multipart upload. Costs 5 credits. Max 20MB on Enterprise."""
self.credits_used += 5
...
class MigrationOrchestrator:
"""Coordinates extract -> transform -> load with persistent ID mapping."""
def __init__(self, extractor, loader):
self.extractor = extractor
self.loader = loader
self.id_map = shelve.open("migration_id_map") # persistent, crash-safe
def run(self):
self.migrate_departments()
self.migrate_accounts()
self.migrate_contacts()
self.migrate_tickets()
self.migrate_threads_and_comments()
self.migrate_attachments()
self.migrate_kb()
self.validate()
def validate(self):
"""Compare source and target counts. Log discrepancies.
Sample 5% of tickets for thread count match."""
...
def __del__(self):
self.id_map.close()
if __name__ == "__main__":
extractor = DeskproExtractor("https://your.deskpro.com/api/v2", "1:YOURKEY")
loader = ZohoDeskLoader("client_id", "secret", "refresh_token", "org_id")
orchestrator = MigrationOrchestrator(extractor, loader)
orchestrator.run()Store your ID cross-reference map (Deskpro ID → Zoho ID) in a persistent store like Python's shelve, SQLite, or Redis — not just in memory. If the script crashes mid-run, you can resume from the last successful record without duplicating data. The map also serves as a permanent audit trail for post-migration troubleshooting.
Making the Call
Deskpro-to-Zoho Desk is a migration with no native shortcut. The absence of a built-in path means you're choosing between CSV (lossy — no threads, no attachments, no historical timestamps), API scripting (2–4 weeks of development for full fidelity), third-party tools (limited customization, per-record pricing), or a managed service.
Key decision factors:
- < 5K tickets, no attachment requirements: CSV or third-party tool. Total effort: 1–3 days.
- 5K–50K tickets with attachments and custom fields: API pipeline or managed service. API pipeline effort: 2–4 weeks development + 3–7 days execution. Managed service: typically 3–5 business days end-to-end.
- 50K+ tickets, multi-department, SLA history required: API pipeline with delta sync or managed service. DIY effort: 4–8 weeks including edge case handling and validation.
The safest approach treats Zoho Desk as a different operating model, not just a different UI. Native CSV is fine for small, simple moves. Once history, attachments, departments, or account/contact relationships matter, move to API or managed migration.
If you want the migration handled end-to-end with full thread, attachment, and custom field fidelity, that's what we do.
Frequently Asked Questions
- Can I migrate from Deskpro to Zoho Desk using a built-in tool?
- No. Zoho Desk's native migration utility (Zwitch) only supports Freshdesk and Zendesk. Deskpro-to-Zoho migrations require CSV export/import, API scripting, a third-party tool, or a managed migration service.
- Does Zoho Desk CSV import preserve ticket threads and attachments?
- No. Zoho Desk's CSV import creates flat ticket records without threads, comments, or attachments. To preserve full conversation history and file attachments, you must use the Zoho Desk REST API.
- What are Zoho Desk's API rate limits for migration?
- Zoho Desk uses a credit-based API system. Credits range from 5,000/day (Free) to 100,000/day + 1,000 per agent (Enterprise). Exceeding credits returns HTTP 429. Paid plans allow purchasing up to 5,000,000 additional credits.
- Does Zoho Desk require a contact for every ticket?
- Yes. Zoho Desk does not allow creating a ticket without associating it to a contact. Orphan Deskpro tickets tied only to an email string need a placeholder contact or a pre-merge pass before loading.
- What data cannot be migrated from Deskpro to Zoho Desk?
- Triggers, escalations, SLA rules, automations, and macros must be recreated manually in Zoho Desk. Forum/community data and Deskpro-specific features like downloads and news posts have no equivalent in Zoho Desk.
