Zendesk to Deskpro Migration: The Complete Technical Guide
Complete technical guide for migrating from Zendesk to Deskpro — covering data model mapping, API constraints, migration approaches, and validation strategies.
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
Zendesk to Deskpro Migration: The Complete Technical Guide
TL;DR — Zendesk to Deskpro Migration
A Zendesk to Deskpro migration translates Zendesk's group-centric architecture into Deskpro's department-centric model. Organizations, Users, Tickets, Comments, Tags, and Help Center Articles all have Deskpro equivalents — but structural differences in custom fields, ticket statuses, and automations mean this is not a lift-and-shift. Deskpro offers a built-in Zendesk importer that handles basic migrations, but it won't reliably transfer custom fields, full conversation threads with inline images, or attachments at scale. For production-grade migrations, you need a Zendesk REST API → Deskpro REST API pipeline. Realistic timeline: 1–2 weeks for under 20K tickets; 3–5 weeks for enterprise datasets with complex custom field schemas. The biggest risk is orphaned ticket threads — Zendesk's CSV exports flatten conversations, losing reply structure and inline attachments.
Technical details in this guide were verified against Zendesk API v2 and Deskpro API v2 as of June 2025. Pricing, rate limits, and API behavior are subject to change.
What Is a Zendesk to Deskpro Migration?
A Zendesk to Deskpro migration extracts your historical support data — Tickets, Users, Organizations, Comments, Attachments, and optionally Help Center Articles — and reconstructs it within Deskpro's relational database.
Zendesk is a cloud-based customer service platform built around a group-centric architecture. Tickets are assigned to Groups (which map to agent teams), and automation rules operate on ticket metadata. Zendesk Suite plans run from $55 to roughly $169 per agent/month on annual billing, with AI add-ons adding per-agent or per-resolution costs on top. (zendesk.com)
Deskpro is a helpdesk platform available as both cloud-hosted and on-premise (self-hosted on your own Linux server with a MySQL/MariaDB or PostgreSQL database). Its data model is department-centric — every ticket belongs to a Department, and tickets require associations with Agents and People (customers). Pricing starts at $29/agent/month (Team) and goes up to $99/agent/month (Enterprise). (deskpro.com)
The on-premise option gives organizations full control over their data and infrastructure, which is a common driver for this migration — particularly in regulated industries where data must remain within specific jurisdictions.
Engineering teams typically drive this transition to regain control over data residency, escape Zendesk's escalating pricing tiers, or consolidate multiple brands into Deskpro's multi-department architecture. But moving between these systems is not a 1:1 mapping exercise. If you attempt a flat CSV import, you will orphan ticket threads, break SLA reporting, and lose inline images.
For teams evaluating multiple target platforms, our Zendesk migration checklist provides a platform-agnostic framework.
Why Teams Migrate from Zendesk to Deskpro
The triggers we see most often:
- Cost reduction. A 20-agent team on Zendesk Suite Professional ($115/agent/month) pays $27,600/year before add-ons. The same team on Deskpro Professional ($59/agent/month) pays $14,160/year — a 49% reduction. Factor in Zendesk's Copilot ($50/agent/month), QA, and Workforce Management add-ons and the gap widens further.
- Data sovereignty and on-premise deployment. Deskpro is one of the few modern helpdesks that offers a genuine self-hosted option. Regulated industries (healthcare under HIPAA, financial services under SOX/PCI-DSS, government under FedRAMP or national data residency laws) often need data on their own infrastructure to meet compliance requirements.
- Pricing predictability. Zendesk's per-resolution AI billing (Zendesk AI agents charge per automated resolution above a threshold) creates variable costs that are hard to forecast. Deskpro's flat per-agent pricing has no usage-based AI surcharges.
- Simpler administration. Teams that don't need Zendesk's full omnichannel stack (messaging, voice, social) find Deskpro's focused feature set easier to manage and configure without dedicated Zendesk administrators.
- Contract flexibility. Zendesk's customer agreement includes early-termination fees and annual upfront billing. Deskpro offers both monthly and annual billing without lock-in penalties on Cloud plans.
If your real driver is CRM or pipeline management rather than support, split the project. Zendesk Sell exposes leads, contacts, and deals through a separate API (/api/v2/contacts, /api/v2/deals under the Sell base URL, which uses OAuth 2.0 rather than the Support API's token-based auth). Deskpro focuses exclusively on support users and organizations. Support data moves to Deskpro; sales data stays in a CRM built for that job.
Core Data Model Differences
Before touching any migration code, understand the structural translation:
| Concept | Zendesk | Deskpro | Notes |
|---|---|---|---|
| Customer grouping | Organizations | Organizations | Near 1:1; Deskpro supports custom fields on Organizations |
| End users | Users (end-users) | People | Deskpro People can belong to multiple Organizations |
| Agent staff | Agents / Light Agents / Admins | Agents (with permission groups) | Map by email; see agent role mapping below |
| Ticket routing | Groups | Departments | Zendesk Groups are agent pools; Deskpro Departments are structural containers with their own forms and email channels |
| Agent teams | No formal concept beyond Groups | Teams | Deskpro has a separate Teams layer for cross-department grouping |
| Ticket replies | Comments (public/private) | Messages + Notes | Private comments → Deskpro Agent Notes |
| Categorization | Tags | Labels | Functional equivalent; both support multiple values per ticket |
| Custom metadata | Ticket Fields / User Fields / Org Fields | Custom Fields (Ticket / People / Organization) | Deskpro supports dependencies between fields |
| Automations | Triggers + Automations | Triggers + Escalations | Must be rebuilt manually — cannot be exported from Zendesk in a Deskpro-compatible format |
| Self-service | Help Center (Guide) | Knowledge Base | Different content structure; see KB migration section |
| Ticket status | Open / Pending / On-hold / Solved / Closed | Awaiting Agent / Awaiting User / Resolved / Archived + custom statuses | Deskpro allows custom ticket statuses |
| Custom objects | Custom Objects (API) | No equivalent | Must be flattened into custom fields |
| SLA policies | SLA Policies | SLAs | Different configuration model; rebuild required |
| Satisfaction | CSAT (Satisfaction Ratings) | Feedback | See CSAT section below |
Agent Role Mapping
Zendesk has four agent-level roles: Full Agent, Light Agent (read-only with limited comment ability, available on Professional+), Admin, and custom roles (Enterprise only). Deskpro uses Agent Permission Groups to control access. Map as follows:
| Zendesk Role | Deskpro Equivalent | Notes |
|---|---|---|
| Full Agent | Agent (standard permission group) | Full ticket access |
| Light Agent | Agent (restricted permission group) | Create a permission group limiting write access |
| Admin | Agent + Admin access enabled | Enable admin toggle on agent profile |
| Custom Roles | Custom permission groups | Recreate each role as a Deskpro permission group |
Zendesk Custom Objects have no Deskpro equivalent. If you use Zendesk Custom Objects to model business entities (orders, subscriptions, assets), you must flatten them into custom fields on Tickets, People, or Organizations. Multi-level relational structures will require schema compromises. You lose the relational query capability and the ability to query across object relationships, but you preserve the data values. For complex object graphs, consider exporting the relational data to a separate database and linking to it via a custom field containing a lookup ID.
CSAT / Satisfaction Data
Zendesk Satisfaction Ratings capture a binary Good/Bad rating plus an optional text comment, tied to specific tickets. Deskpro's Feedback feature captures star ratings per ticket. The mapping is lossy:
- Zendesk
Good→ Deskpro 5-star (or a positive feedback flag) - Zendesk
Bad→ Deskpro 1-star (or a negative feedback flag) - Zendesk satisfaction reason codes have no Deskpro equivalent
- Zendesk's CSAT survey timing and trigger logic must be rebuilt in Deskpro
For historical CSAT data, the practical options are: (1) store the Zendesk rating and comment as a custom field on the migrated ticket, (2) import as an agent note appended to the ticket, or (3) export CSAT data to a BI tool and accept that Deskpro will only capture CSAT going forward. Most teams choose option 1 or 3.
Migration Approaches
There is no native, one-click migration path from Zendesk to Deskpro. You must choose a strategy based on your data volume, schema complexity, and engineering bandwidth.
1. Deskpro's Built-in Zendesk Importer
Deskpro ships with a native Zendesk import tool accessible under Admin > Apps > Importer. You provide your Zendesk admin API token, and the importer pulls data directly from Zendesk's API.
When to use it: Small to mid-size datasets (under ~20K tickets) with no complex custom field schemas.
Pros: Zero code required. Built into the product — no third-party tools. Handles core objects (tickets, users, organizations).
Cons: Limited control over field mapping. Doesn't handle custom fields, attachments, or full conversation threads reliably at scale. No delta/incremental migration support. No pre-migration dry-run validation. No granular error reporting when individual records fail.
Complexity: Low | Scalability: Small datasets only
2. API-Based Migration (Zendesk REST API → Deskpro REST API)
Engineers write scripts to extract data from Zendesk's Incremental API and push it to Deskpro's REST API v2, respecting dependency order (Organizations → People → Agents → Tickets → Messages → Attachments).
When to use it: Any migration with custom fields, full thread history, attachments, or more than ~20K tickets. This is the recommended approach for production migrations.
Pros: Full control over field mapping and data transformation. Handles custom fields, attachments, inline images. Supports incremental/delta migration. Can preserve original timestamps via API parameters. Allows idempotent retry logic for failed records.
Cons: Requires engineering effort (Python/Node.js scripting, typically 80–160 hours for mid-complexity migrations). Must handle API rate limits on both sides. Error handling, retry logic, and idempotency are your responsibility.
Complexity: High | Scalability: Enterprise-grade
3. CSV Export/Import
Export Zendesk data to CSV and upload via Deskpro's native importer.
When to use it: Never for ticket migration unless you only need basic user and organization records with no conversation history. (support.zendesk.com)
Pros: Fast, requires zero code.
Cons: Zendesk CSV exports omit ticket comments and descriptions entirely. Deskpro's CSV importer only imports ticket properties and the first message — no threaded conversations or attachments. Zendesk's native export must be enabled by contacting Zendesk support and is not available on Team plans. Professional plans can export tickets but with limitations. Enterprise plans support full JSON exports. Zendesk recommends JSON for datasets above 200,000 tickets.
Complexity: Low | Scalability: Small datasets only (< 5K tickets)
4. Third-Party Migration Tools
Tools like Help Desk Migration (HDM) support Zendesk-to-Deskpro transfers via a wizard-based interface. Deskpro has officially partnered with Help Desk Migration for migrations from platforms without a built-in importer.
When to use it: Teams without engineering resources that need more control than the built-in importer provides, including custom field mapping and delta migration.
Pros: No code; wizard-based interface. Supports custom field mapping. Delta migration option available. Free demo migration on a subset of records.
Cons: Per-record pricing adds up for large datasets (typically $1–$5 per 100 records depending on object type; a 100K ticket migration can cost $1,000–$5,000+). Limited control over edge cases (duplicate handling, attachment re-linking). You're dependent on the tool's mapping logic for accuracy.
Complexity: Low–Medium | Scalability: Medium (works for mid-size, gets expensive at scale)
5. Middleware Platforms (Zapier, Make)
Setting up point-to-point triggers between the two systems using event-driven workflows.
When to use it: Only for ongoing, real-time syncs after a historical migration, or for keeping new tickets flowing to Deskpro during a phased cutover. Not for bulk historical migration. (help.zapier.com)
Pros: No-code setup, fast to deploy.
Cons: Cannot process historical data in bulk. Will hit API rate limits on large datasets. No thread/conversation support. Per-task pricing gets expensive at volume (Zapier charges per task; syncing a single ticket with 5 comments = 6+ tasks).
Complexity: Medium | Scalability: Not viable for historical data
Approach Comparison
| Criteria | Built-in Importer | API Pipeline | CSV Export/Import | Third-Party Tool | Middleware |
|---|---|---|---|---|---|
| Engineering effort | None | High (80–160 hrs) | Low | Low | Medium |
| Custom field support | Limited | Full | No | Partial | No |
| Full thread history | Partial | Yes | No | Yes | No |
| Attachments | Partial | Yes | No | Yes | No |
| Delta/incremental | No | Yes | No | Yes | No |
| Cost | Free | Dev time | Free | Per-record ($1K–$5K+) | Per-task |
| Best for | < 20K simple tickets | Enterprise | Quick snapshots | Mid-market no-code | Phased sync |
Recommendations by scenario:
| Scenario | Recommended path |
|---|---|
| Small business, < 10K tickets, no custom fields | Built-in importer or third-party tool |
| Agents need old threads and attachments | API-based migration |
| Enterprise or regulated environment | API pipeline or managed service |
| Ongoing sync during phased cutover | API pipeline for history + middleware for live ticket flow |
| No engineering team, complex data | Managed migration service or third-party tool with support |
When to Use a Managed Migration Service
DIY migrations fail most often on the edges — not the core objects. It's the inline image referencing a Zendesk CDN URL that will 404 in six months when your Zendesk subscription expires. It's the private comment that gets imported as a public reply because the public flag wasn't checked. It's the 47 custom fields that need type conversion and picklist remapping across two systems that use different internal IDs.
Building an in-house ETL pipeline for a helpdesk migration looks like a weekend project — pull JSON from Zendesk, push JSON to Deskpro. In practice, teams spend weeks handling Zendesk's undocumented API behaviors (the Incremental Export API returns deleted tickets mixed with active ones; ticket events include system-generated comments that shouldn't be imported), parsing HTML entities in legacy tickets, and re-associating orphaned attachments.
Common failure modes from production migrations:
-
Orphaned attachments (most frequent). Inline images in HTML comment bodies reference Zendesk CDN URLs with signed tokens. After migration, these URLs return 403 or 404. Every image in every historical ticket breaks. Fix requires re-parsing all HTML bodies and re-uploading images — effectively re-running the migration.
-
Public/private comment inversion. A bug in the mapping logic imports private agent notes as public customer-visible messages. Agents discover this days later when customers reference internal notes. Rollback and re-import required.
-
Timestamp loss. Failing to explicitly set
date_createdon Deskpro tickets causes all migrated tickets to show today's date. SLA reporting, aging reports, and "oldest unresolved" views become meaningless. -
Duplicate people records. Zendesk allows multiple user records with the same email (primary + secondary identities). Deskpro rejects duplicate emails. Without de-duplication logic, the migration halts partway through user import, and subsequent ticket imports fail because requester IDs don't resolve.
-
Trigger-fired notifications. Importing 50,000 tickets into Deskpro without disabling triggers sends 50,000 "new ticket" notification emails to customers. This has happened in real migrations.
Consider a managed service when:
- Your dataset exceeds 50K tickets with full thread history
- You have complex custom field schemas with dependencies or conditional logic
- You need zero-downtime cutover (agents can't stop working during migration)
- You lack dedicated engineering bandwidth for a 2–4 week project
- You have regulatory requirements (HIPAA, GDPR) that demand documented audit trails
- You have multi-brand or multi-instance Zendesk configurations
Realistic costs of DIY migration:
- Engineering time: 80–160 hours for a mid-complexity migration (20K–100K tickets, 10–30 custom fields)
- Opportunity cost: engineers pulled off product work for 2–4 weeks
- Risk: a single missed relationship (ticket-to-org) can break reporting permanently and require re-import
- Rework: first migration attempt rarely succeeds — plan for 2–3 full test runs minimum
Pre-Migration Planning
Data Audit
Before writing any migration code, inventory what you have and what you actually need:
- Tickets: Total count (use
GET /api/v2/tickets/count.jsonfor an exact baseline), date range, status distribution. Break down by status: how many are solved/closed vs. open/pending? - Users: End-users, agents, admins, light agents — identify inactive/suspended accounts. Use
GET /api/v2/users/count.json?role=end-userand similar for role-specific counts. - Organizations: Count, custom fields, membership rules, shared organization settings
- Custom ticket fields: List all from
GET /api/v2/ticket_fields.json— record field key, type, picklist values, required/optional status, and any dependencies - Custom user/org fields: Same via
GET /api/v2/user_fields.jsonandGET /api/v2/organization_fields.json - Tags: Deduplicate — Zendesk often accumulates thousands of tags over years. Use
GET /api/v2/tags.jsonto enumerate; decide which to migrate as Deskpro labels - Macros: Identify which are actively used vs. stale.
GET /api/v2/macros.jsonincludesusage_30din the response - Triggers and Automations: Document logic — these must be rebuilt manually in Deskpro. Export definitions via
GET /api/v2/triggers.jsonandGET /api/v2/automations.jsonfor reference - SLA Policies: Document targets and conditions via
GET /api/v2/slas/policies.json - Help Center Articles: Count articles, categories, sections; check for embedded media.
GET /api/v2/help_center/articles.jsonwith pagination - Attachments: Estimate total volume (GB) — this determines migration time. A rough estimate: average 2–3 attachments per ticket at 500KB each
- Custom Objects: Identify all schemas via
GET /api/v2/custom_objects.json; plan flattening strategy for each - Side Conversations: Count via
GET /api/v2/tickets/{id}/side_conversations; decide handling strategy - Webhooks: List all via
GET /api/v2/webhooks.json; document endpoint URLs and event subscriptions
Define Migration Scope
Not everything needs to move. Common exclusions:
- Tickets older than 2–3 years (archive to CSV/JSON instead)
- Suspended/spam tickets
- Deleted users
- Unused custom fields and tags
- Test/sandbox data
- Zendesk system-generated ticket events (field change logs, assignment logs) — these are not comments and don't belong in Deskpro messages
For detailed guidance on what to keep versus archive, see our Help Desk Data Migration Playbook.
Migration Strategy
| Strategy | When to Use | Risk Level | Downtime |
|---|---|---|---|
| Big Bang | Small dataset (< 20K tickets), short downtime acceptable | Medium — all-or-nothing | 2–8 hours |
| Phased | Large dataset, multiple departments | Low — migrate dept by dept | Minutes per department |
| Incremental | Zero downtime required, ongoing sync | Medium — requires delta logic | Zero |
For most Zendesk-to-Deskpro migrations, we recommend a phased approach: migrate historical data first (during off-hours), validate, then cut over live ticket flow department by department. Run a delta sync right before DNS cutover to catch tickets created or modified during the migration window.
Delta sync implementation: Use Zendesk's Incremental Export API with the start_time parameter set to the timestamp of your bulk extraction. This returns only tickets created or modified since that point. Your loader must handle both creates (new tickets) and updates (modified tickets already in Deskpro) — use the ID mapping table to determine which.
Risk Documentation
Document exactly what will break and the mitigation for each:
| Risk | Impact | Mitigation |
|---|---|---|
| Triggers/Automations don't migrate | No automated routing, escalation, or notifications | Rebuild in Deskpro before cutover; test each |
| SLAs must be rebuilt | SLA breach calculations reset | Configure SLAs in Deskpro; accept historical SLA data is lost |
| Side conversations lost | Agent context gaps | Convert to agent notes or document as known loss |
| Zendesk apps/integrations break | Workflows depending on Zendesk apps stop | Identify Deskpro equivalents or custom app alternatives |
| Inline images break | Historical tickets show broken images | Re-upload images during migration; rewrite HTML src attributes |
| Webhooks stop firing | Downstream systems lose ticket events | Re-point webhooks to Deskpro's webhook system |
Data Mapping: Zendesk Fields → Deskpro Fields
Core Object Mapping
| Zendesk Object | Zendesk Field | Deskpro Object | Deskpro Field | Transformation Notes |
|---|---|---|---|---|
| Organization | id |
Organization | id (new) |
Store Zendesk ID in a custom field for cross-reference |
| Organization | name |
Organization | name |
Direct map |
| Organization | domain_names |
Organization | email_domains |
Array → comma-separated |
| Organization | tags |
Organization | Labels | Create labels if they don't exist |
| User | id |
Person | id (new) |
Map via email address as primary key |
| User | name |
Person | name |
Direct map |
| User | email |
Person | primary_email |
Deskpro uses email as unique key; duplicates rejected |
| User | organization_id |
Person | organization |
Resolve via ID mapping table |
| User | role (end-user) |
Person | User | End-users become People |
| User | role (agent/admin) |
Agent | Agent | Create via agent endpoints |
| Ticket | id |
Ticket | id (new) |
Store Zendesk ID as custom field; never reuse Zendesk IDs |
| Ticket | subject |
Ticket | subject |
Direct map |
| Ticket | status |
Ticket | status |
See status mapping below |
| Ticket | priority |
Ticket | priority |
See priority mapping below |
| Ticket | group_id |
Ticket | department |
Map Zendesk Groups → Deskpro Departments |
| Ticket | assignee_id |
Ticket | agent |
Resolve via agent ID mapping |
| Ticket | requester_id |
Ticket | person |
Resolve via person ID mapping; null requesters will be rejected |
| Ticket | tags |
Ticket | labels |
Create labels if needed |
| Ticket | created_at |
Ticket | date_created |
ISO 8601; must be set explicitly or defaults to now |
| Comment | html_body |
Message | message |
Prefer html_body over body for formatting preservation |
| Comment | public (true) |
Message | Message (reply) | Public comments → ticket replies |
| Comment | public (false) |
Note | Agent Note | Private comments → internal notes |
| Comment | attachments |
Attachment | Attachment | Download from Zendesk CDN, re-upload to Deskpro |
Ticket Status Mapping
| Zendesk Status | Deskpro Status | Notes |
|---|---|---|
new |
awaiting_agent |
Both indicate agent action needed |
open |
awaiting_agent |
|
pending |
awaiting_user |
Waiting on customer reply |
on-hold |
awaiting_agent (or custom status) |
Deskpro has no native on-hold; create a custom status named "On Hold" if your team uses this distinction |
solved |
resolved |
|
closed |
archived |
Closed tickets in Zendesk are immutable; archived in Deskpro is the closest equivalent |
Priority Mapping
| Zendesk Priority | Deskpro Priority | Deskpro Numeric Value |
|---|---|---|
low |
Low | 1 |
normal |
Normal | 5 |
high |
High | 7 |
urgent |
Urgent | 10 |
null (unset) |
Normal | 5 |
Custom Field Type Mapping
| Zendesk Field Type | Deskpro Field Type | Transformation Notes |
|---|---|---|
text |
Text (single-line) | Deskpro single-line text has a character limit; long values → multi-line |
textarea |
Textarea (multi-line) | Direct map |
checkbox |
Toggle | Boolean mapping |
integer |
Number | Direct map |
decimal |
Number | Deskpro numbers support decimals |
date |
Date | ISO 8601 format |
tagger (drop-down) |
Choice | Zendesk generates tags from selections; Deskpro uses choice IDs. Remap every option ID. |
multiselect |
Multi-choice | Same tag-to-ID remapping required |
regexp |
Text | Deskpro doesn't support regex validation on custom fields; validate in your migration script |
lookup (relationship) |
Text or Choice | Zendesk lookup fields reference other objects; flatten to text values |
Zendesk drop-down and multi-select fields generate tags as a side effect, while Deskpro uses choice-style fields with internal IDs. Picklist option IDs will never match between systems — you must re-map every one. Record all Deskpro field IDs and choice IDs in the mapping spec before the first pilot run. (support.zendesk.com)
Deskpro default field values are not applied when tickets are created via API — your loader must set every value explicitly.
Migration Architecture
A resilient migration architecture follows an Extract, Transform, Load (ETL) pattern with local staging to prevent data loss during API timeouts.
Dependency Order
Deskpro enforces referential integrity. You must load objects in this order:
- Organizations — no dependencies
- People (end-users) — depends on Organizations
- Agents — independent, but needed before ticket assignment
- Departments — must exist before tickets
- Custom Fields — must exist before tickets with custom data
- Tickets — depends on People, Agents, Departments
- Messages/Notes — depends on Tickets (load in chronological order per ticket)
- Attachments — depends on Messages
- Labels — can be created during ticket import
- Knowledge Base — independent, can be migrated in parallel (see KB section)
A Ticket in Deskpro must belong to a Person and a Department. If a Zendesk ticket has a null requester (which occurs with suspended or deleted users), the Deskpro API will reject it with a 400 Bad Request. Your script must assign a fallback "Unknown User" Person for these cases.
Zendesk API: Extraction Strategy
For bulk extraction, use Zendesk's Incremental Export API rather than listing endpoints. Do not use the standard /api/v2/tickets.json — it is heavily rate-limited, paginates slowly (100 records per page), and caps at 10,000 results without cursor pagination. The Incremental API (/api/v2/incremental/tickets.json?start_time={unix_epoch}) is designed for bulk extraction and returns tickets modified since the provided timestamp. (developer.zendesk.com)
import requests
import time
import json
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
ZENDESK_SUBDOMAIN = "yourcompany"
ZENDESK_EMAIL = "admin@yourcompany.com"
ZENDESK_TOKEN = "your_api_token"
CHECKPOINT_FILE = Path("extraction_checkpoint.json")
def load_checkpoint():
if CHECKPOINT_FILE.exists():
return json.loads(CHECKPOINT_FILE.read_text())
return {"start_time": 0, "tickets_extracted": 0}
def save_checkpoint(start_time, count):
CHECKPOINT_FILE.write_text(json.dumps({
"start_time": start_time,
"tickets_extracted": count
}))
def extract_tickets_incremental(start_time=0):
url = f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2/incremental/tickets.json"
params = {"start_time": start_time}
auth = (f"{ZENDESK_EMAIL}/token", ZENDESK_TOKEN)
all_tickets = []
while url:
try:
response = requests.get(url, params=params, auth=auth, timeout=30)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
logger.warning(f"Rate limited. Retrying in {retry_after}s")
time.sleep(retry_after)
continue
if response.status_code != 200:
logger.error(f"HTTP {response.status_code}: {response.text[:200]}")
raise Exception(f"Zendesk API error: {response.status_code}")
data = response.json()
# Filter out deleted tickets returned by incremental API
active_tickets = [t for t in data["tickets"] if t["status"] != "deleted"]
all_tickets.extend(active_tickets)
# Checkpoint every page
end_time = data.get("end_time", start_time)
save_checkpoint(end_time, len(all_tickets))
logger.info(f"Extracted {len(all_tickets)} tickets so far")
if data.get("end_of_stream"):
break
url = data.get("next_page")
params = {} # next_page URL includes params
except requests.exceptions.RequestException as e:
logger.error(f"Network error: {e}. Retrying in 30s")
time.sleep(30)
continue
return all_ticketsZendesk Incremental Export rate limit: 10 requests per minute on standard plans (30 with the High Volume API add-on, which requires 10+ agent seats). A 500K ticket dataset at ~1,000 tickets per page takes approximately 500 pages = 50 minutes minimum at the standard rate. Plan extraction timing accordingly and run during off-peak hours.
Important Incremental API behaviors:
- Returns deleted tickets mixed with active ones — filter by
status != "deleted" - Returns system-generated events as ticket updates — not all represent real changes
- Results lag by approximately 5 minutes from real-time
- The
start_timeparameter is a Unix epoch timestamp, not ISO 8601
Deskpro API: Loading Strategy
Push data to Deskpro using the REST API v2. Deskpro's API authenticates via API keys created under Admin > Apps > API Keys. For migration scripts, use a superuser API key — this allows requests to be made on behalf of any agent, which is essential for preserving correct agent attribution on ticket replies.
import requests
import time
import logging
logger = logging.getLogger(__name__)
DESKPRO_URL = "https://yourcompany.deskpro.com/api/v2"
DESKPRO_KEY = "2:YourAPIKey"
FAILED_RECORDS_LOG = "failed_records.jsonl"
def deskpro_request(method, endpoint, payload=None, max_retries=3):
"""Make a Deskpro API request with retry logic and error handling."""
headers = {
"Authorization": f"key {DESKPRO_KEY}",
"Content-Type": "application/json"
}
url = f"{DESKPRO_URL}{endpoint}"
for attempt in range(max_retries):
try:
response = requests.request(
method, url, json=payload, headers=headers, timeout=30
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
logger.warning(f"Deskpro rate limit. Retry in {retry_after}s")
time.sleep(retry_after)
continue
if response.status_code == 400:
logger.error(
f"Bad request to {endpoint}: {response.text[:300]}"
)
# Log to dead-letter file for manual review
with open(FAILED_RECORDS_LOG, "a") as f:
f.write(json.dumps({
"endpoint": endpoint,
"payload": payload,
"error": response.text[:500],
"timestamp": time.time()
}) + "\n")
return None
if response.status_code in (201, 200):
return response.json()
logger.error(
f"Unexpected {response.status_code} from {endpoint}"
)
except requests.exceptions.RequestException as e:
wait = 2 ** attempt * 5 # 5, 10, 20 seconds
logger.error(f"Network error: {e}. Retry in {wait}s")
time.sleep(wait)
return None
def create_ticket(ticket_data):
payload = {
"subject": ticket_data["subject"],
"person": ticket_data["person_id"],
"department": ticket_data["department_id"],
"status": ticket_data["status"],
"date_created": ticket_data["created_at"],
"fields": ticket_data.get("custom_fields", {})
}
return deskpro_request("POST", "/tickets", payload)
def add_message(ticket_id, message_data):
payload = {
"message": message_data["html_body"],
"is_note": not message_data["public"],
"person": message_data["person_id"],
"date_created": message_data.get("created_at")
}
return deskpro_request("POST", f"/tickets/{ticket_id}/messages", payload)
def upload_blob(file_data, filename):
"""Upload a file to Deskpro's temporary blob storage."""
headers = {
"Authorization": f"key {DESKPRO_KEY}",
"Content-Disposition": f'attachment; filename="{filename}"'
}
response = requests.post(
f"{DESKPRO_URL}/blobs/temp",
data=file_data,
headers=headers,
timeout=120
)
if response.status_code == 201:
return response.json()["data"]["blob_auth"]
return NoneDeskpro On-Premise considerations: If you are self-hosting Deskpro, the API is identical but there are no enforced rate limits — throughput is constrained only by your server's CPU, memory, and database performance. You can bypass the API and write directly to the MySQL/PostgreSQL database, but this bypasses application-level validation, event hooks, and search indexing. It is highly risky and not recommended unless you rebuild the search index afterward.
Rate Limits
| Platform | Default Limit | Notes |
|---|---|---|
| Zendesk (Team) | 200 req/min | Account-wide; all API consumers share this quota |
| Zendesk (Professional) | 400 req/min | |
| Zendesk (Enterprise) | 700 req/min | |
| Zendesk (High Volume add-on) | 2,500 req/min | Requires 10+ agent seats; separate purchase |
| Zendesk Incremental Export | 10 req/min | 30 req/min with High Volume add-on |
| Deskpro (Cloud) | Configurable (hourly/daily quotas) | Request a temporary increase via support@deskpro.com for migration windows |
| Deskpro (On-Premise) | No enforced limit | Constrained only by server resources |
Your scripts must implement exponential backoff on 429 Too Many Requests responses. Both platforms return a Retry-After header with the number of seconds to wait.
Step-by-Step Migration Process
Step 1: Extract Data from Zendesk
- Tickets: Use Incremental Export API for bulk extraction. Use
include=comment_countsideload to identify tickets with many comments early. - Users/Organizations: Use
/api/v2/users.jsonand/api/v2/organizations.jsonwith cursor-based pagination (page [after]=...). - Ticket Comments: Use
/api/v2/tickets/{id}/comments.jsonfor each ticket. For efficiency, batch ticket IDs and use sideloads where available. Comments are returned in chronological order by default. - Attachments: Download each attachment binary from the
content_urlprovided in comment data. Store locally with ticket/comment reference metadata. Zendesk attachment URLs include authentication tokens and expire — download during extraction, not later. - Help Center: Use the Help Center API —
/api/v2/help_center/articles.jsonwith category and section metadata. See Knowledge Base migration section below. - Custom Fields: Extract field definitions from
/api/v2/ticket_fields.json,/api/v2/user_fields.json,/api/v2/organization_fields.json. These define your target schema in Deskpro. - Webhooks: Export definitions via
/api/v2/webhooks.jsonfor post-migration re-pointing.
Store all extracted data as JSON files on disk, organized by object type. This local staging layer protects against Zendesk API outages during the load phase and allows you to restart the load without re-extracting.
Zendesk data exports are not enabled by default. The account owner must contact Zendesk Customer Support to enable bulk data exports via Admin Center. The API is available on all plans, but the Admin Center export tool has plan-level restrictions:
- Team plan: No Admin Center export; API only
- Professional plan: CSV and JSON export available once enabled
- Enterprise plan: Full export including JSON with comments
Zendesk JSON exports can omit comments for tickets over 1 MB, exclude the most recent six minutes of data, and cannot export Zendesk AI agent tickets. (support.zendesk.com)
For complete data, always use the API.
Step 2: Create Schema in Deskpro
Before importing any records:
- Create Departments (mapped from Zendesk Groups/Brands). If using Deskpro Brands (Professional+ plans), create those first.
- Create Custom Fields (mapped from Zendesk Ticket/User/Org fields) with correct types, picklist values, and any field dependencies. Record every Deskpro field ID and choice option ID in your mapping table.
- Create Agents so tickets can be assigned to them. Set up permission groups to match Zendesk role mappings.
- Create any custom ticket statuses needed (e.g., an "On Hold" equivalent for Zendesk's
on-hold). - Disable all Deskpro Triggers, Escalations, and Automations. If you skip this, every imported ticket will fire notification emails, SLA timers, and auto-assignment rules. For a 50,000-ticket import, this means 50,000+ emails sent to customers about tickets that are years old.
- Take a database snapshot/backup. For Cloud, request this from Deskpro support. For On-Premise, back up your database directly.
Record all new Deskpro IDs for your mapping table.
Step 3: Load Organizations and People
Extract all Zendesk Organizations and load them into Deskpro. Handle duplicate organization names (Deskpro allows them but it creates confusion — consider appending a Zendesk ID suffix).
Then extract Zendesk Users:
- End-users → create Deskpro People for each
- Agents/Admins → create Deskpro Agents for each
- Suspended users → decide: skip, or create as inactive People
- Users with duplicate emails → merge into a single Person record; log the merge for audit
Store the mapping of Zendesk_User_ID → Deskpro_Person_ID in a persistent store. SQLite works well for migrations under 1M records; Redis for larger datasets where lookup speed matters.
Step 4: Transform and Load Tickets
For each ticket:
- Resolve
person_id,department_id, andagent_idusing your mapping table — never use Zendesk IDs directly - Map status and priority per the mapping tables above
- Handle null values: unassigned tickets (
assignee_id: null) → leaveagentunset in Deskpro; no group → assign to a default department - Set
date_createdexplicitly to preserve historical timestamps (the Deskpro API defaults to current time if omitted) - Map custom fields using field ID translation and choice ID translation
- Push to Deskpro's
POST /api/v2/ticketsendpoint - Store the ticket ID mapping for message loading
Batch in groups of 100 tickets with a checkpoint save after each batch. If the script crashes at ticket 45,001, you restart from ticket 45,001 rather than ticket 1.
Step 5: Load Messages and Notes
For each ticket, fetch comments via GET /api/v2/tickets/{ticket_id}/comments.json. Create each comment as a Deskpro Message (if public: true) or Agent Note (if public: false), preserving:
- Author: Resolve
author_idvia your mapping table. If the author is an agent, use the agent ID. If a customer, use the person ID. - Chronological order: Deskpro displays messages in creation order. Import in the same order they appear in Zendesk.
- Timestamps: Set
date_createdon each message to preserve original timing. - HTML formatting: Use the
html_bodyfield, notbody, to retain formatting, links, and inline images.
Skip Zendesk system-generated comments (status changes, field updates) unless your team needs them — they add noise to Deskpro's message thread.
Step 6: Handle Attachments
Zendesk attachments are returned as URLs with authentication tokens in comment data. Your script must:
- Download the file from Zendesk (using the
content_urlfrom the comment attachment object, passing authentication headers:Authorization: Bearer {token}or basic auth) - Upload it to Deskpro via
POST /api/v2/blobs/tempwith the file data in the request body andContent-Dispositionheader containing the filename - Attach the returned
blob_authtoken to the Deskpro Message payload in theattachmentsarray
For inline images embedded in HTML comment bodies: This is the most error-prone step. You must:
- Parse the HTML body for
<img>tags - Extract each
srcURL (typicallyhttps://yourcompany.zendesk.com/attachments/token/...) - Download each image from Zendesk's CDN — these URLs require authentication headers; unauthenticated requests return 403
- Upload each image to Deskpro via the blob API
- Replace the
srcattribute in the HTML with the new Deskpro URL
If your script doesn't pass Zendesk authentication headers when downloading inline images, every image will break in Deskpro. This is the single most common failure mode in Zendesk migrations.
Step 7: Migrate Knowledge Base
Zendesk Help Center uses a three-level hierarchy: Category → Section → Article. Deskpro Knowledge Base uses: Category → Section → Article — structurally similar but with different IDs and configuration.
For each Help Center article:
- Extract via
GET /api/v2/help_center/articles.jsonwithinclude=sections,categoriessideloads - Create categories in Deskpro KB matching Zendesk categories
- Create sections within those categories
- Create articles with HTML body content
- Handle translations: If you use Zendesk's multi-language Help Center, extract each locale separately via
GET /api/v2/help_center/{locale}/articles.json - Handle embedded media: Same inline image problem as tickets — parse article HTML, download images from Zendesk CDN, re-upload to Deskpro, rewrite URLs
- Handle article attachments: Download and re-upload separately
- Preserve SEO: Note the old Zendesk Help Center URLs and set up redirects if you're moving to Deskpro's KB on a new domain
Limitations: Zendesk community posts (forums) have no direct Deskpro equivalent. Zendesk article vote counts and view counts do not transfer.
Step 8: Validate
See the Validation section below.
Automation Script Outline
High-level Python structure for an API-based Zendesk → Deskpro migration:
# zendesk_to_deskpro.py — structural outline
import logging
import json
from zendesk_client import ZendeskAPI
from deskpro_client import DeskproAPI
from mapping import StatusMapper, PriorityMapper, FieldMapper
from storage import IDMappingStore
from html_processor import rewrite_inline_images
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
handlers=[
logging.FileHandler("migration.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
BATCH_SIZE = 100
FAILED_RECORDS = "failed_records.jsonl"
def main():
zd = ZendeskAPI(subdomain="...", email="...", token="...")
dp = DeskproAPI(url="...", api_key="...")
id_map = IDMappingStore("id_mapping.db") # SQLite for persistence
# Phase 1: Organizations
logger.info("Phase 1: Migrating organizations")
for org in zd.list_organizations():
try:
dp_org = dp.create_organization({
"name": org["name"],
"email_domains": org.get("domain_names", [])
})
if dp_org:
id_map.store("org", org["id"], dp_org["data"]["id"])
logger.info(f"Org {org['id']} -> {dp_org['data']['id']}")
except Exception as e:
log_failure("org", org, e)
# Phase 2: People (end-users) with deduplication
logger.info("Phase 2: Migrating people")
seen_emails = set()
for user in zd.list_users(role="end-user"):
email = user.get("email", "").lower()
if not email or email in seen_emails:
if email in seen_emails:
# Map duplicate to existing person
existing_id = id_map.get_by_email("person", email)
if existing_id:
id_map.store("person", user["id"], existing_id)
continue
seen_emails.add(email)
try:
dp_person = dp.create_person({
"name": user["name"] or "Unknown",
"email": email,
"organization": id_map.get("org", user.get("organization_id"))
})
if dp_person:
id_map.store("person", user["id"], dp_person["data"]["id"])
except Exception as e:
log_failure("person", user, e)
# Phase 3: Agents
logger.info("Phase 3: Migrating agents")
for agent in zd.list_users(role="agent"):
try:
dp_agent = dp.create_agent({
"name": agent["name"],
"email": agent["email"]
})
if dp_agent:
id_map.store("agent", agent["id"], dp_agent["data"]["id"])
except Exception as e:
log_failure("agent", agent, e)
# Phase 4: Tickets + Messages (batched with checkpoints)
logger.info("Phase 4: Migrating tickets")
checkpoint = load_checkpoint()
tickets = zd.list_tickets_incremental(start_time=checkpoint.get("start_time", 0))
for i, ticket in enumerate(tickets):
if ticket["id"] <= checkpoint.get("last_ticket_id", 0):
continue # Skip already-migrated tickets
try:
# Resolve foreign keys
person_id = id_map.get("person", ticket["requester_id"])
if not person_id:
# Create fallback unknown person
person_id = id_map.get("person", "UNKNOWN_FALLBACK")
agent_id = id_map.get("agent", ticket.get("assignee_id"))
dept_id = id_map.get("group", ticket.get("group_id"))
dp_ticket = dp.create_ticket({
"subject": ticket["subject"] or "(no subject)",
"person": person_id,
"agent": agent_id,
"department": dept_id,
"status": StatusMapper.map(ticket["status"]),
"priority": PriorityMapper.map(ticket.get("priority")),
"date_created": ticket["created_at"],
"fields": FieldMapper.map_custom_fields(ticket.get("custom_fields", []))
})
if not dp_ticket:
log_failure("ticket", ticket, "API returned None")
continue
dp_ticket_id = dp_ticket["data"]["id"]
id_map.store("ticket", ticket["id"], dp_ticket_id)
# Messages (in chronological order)
for comment in zd.get_ticket_comments(ticket["id"]):
# Rewrite inline images in HTML body
html_body = rewrite_inline_images(
comment.get("html_body", ""),
zd_downloader=zd,
dp_uploader=dp
)
author_id = (
id_map.get("person", comment["author_id"]) or
id_map.get("agent", comment["author_id"])
)
dp.add_message(dp_ticket_id, {
"message": html_body,
"is_note": not comment["public"],
"person": author_id,
"date_created": comment["created_at"]
})
# File attachments
for attachment in comment.get("attachments", []):
try:
file_data = zd.download_attachment(
attachment["content_url"]
)
dp.upload_and_attach(
dp_ticket_id,
file_data,
attachment["file_name"]
)
except Exception as e:
log_failure("attachment", attachment, e)
# Checkpoint every BATCH_SIZE tickets
if (i + 1) % BATCH_SIZE == 0:
save_checkpoint(ticket["id"], ticket["created_at"])
logger.info(f"Checkpoint: {i + 1} tickets migrated")
except Exception as e:
log_failure("ticket", ticket, e)
logger.info("Migration complete. Run validation.")
def log_failure(object_type, record, error):
with open(FAILED_RECORDS, "a") as f:
f.write(json.dumps({
"type": object_type,
"zendesk_id": record.get("id"),
"error": str(error)
}) + "\n")
logger.error(f"Failed {object_type} {record.get('id')}: {error}")
if __name__ == "__main__":
main()Production tips:
- Every API call should be wrapped in retry logic with exponential backoff (the
deskpro_requestfunction above demonstrates this pattern). - Use batch sizes of 100 records with checkpoint saves so you can resume from the last successful record after a failure.
- Log failed records to a JSONL file (dead-letter queue) for manual review and retry after the main migration completes.
- Make your script idempotent: before creating a record, check the ID mapping table to see if it was already migrated. This allows safe re-runs.
Edge Cases and Challenges
Duplicate Users
Zendesk allows multiple users with the same email (one primary, others as secondary identities via the Identities API). Deskpro uses email as a unique key for People and will reject creation if an email already exists with a 400 Bad Request. De-duplicate before import: merge users or choose a primary record. Your script must catch duplicate-email errors and map the Zendesk user to the existing Deskpro Person instead of failing.
Multi-Brand Configurations
Zendesk multi-brand (Enterprise plan feature) maps to Deskpro's Brands feature (available on Professional and Enterprise plans). Each Zendesk Brand typically needs its own Deskpro Department or Brand. Each brand may also have its own Help Center, requiring separate KB migrations. Plan this mapping before migration — a 5-brand Zendesk instance may need 5 Deskpro departments with distinct email channels, custom forms, and KB categories.
Side Conversations
Zendesk Side Conversations (child tickets, Slack threads, email side conversations) have no direct Deskpro equivalent. Options: (1) convert to agent notes on the parent ticket with a " [Side Conversation]" prefix for searchability, (2) create as separate linked tickets in Deskpro, or (3) skip entirely and accept the context loss. (developer.zendesk.com)
Inline Images
Zendesk auth-protects inline images embedded in HTML comment bodies. The URLs reference Zendesk's CDN with signed tokens (format: https://{subdomain}.zendesk.com/attachments/token/{token_value}/...). During extraction, download all inline images locally using authenticated requests, re-upload to Deskpro's blob storage, and rewrite the src attributes in the HTML. If you skip this step, every inline image will display as a broken image icon after you cancel your Zendesk subscription and the CDN URLs stop resolving.
Suspended Tickets
Zendesk retains suspended tickets (typically spam) in a separate queue. These have no Deskpro equivalent queue. Decision: import as regular tickets with a "suspended" label/tag for filtering, or exclude them from your extraction query (/api/v2/suspended_tickets.json is a separate endpoint from the main tickets API).
Archived Tickets
Zendesk automatically archives tickets after 120 days of closing. The Incremental Export API includes archived tickets, but standard listing endpoints (/api/v2/tickets.json) do not return them. Always use the Incremental API to ensure you capture the complete dataset.
Zendesk Export Edge Cases
Zendesk's JSON export can omit comments for tickets exceeding 1 MB in total size, and the export excludes the most recent six minutes of data at the time the export is generated. Zendesk AI agent (formerly Answer Bot) tickets cannot be exported from the Admin Center export tool. For complete data extraction, always use the API rather than the Admin Center export. (support.zendesk.com)
Triggers, Automations, and Macros
These cannot be exported from Zendesk in a Deskpro-compatible format. You can extract their definitions programmatically via GET /api/v2/triggers.json, GET /api/v2/automations.json, and GET /api/v2/macros.json — but these return Zendesk-specific JSON schemas with Zendesk field IDs, group IDs, and condition syntax that have no Deskpro parser.
You must:
- Export definitions from Zendesk API for documentation
- Map each trigger's conditions and actions to Deskpro equivalents manually
- Recreate in Deskpro's Trigger/Escalation/Macro builder
- Test each automation with sample tickets before cutover
Typical effort: 2–4 hours per complex trigger, 15–30 minutes per simple macro.
For more on this challenge, see our guide on migrating automations, macros, and workflows.
Webhook Migration
Zendesk webhooks (GET /api/v2/webhooks.json) fire HTTP callbacks to external systems on ticket events. After migration, these must be re-pointed to Deskpro's webhook system or recreated as Deskpro Triggers with "Send webhook" actions. Document each webhook's endpoint URL, authentication method, event types, and payload format. Verify with the receiving system that the Deskpro payload structure meets their requirements — Deskpro's webhook payload schema differs from Zendesk's.
Limitations and Constraints
| Constraint | Impact | Workaround |
|---|---|---|
| Deskpro has no custom objects | Zendesk Custom Objects must be flattened into custom fields | Create multi-field groups on tickets/people/orgs; lose relational queries |
| Deskpro CSV importer imports only ticket properties + first message | Full thread migration requires the API | Use API pipeline |
| Zendesk CSV export flattens conversations | Not suitable for preserving reply history | Use API extraction |
| Zendesk data export not enabled by default | Must request activation from Zendesk support | Use API (always available) |
| Zendesk Team plan has no Admin Center export | API only | Use API extraction scripts |
| Deskpro Cloud has API rate limits | Large imports throttled | Request temporary increase from Deskpro support |
| Zendesk Incremental Export: 10 req/min | 500K tickets takes ~50+ minutes to extract | Schedule during off-hours; use High Volume add-on if available |
| Zendesk attachment URLs are temporary | Must download during extraction; expire after subscription ends | Download and store locally during extraction phase |
| Zendesk JSON export omits comments for tickets over 1 MB | Large tickets lose conversation history in exports | Use per-ticket API calls for comments |
| Satisfaction ratings have limited parity | Zendesk binary Good/Bad + reasons ≠ Deskpro star ratings | Store as custom field or agent note |
Zendesk on-hold status has no native Deskpro match |
Status mapping gap | Create a custom status in Deskpro |
| Timestamps default to current time via Deskpro API | All tickets show today's date if not explicitly set | Always pass date_created parameter |
| Community forums don't transfer | Zendesk Guide community posts have no Deskpro equivalent | Archive separately or exclude |
| Zendesk Answer Bot / AI agent data | Cannot be exported via Admin Center | Use API if you need this data |
Validation and Testing
Never run a migration directly into production without at least two dry runs on a staging instance.
Record Count Comparison
After loading, compare counts for every object type:
validation_results = {}
checks = [
("organizations", zendesk_org_count, deskpro_org_count),
("people", zendesk_enduser_count, deskpro_people_count),
("agents", zendesk_agent_count, deskpro_agent_count),
("tickets", zendesk_ticket_count, deskpro_ticket_count),
("messages", zendesk_comment_count, deskpro_message_count),
]
for name, expected, actual in checks:
match = expected == actual
validation_results[name] = {
"expected": expected,
"actual": actual,
"match": match,
"delta": actual - expected
}
status = "✓" if match else "✗"
print(f"{status} {name}: expected {expected}, got {actual} (Δ {actual - expected})")
# Also check failed_records.jsonl for records that couldn't be migrated
with open("failed_records.jsonl") as f:
failed_count = sum(1 for _ in f)
print(f"Failed records: {failed_count} (review failed_records.jsonl)")A small negative delta is expected if you excluded suspended tickets, deleted users, or system-generated comments. Document any delta with an explanation.
Field-Level Validation
Sample 5% of records (minimum 50, maximum 500) and verify:
- Subject matches exactly
- Status mapped correctly per mapping table
- Priority mapped correctly
- Custom field values preserved (check each field type: text, choice, date, number)
- Requester linked to correct Person (verify by email match, not just ID)
- Assignee linked to correct Agent
- Organization membership intact
- Department assignment correct
- All comments/messages present in correct chronological order
- Message public/private flag matches Zendesk comment public flag
- Attachments present and downloadable
- Inline images render correctly (not broken)
date_createdmatches original Zendeskcreated_at- Labels/tags present
Sampling Strategy
- Random sample across date ranges (oldest 10%, newest 10%, middle 10%)
- Include the 10 tickets with the highest comment counts (stress test for large threads)
- Include 10 tickets with attachments and inline images
- Include tickets with every custom field type used
- Include tickets from each Zendesk Group/Brand
- Include at least 5 tickets with null/empty edge cases (no assignee, no group, no priority)
UAT Process
- Set up 2–3 agents with access to the staging Deskpro instance
- Have them work with migrated data for 48 hours using their normal workflows
- Compare their experience to the same tickets in Zendesk (keep Zendesk read-only)
- Test specifically: search by keyword, search by requester email, reply to a ticket, download an attachment, view organization history, use a rebuilt macro, verify routing rules
- Document every discrepancy with ticket ID, expected value, and actual value
- Fix and re-run if validation failures exceed 1% of sampled records
Rollback Plan
Deskpro doesn't have a one-click "undo import" function. Your rollback options:
- Deskpro Cloud:
Frequently Asked Questions
- Does Deskpro have a built-in tool to migrate from Zendesk?
- Yes. Deskpro includes a native Zendesk importer under Admin > Apps > Importer. You provide your Zendesk subdomain and an admin API token, and it pulls core data (tickets, users, organizations) directly. However, it has limited support for custom fields, full conversation threads, and attachments — so complex migrations still require the API pipeline approach.
- How long does a Zendesk to Deskpro migration take?
- For small datasets (under 20K tickets), expect 1–2 weeks including planning, test migration, and validation. Enterprise datasets with complex custom field schemas, attachments, and multi-brand configurations typically take 3–5 weeks. The Zendesk Incremental Export API rate limit (10 requests/minute) can extend extraction time for large datasets.
- Can I migrate from Zendesk to Deskpro using CSV files?
- Only for basic user and organization records. Zendesk CSV exports omit ticket comments and descriptions, and Deskpro's CSV importer only imports ticket properties and the first message — no threaded conversations or attachments. An API-based migration is required for full fidelity.
- Can I migrate Zendesk Custom Objects to Deskpro?
- Not directly. Deskpro has no custom object model. Zendesk Custom Objects must be flattened into custom fields on Tickets, People, or Organizations. You lose the relational query capability, but you preserve the data as field values.
- Will my Zendesk triggers, macros, and automations migrate to Deskpro?
- No. Triggers, Automations, and Macros cannot be exported from Zendesk in a machine-readable format that Deskpro understands. They must be manually documented and recreated as Deskpro Triggers, Escalations, and Macros/Snippets.


