Skip to content

Zammad to Zendesk Migration: The Technical Guide

A step-by-step technical guide to migrating from Zammad to Zendesk. Covers API extraction, object mapping, attachment handling, rate limits, and edge cases.

Abdul Abdul · · 29 min read
Zammad to Zendesk Migration: The Technical Guide
TALK TO AN ENGINEER

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

Zammad to Zendesk Migration: The Technical Guide

Migrating from Zammad to Zendesk is an API-to-API data translation problem. There is no native import wizard, no built-in connector, and no vendor-provided migration path between the two platforms. Zammad stores ticket conversations as articles — individual messages attached to a ticket — while Zendesk uses comments within a discrete ticket model. If you need full support history in Zendesk with timestamps, internal notes, attachments, and customer relationships intact, the practical path is extracting via Zammad's REST API and loading through Zendesk's Ticket Import API. (docs.zammad.org)

This guide covers the full architecture: object mapping, API constraints on both sides, extraction strategies, transform logic, time/cost estimation, and the failure modes that derail teams mid-migration.

Tested against: Zammad 6.x REST API and Zendesk API v2 as of mid-2025. Zammad's API can change between major versions — verify endpoint behavior against your installed version before scripting.

For a broader pre-migration framework, see the Zendesk Migration Checklist. For general scoping guidance, see the Help Desk Data Migration Playbook.

Warning

There is no official reverse of Zammad's inbound Zendesk migrator. Zammad documents Zendesk → Zammad migration, but for Zammad → Zendesk you must use API-based import workflows. (docs.zammad.org)

Why Companies Migrate from Zammad to Zendesk

The most common drivers across Zammad-to-Zendesk migration projects:

  • Ecosystem breadth. Zendesk's marketplace has 1,500+ integrations. Zammad's integration ecosystem is narrower, relying heavily on its REST API for custom connections. Teams that need native connectors to Salesforce, Shopify, or Jira hit limits with Zammad first.
  • Managed SaaS operations. Zammad can be self-hosted, which means your team owns infrastructure, upgrades, and database maintenance. Moving to Zendesk shifts that operational burden to the vendor.
  • AI and automation. Zendesk's investment in AI — Intelligent Triage, AI Agents, Answer Bot — is tightly coupled with its ticketing data model. Zammad's AI features are newer and less mature.
  • Reporting and compliance. Zendesk Explore provides built-in analytics dashboards, CSAT surveys, SLA enforcement, and agent capacity metrics out of the box. Zammad's reporting is more limited and often requires custom queries or external BI tools.
  • Team scaling. Organizations growing past 20–30 agents often find Zendesk's role-based permissions, multi-brand support, and enterprise security features (SSO, audit logs, data locality) more aligned with their needs.

If cost is the headline reason for evaluating platforms, look at total operating cost rather than seat price alone. Zammad often wins on pure licensing, while Zendesk can win on day-to-day administration and ecosystem breadth.

Zammad vs Zendesk: Core Data Model Differences

Before you map a single field, understand the structural gap between these two platforms.

Concept Zammad Zendesk
Ticket conversations Articles — each message, note, or phone call is a separate article with its own type, sender, and internal flag Comments — each reply or note is a comment on the ticket, marked public or not
Customer records Users (role-based: customer, agent, admin) Users (split into end-users and agents with separate permissions)
Companies Organizations Organizations
Groups/Teams Groups (used for routing and permissions) Groups (used for assignment and views)
Custom fields Object attributes on tickets, users, organizations, and groups Custom fields on tickets, users, organizations; Custom objects for extended data
Knowledge base Knowledge Base with categories and answers (multilingual via translations) Help Center (Zendesk Guide) with sections and articles
Tags Tags on tickets Tags on tickets, users, organizations
Attachments Stored per article, fetched via article attachment endpoint Stored per comment, uploaded via Upload API and referenced by token

The key translation challenge: Zammad articles map to Zendesk comments, but the metadata structure differs. Zammad articles carry a type field (email, phone, note, web, sms, chat, social) and a sender field (Customer, Agent, System). Zendesk comments are simpler — they're either public or internal, with an author_id. Some source-channel nuance gets flattened in the translation. (docs.zammad.org)

Migration Approaches Compared

How it works: Extract all data from Zammad using its REST API (/api/v1/), transform it in a middle layer, and load it into Zendesk using the Ticket Import API (/api/v2/imports/tickets) and standard object APIs. For batch creation, Zendesk also offers /api/v2/imports/tickets/create_many which accepts up to 100 tickets per request and returns a job status for tracking. (developer.zendesk.com)

When to use it: Any migration where you need full ticket history, attachments, timestamps, and relationship integrity.

Pros:

  • Full control over data mapping and transformation
  • Preserves timestamps (created_at, updated_at, solved_at)
  • Handles attachments, internal notes, and custom fields
  • Can be scripted for repeatability and test runs
  • Batch endpoint (create_many) reduces total API calls

Cons:

  • Requires engineering time to build and test
  • Must handle pagination, rate limiting, and error recovery
  • Zendesk Ticket Import API has specific constraints (no SLA metrics, no side conversations)

Complexity: High

2. CSV Export/Import

How it works: Export flat records from Zammad (or derive CSVs from API pulls) and import into Zendesk via its data importer for users, organizations, and custom object records. (support.zendesk.com)

When to use it: Only for migrating users and organizations — not full ticket history.

Pros:

  • No code required for basic imports
  • Fast to test

Cons:

  • Zammad has no native CSV export for ticket history
  • Zendesk's CSV importer doesn't handle full ticket conversation history
  • No attachment support
  • No timestamp preservation for tickets

Complexity: Low

3. Direct Database Access (Self-Hosted Zammad Only)

How it works: If you run self-hosted Zammad, query the PostgreSQL database directly to extract tickets, articles, users, organizations, and attachments. Transform with SQL or ETL scripts, then load into Zendesk via API.

When to use it: Large migrations (100K+ tickets) where API extraction would be too slow, or when you need data that the API doesn't expose.

Key tables in Zammad's PostgreSQL schema (Rails conventions):

  • tickets — core ticket records
  • ticket_articles — individual messages/notes per ticket
  • ticket_article_types — lookup for article type names
  • ticket_article_senders — lookup for sender types
  • stores — attachment metadata; binary content stored in store_files or on filesystem depending on storage adapter
  • users, organizations, groups — standard entity tables
  • object_manager_attributes — custom field definitions
  • tags, tag_objects, tag_items — tag associations

Pros:

  • Fastest extraction method
  • Access to all data including audit trails and internal metadata
  • No API rate limit concerns on the source side

Cons:

  • Only works for self-hosted Zammad instances
  • Requires understanding Zammad's database schema (Ruby on Rails conventions)
  • Schema may change between Zammad versions — verify against your installed version
  • Still need Zendesk API for loading

Complexity: High

4. Third-Party Migration Tools

How it works: Platforms like Help Desk Migration offer pre-built connectors for helpdesk-to-helpdesk moves. Connect source and target accounts, map fields, run a demo migration, validate, then run full and optional delta passes. (help-desk-migration.com)

When to use it: Small to mid-size migrations where speed matters more than customization.

Pros:

  • Pre-built mapping for common objects
  • Visual interface, less engineering required
  • Often include demo/trial migration

Cons:

  • Zammad is not commonly supported by most third-party migration tools
  • Limited control over field-level transformations
  • Custom fields and complex relationships may not map correctly
  • Per-record pricing can get expensive at scale

Complexity: Low–Medium

5. Middleware / iPaaS (Zapier, Make, n8n)

How it works: Use an integration platform to connect Zammad triggers/API to Zendesk actions. Typically event-driven, not suited for bulk historical migration. Zammad webhooks are event-driven and retry failed deliveries up to four times. (admin-docs.zammad.org)

When to use it: Ongoing sync of new tickets or incremental forwarding during a phased transition — not historical migration.

Pros:

  • No code for simple workflows
  • Good for post-migration sync

Cons:

  • Not designed for bulk data migration
  • Extremely slow for historical data
  • Rate limit issues on both sides
  • No timestamp preservation for historical records

Complexity: Low

Approach Comparison Table

Method Ticket History Attachments Timestamps Custom Fields Complexity Best For
API-to-API ✅ Full ✅ Yes ✅ Preserved ✅ Mapped High Any migration
CSV Export/Import ❌ No ❌ No ❌ No Partial Low Users/orgs only
Direct DB Access ✅ Full ✅ Yes ✅ Preserved ✅ All High Self-hosted, large datasets
Third-Party Tools Partial Varies Varies Limited Low–Med Small, standard migrations
Middleware/iPaaS ❌ No (live only) Varies ❌ No Limited Low Ongoing sync

Which Approach to Choose

  • Small team, < 5K tickets, no custom fields: CSV for users + a third-party tool for tickets (if one supports Zammad). Otherwise, API-to-API.
  • Mid-market, 5K–100K tickets, custom fields: API-to-API migration. No shortcut.
  • Enterprise, 100K+ tickets, self-hosted Zammad: Direct database extraction + Zendesk API loading.
  • Ongoing sync needed post-migration: API-to-API for historical data, then middleware or Zammad webhooks for forward sync.
  • Low engineering bandwidth: Third-party tool or managed migration service.

Migration Time & Cost Estimation

Before committing to an approach, estimate the API call volume and wall-clock time.

API Call Formula

For an API-to-API migration with T tickets, A average articles per ticket, and F average attachments per article:

Total API calls ≈
    (T / 100)                          # Zammad ticket pagination
  + T                                   # Zammad article fetches (1 per ticket)
  + (T × A × F)                        # Zammad attachment downloads
  + ceil(T × A × F)                    # Zendesk attachment uploads (1 per file)
  + (users / 100)                       # Zammad user pagination
  + users                               # Zendesk user create_or_update
  + ceil(T / 100)                       # Zendesk ticket import (using create_many, 100/batch)

Example: 50,000 Tickets

Assumptions: 50K tickets, 4 articles/ticket avg, 0.3 attachments/article avg, 5K users.

Phase API Calls Target
Extract tickets (paginated) 500 Zammad
Extract articles 50,000 Zammad
Download attachments 60,000 Zammad
Upload attachments 60,000 Zendesk
Create/update users 5,000 Zendesk
Import tickets (batched at 100) 500 Zendesk
Total Zendesk API calls ~65,500

Wall-Clock Time by Plan

Zendesk Plan Rate Limit Zendesk-Side Time (est.) Total with Extraction (est.)
Team (200 req/min) 200/min ~5.5 hours 8–12 hours
Professional (700 req/min) 700/min ~1.6 hours 4–6 hours
Enterprise (2,500 req/min) 2,500/min ~26 min 2–4 hours

These are optimistic lower bounds. In practice, add 30–50% for retries, rate limit backoff, error handling, and Zammad-side extraction time. Attachment-heavy instances (e.g., insurance, legal) may double these figures.

Engineering Time

Task Estimated Hours
Data audit and field mapping 4–8
Script development (extraction, transform, load) 40–80
Attachment handling logic 8–16
Error handling, retry logic, checkpointing 8–16
Test migration (sandbox) × 2 runs 8–16
Validation and debugging 8–24
Cutover execution and monitoring 4–8
Total 80–168 hours

When to Use a Managed Migration Service

Building an API-to-API migration script in-house is technically straightforward on paper. In practice, it breaks down in predictable ways:

  • Attachment handling doubles the complexity — you must download from Zammad, upload to Zendesk's Upload API, retrieve a token, and attach it to the correct comment.
  • Rate limiting on Zendesk's side means a 50K-ticket migration can take days if not properly batched and throttled.
  • Data validation across two different schemas requires automated comparison tooling that most teams build ad-hoc and never reuse.
  • Edge cases — suspended users, empty comment bodies, inline images, merged tickets — each require specific handling that only surfaces during testing.
  • Engineering opportunity cost — every sprint your team spends on migration scripts is a sprint not spent on product.

A script that works for 100 tickets will often fail at 100,000 tickets due to undocumented API behaviors, rate limit throttling, and malformed historical data.

Common DIY failure modes:

  • Overwritten timestamps: Using the standard Zendesk Tickets API instead of the Import API timestamps every historical ticket with the date of the migration.
  • Broken inline images: Zammad stores inline images as CID references. If these aren't extracted, uploaded to Zendesk, and re-linked in the HTML body, you end up with broken image icons in every historical ticket.
  • Suspended account rejections: Migrating tickets tied to agents who no longer work at the company can cause Zendesk to reject the payload entirely.

If you're evaluating build vs. buy, the comparison is worth reading.

Pre-Migration Planning

Before writing a single line of migration code, audit your Zammad instance and define the scope of the move.

Data Audit Checklist

  • Tickets: Total count, date range, status distribution (open, closed, pending)
  • Articles per ticket: Average and max — this determines Zendesk API call volume (see estimation formula above)
  • Users: Total customers, agents, and admins. Identify suspended or inactive users
  • Organizations: Count, membership sizes, custom fields
  • Custom object attributes: List all custom fields on tickets, users, and organizations. Note data types (text, select, date, boolean, integer)
  • Tags: Collect all tags in use across tickets
  • Knowledge Base: Category count, answer count, languages, attachments
  • Attachments: Total size, largest individual attachment (Zendesk limit: 50 MB/file), file types
  • Groups: Current group structure and routing rules
  • Zammad version: Check GET /api/v1/version — API behavior varies across major versions

Data Residency & Compliance

For organizations subject to GDPR, HIPAA, or other data protection frameworks:

  • Data transfer mechanisms: If your Zammad instance is hosted in the EU and your Zendesk instance is US-based, ensure Standard Contractual Clauses (SCCs) or equivalent transfer mechanisms are in place before moving personal data.
  • Encryption in transit: Both Zammad and Zendesk APIs support TLS 1.2+. Verify your migration scripts enforce HTTPS and do not write unencrypted PII to intermediate storage (local disk, staging databases).
  • Data minimization: Filter out data you don't need before extraction. Migrating PII you intend to delete anyway increases regulatory exposure.
  • Right to erasure: If you have pending deletion requests in Zammad, process them before migration. Don't migrate data that should have been deleted.
  • Data Processing Agreements: Confirm your Zendesk DPA covers the data categories you're migrating. Zendesk publishes its DPA at zendesk.com/company/agreements-and-terms/data-processing-agreement.

Define Migration Scope

Not everything needs to move. Common exclusions:

  • Spam and test tickets — filter by tag or status before extraction
  • Tickets older than 2–3 years — unless regulatory requirements dictate otherwise
  • System-generated articles — Zammad creates system notes for status changes and group reassignments. Decide during scoping whether to import them as internal notes or skip them
  • Audit trails and time accounting — Zendesk doesn't import these; archive separately
  • Unused custom fields — delete or ignore before mapping to Zendesk

Zendesk Sandbox Provisioning

Test migrations require a Zendesk sandbox environment:

  • Zendesk Professional and Enterprise plans include a sandbox (called "Premium Sandbox" on Enterprise). It mirrors your production configuration.
  • Team and Growth plans do not include a sandbox. You can request a trial instance from Zendesk sales, or create a separate Zendesk trial account for testing.
  • Sandboxes have the same API rate limits as your production plan tier.

Migration Strategy

Strategy Description When to Use
Big bang Migrate everything in a single cutover window Small datasets (< 10K tickets), tolerance for brief downtime
Phased Migrate by group, date range, or ticket status in stages Mid-size datasets, need to validate each batch
Incremental (big bang + delta sync) Migrate historical data first, then sync new data in near-real-time until cutover Large datasets, zero-downtime requirement

For most large Zammad-to-Zendesk moves, the incremental approach works best: migrate closed/archived tickets first (using Zendesk's archive_immediately flag), validate, then run a final delta migration on cutover weekend to catch tickets created or updated since the initial sync. For the delta pass, Zammad's webhook system can trigger extraction of newly created or updated tickets, feeding them into the same transform-and-load pipeline.

Data Model & Object Mapping

This is the most important section. Get the mapping wrong and everything downstream breaks.

A crosswalk is a lookup table that maps IDs from the source system to IDs in the target system (e.g., {zammad_user_id: 42 → zendesk_user_id: 98765}). You build it during user/org creation and reference it when constructing ticket payloads.

Core Object Mapping

Zammad Object Zendesk Object Notes
Ticket Ticket 1:1 mapping. Use Ticket Import API (/api/v2/imports/tickets) or batch endpoint (/api/v2/imports/tickets/create_many)
Ticket Article Ticket Comment Map internal flag → public: false. Map article type and sender to determine comment author
User (customer) User (end-user) Create via Users API before importing tickets
User (agent) User (agent) Must exist in Zendesk before ticket import — tickets reference assignee_id and author_id
Organization Organization Create via Organizations API. Map organization_id on users
Group Group Create manually or via API. Map group_id on tickets
Tag Tag Applied to tickets during import
Knowledge Base Category Help Center Section Requires Zendesk Guide. Map category hierarchy
Knowledge Base Answer Help Center Article HTML content migration. Handle embedded images
Custom Object Attribute Custom Ticket/User/Org Field Must create fields in Zendesk first with matching data types
Warning

Critical: Zendesk's Ticket Import API requires that all referenced users (requester_id, assignee_id, author_id) exist before ticket creation. Always create users and organizations first. Decide whether to provision paid agent seats for former employees or map their historical tickets to a generic "Legacy Agent" account.

Ticket State Mapping

Zammad states do not align perfectly with Zendesk. You must implement a translation layer:

Zammad State Zendesk Status Notes
new new Direct map
open open Direct map
pending reminder pending Map pending_time to Zendesk's due date if applicable
pending close pending Collapsed with pending reminder
closed closed See immutability warning below
merged closed Add internal note or tag referencing original ticket number
Warning

Zendesk strictly enforces the closed status. Once a ticket is set to closed via the API, it can never be updated again. Ensure all comments, tags, and attachments are correct in the payload before setting this status.

Field-Level Mapping: Tickets

Zammad Field Zendesk Field Transformation
number external_id Preserves original ticket number for agent cross-reference
title subject Direct map
state.name status See state mapping table above
priority.name priority Map: 1 lowlow, 2 normalnormal, 3 highhigh
group.name group_id Lookup group ID in Zendesk by name via crosswalk
owner_id assignee_id Map Zammad user ID → Zendesk user ID via crosswalk table
customer_id requester_id Map Zammad user ID → Zendesk user ID via crosswalk table
created_at created_at Direct map (ISO 8601). Must use Import API to set
updated_at updated_at Direct map
close_at solved_at Only for closed tickets
tags tags Array → comma-separated or array

Field-Level Mapping: Articles → Comments

Zammad Article Field Zendesk Comment Field Transformation
body html_body Zammad stores HTML. Requires inline image URL rewriting
internal public Invert: internal: truepublic: false
created_by_id author_id Map via user crosswalk
created_at created_at Direct map
attachments uploads Download from Zammad, upload to Zendesk, attach via token

Handling Custom Fields

Zammad lets you define custom object attributes on tickets, users, organizations, and groups. Before migration:

  1. Export your Zammad object definitions via GET /api/v1/object_manager_attributes to list all custom fields with their data types and options (docs.zammad.org)
  2. Create matching fields in Zendesk — ticket fields, user fields, or organization fields. Note that Zendesk field types may not map 1:1 (e.g., Zammad's richtext has no direct equivalent in Zendesk custom fields; convert to textarea or text)
  3. Map picklist values — if Zammad uses a select or tree_select field, ensure every option value exists in the Zendesk dropdown. Zammad's tree_select (hierarchical) must be flattened to a single-level dropdown in Zendesk or encoded with a delimiter (e.g., Parent::Child)
Info

Zendesk supports custom objects, but they're plan-gated with limits: 3, 5, 30, or 50 custom objects depending on plan, up to 100 fields per object, and up to 5 or 10 lookup relationship fields per object. (support.zendesk.com)

Migration Architecture

The architecture follows a standard Extract, Transform, Load (ETL) pattern, but requires specific sequencing to maintain relational integrity.

Data Flow

┌─────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Zammad     │     │   Transform      │     │    Zendesk      │
│   REST API   │────▶│   Layer          │────▶│   Import API    │
│   /api/v1/   │     │   (Python/Node)  │     │   /api/v2/      │
└─────────────┘     └──────────────────┘     └─────────────────┘
     Extract              Transform                Load

  • Tickets            • Field mapping          • Users/Orgs API
  • Articles           • Status translation     • Ticket Import API
  • Users              • User ID crosswalk      • Upload API
  • Organizations      • Attachment download    • Help Center API
  • KB Content         • HTML sanitization
  • Attachments        • CID image rewriting

Dependency Sequence

Relational integrity requires loading objects in this order:

  1. Extract and load Organizations
  2. Extract and load Users (map to Organizations)
  3. Extract and load Groups
  4. Extract Tickets with their Articles and Attachments
  5. Upload Attachments to Zendesk (get tokens)
  6. Load Tickets with comments and attachment tokens via Ticket Import API
  7. Migrate Knowledge Base content (see KB section below)

Zammad API: Extraction Details

Zammad's API follows REST conventions at /api/v1/. Key endpoints for extraction:

  • Tickets: GET /api/v1/tickets?page={n}&per_page={n} — returns up to 100 tickets per page
  • Ticket Articles: GET /api/v1/ticket_articles/by_ticket/{ticket_id} — all articles for a ticket
  • Users: GET /api/v1/users?page={n}&per_page={n}
  • Organizations: GET /api/v1/organizations?page={n}&per_page={n}
  • Groups: GET /api/v1/groups
  • Tags: GET /api/v1/tags?object=Ticket&o_id={ticket_id}
  • Object Attributes: GET /api/v1/object_manager_attributes
  • Attachments: GET /api/v1/ticket_attachment/{ticket_id}/{article_id}/{attachment_id}
  • Knowledge Base: GET /api/v1/knowledge_bases (categories and answers accessible via sub-endpoints)

Authentication: Token-based (Authorization: Token token={your_token}) is recommended over basic auth. (docs.zammad.org)

Info

Pagination limit: Zammad enforces a hard maximum of 100 objects per page. You cannot raise this limit. Use page and per_page parameters and iterate until the response returns fewer items than per_page. Adding expand=true includes related object names inline, reducing follow-up lookups.

Zendesk API: Loading Details

  • Users: POST /api/v2/users/create_or_update
  • Organizations: POST /api/v2/organizations/create_or_update
  • Ticket Import (single): POST /api/v2/imports/tickets — supports multiple comments per ticket, timestamp overrides, and archive_immediately for closed tickets (developer.zendesk.com)
  • Ticket Import (batch): POST /api/v2/imports/tickets/create_many — accepts up to 100 tickets per request, returns a job_status URL for polling completion
  • Attachments: POST /api/v2/uploads — returns a token to reference in comment payloads
  • Help Center Articles: POST /api/v2/help_center/sections/{section_id}/articles

Rate limits by plan:

Zendesk Plan Requests/Minute
Team 200
Growth 400
Professional 700
Enterprise 2,500

The Ticket Import endpoint shares the account-level rate limit. For a Professional plan, you can create ~700 tickets per minute under ideal conditions. In practice, attachment uploads and user lookups consume part of that budget.

Warning

Zendesk Ticket Import API constraints: Triggers don't fire on imported tickets. SLA metrics (first reply time, resolution time) are not calculated for imported tickets. No side conversation support on imported tickets. Requesters must be active — suspended users will cause the import to fail with a 422 Unprocessable Entity. (support.zendesk.com)

Tip

If you're building a new migration utility in mid-2026 or later, prefer Zendesk OAuth for authentication. Zendesk has announced that API tokens will be permanently deactivated on April 30, 2027. (developer.zendesk.com)

Step-by-Step Migration Process

Step 1: Extract Data from Zammad

Paginate through all endpoints and store the raw JSON responses. Use a staging database (PostgreSQL, MongoDB, or local JSON files) to prevent re-querying Zammad during transformation failures. Encrypt staging storage if it contains PII.

import requests
 
ZAMMAD_URL = "https://your-zammad.example.com"
ZAMMAD_TOKEN = "your-api-token"
headers = {"Authorization": f"Token token={ZAMMAD_TOKEN}"}
 
def extract_all(endpoint, per_page=100):
    page = 1
    all_records = []
    while True:
        resp = requests.get(
            f"{ZAMMAD_URL}/api/v1/{endpoint}",
            headers=headers,
            params={"page": page, "per_page": per_page, "expand": "true"}
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        all_records.extend(data)
        if len(data) < per_page:
            break
        page += 1
    return all_records
 
tickets = extract_all("tickets")
users = extract_all("users")
orgs = extract_all("organizations")

For articles and attachments, iterate per ticket:

def extract_articles(ticket_id):
    resp = requests.get(
        f"{ZAMMAD_URL}/api/v1/ticket_articles/by_ticket/{ticket_id}",
        headers=headers
    )
    resp.raise_for_status()
    return resp.json()
 
def download_attachment(ticket_id, article_id, attachment_id):
    resp = requests.get(
        f"{ZAMMAD_URL}/api/v1/ticket_attachment/{ticket_id}/{article_id}/{attachment_id}",
        headers=headers
    )
    resp.raise_for_status()
    return resp.content  # binary data

Step 2: Build the User Crosswalk

Create users and organizations in Zendesk first. Store the mapping: {zammad_user_id: zendesk_user_id}.

ZENDESK_URL = "https://your-instance.zendesk.com"
ZENDESK_AUTH = ("admin@company.com/token", "your-zendesk-token")
 
def create_zendesk_user(zammad_user):
    payload = {
        "user": {
            "name": f"{zammad_user['firstname']} {zammad_user['lastname']}",
            "email": zammad_user["email"],
            "role": "end-user",  # or "agent" based on Zammad role
            "external_id": str(zammad_user["id"])
        }
    }
    resp = requests.post(
        f"{ZENDESK_URL}/api/v2/users/create_or_update",
        auth=ZENDESK_AUTH,
        json=payload
    )
    resp.raise_for_status()
    return resp.json()["user"]["id"]

Step 3: Transform and Load Tickets

For each Zammad ticket, build the Zendesk Ticket Import payload with all comments:

def build_zendesk_ticket(zammad_ticket, articles, user_map, group_map):
    comments = []
    for article in articles:
        comment = {
            "author_id": user_map.get(article["created_by_id"]),
            "html_body": rewrite_inline_images(
                article.get("body", ""),
                article,
                zammad_ticket["id"]
            ),
            "public": not article.get("internal", False),
            "created_at": article["created_at"]
        }
        # Handle attachments if present
        if article.get("attachments"):
            tokens = upload_attachments(article, zammad_ticket["id"])
            comment["uploads"] = tokens
        # Zendesk rejects empty comment bodies
        if not comment["html_body"] or not comment["html_body"].strip():
            comment["html_body"] = "[Attachment only]"
        comments.append(comment)
 
    status_map = {
        "new": "new", "open": "open",
        "pending reminder": "pending", "pending close": "pending",
        "closed": "closed", "merged": "closed"
    }
 
    return {
        "ticket": {
            "subject": zammad_ticket.get("title", "No subject"),
            "requester_id": user_map.get(zammad_ticket["customer_id"]),
            "assignee_id": user_map.get(zammad_ticket.get("owner_id")),
            "group_id": group_map.get(
                zammad_ticket["group_id"],
                DEFAULT_FALLBACK_GROUP_ID  # for tickets without group assignment
            ),
            "status": status_map.get(zammad_ticket.get("state"), "open"),
            "external_id": str(zammad_ticket["number"]),
            "created_at": zammad_ticket["created_at"],
            "updated_at": zammad_ticket["updated_at"],
            "tags": zammad_ticket.get("tags", []),
            "comments": comments
        }
    }

Step 4: Upload Attachments

def upload_attachments(article, ticket_id):
    tokens = []
    for att in article["attachments"]:
        # Check Zendesk's 50 MB per-file limit
        if att.get("size", 0) > 50 * 1024 * 1024:
            log.warning(f"Skipping oversized attachment {att['filename']} "
                       f"({att['size']} bytes) on ticket {ticket_id}")
            continue
        content = download_attachment(ticket_id, article["id"], att["id"])
        resp = requests.post(
            f"{ZENDESK_URL}/api/v2/uploads?filename={att['filename']}",
            auth=ZENDESK_AUTH,
            headers={"Content-Type": att.get("preferences", {}).get(
                "Content-Type", "application/octet-stream"
            )},
            data=content
        )
        resp.raise_for_status()
        tokens.append(resp.json()["upload"]["token"])
    return tokens

Step 5: Import with Rate Limit Handling

import time
import logging
 
log = logging.getLogger(__name__)
 
def import_ticket(ticket_payload, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.post(
            f"{ZENDESK_URL}/api/v2/imports/tickets.json",
            auth=ZENDESK_AUTH,
            json=ticket_payload
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            log.warning(f"Rate limited. Retry-After: {retry_after}s")
            time.sleep(retry_after)
            continue
        if resp.status_code == 422:
            # Common causes: suspended user, missing required field,
            # empty comment body, invalid group_id
            log.error(f"422 Unprocessable Entity: {resp.json()}")
            # Example error response:
            # {"error": "RecordInvalid",
            #  "description": "Validation failed: Requester is suspended"}
            return None
        resp.raise_for_status()
        return resp.json()
    raise Exception(f"Failed after {max_retries} retries")
 
# Production implementation requirements beyond this pseudocode:
# - Checkpointing: persist last-processed ticket ID to resume after crashes
# - Crosswalk persistence: store {zammad_id: zendesk_id} mappings in a database
# - Delta sync: track tickets modified after initial extraction timestamp
# - Structured logging: log every success/failure with ticket IDs for audit
# - Idempotency: use external_id to prevent duplicate imports on re-runs

Step 6: Validate the Migration

After loading, run validation checks:

  • Record counts: Compare ticket, user, and organization counts between source and target
  • Field spot-checks: Sample 50–100 tickets and compare field values side by side
  • Attachment verification: Confirm attachments are accessible and correctly linked
  • Relationship integrity: Verify that tickets point to the correct requester, assignee, and organization
  • Timestamp accuracy: Confirm created_at timestamps match original Zammad values
  • Internal notes: Verify internal notes are not visible to end-users in the Help Center

Knowledge Base Migration

The Knowledge Base is often mentioned as a line item but requires its own migration workflow. Zammad's Knowledge Base uses a category → answer hierarchy with built-in multilingual support via translation_ids. Zendesk Guide uses a category → section → article three-level hierarchy.

Mapping Structure

Zammad KB Object Zendesk Guide Object Notes
Knowledge Base Help Center 1:1; Zendesk Guide must be enabled
Category (top-level) Category POST /api/v2/help_center/categories
Category (nested) Section POST /api/v2/help_center/categories/{id}/sections
Answer Article POST /api/v2/help_center/sections/{id}/articles
Translation Article Translation Separate article per locale, or use POST /api/v2/help_center/articles/{id}/translations

Implementation Steps

  1. Extract KB structure: GET /api/v1/knowledge_bases returns the knowledge base with nested categories. Extract answers via category endpoints.
  2. Create category/section hierarchy in Zendesk Guide before creating articles.
  3. Migrate article HTML content: Zammad KB answers store HTML bodies. Download any embedded images, upload them to Zendesk via the Attachments API or host them externally, and rewrite src attributes in the HTML.
  4. Handle translations: For each Zammad answer with multiple translations, create the article in the default locale first, then add translations via the Zendesk Translations API. Zendesk requires that the Help Center has the target locale enabled before translations can be added.
  5. Verify rendering: Check that tables, code blocks, and formatted content render correctly in Zendesk Guide — CSS differences between platforms can break visual layouts.

KB Migration Limitations

  • Zammad KB supports draft and published states per translation; Zendesk Guide uses draft and published per article. Ensure draft articles don't accidentally publish.
  • Zendesk Guide article body size limit is 65,535 characters (approximately 64 KB of HTML).
  • Internal-only KB articles in Zammad should map to articles in a restricted Zendesk Guide section (requires Zendesk Guide Professional or Enterprise for user segment restrictions).

Edge Cases & Challenges

These are the issues that surface mid-migration and cost teams days.

Inline Images (CID References)

Zammad frequently stores images pasted into emails as inline CID (Content-ID) attachments. The HTML body references them with <img src="cid:image001.png@01D...">. Your script must:

  1. Parse the HTML body of each Zammad article
  2. Identify cid: references in src attributes
  3. Match each CID to the corresponding attachment in the article's attachment list
  4. Download the attachment binary from Zammad
  5. Upload it to Zendesk's Upload API
  6. Rewrite the src attribute to the Zendesk-hosted URL

If you skip this, every historical ticket with inline images will display broken image icons.

Suspended Users

Zendesk's Ticket Import API requires that requesters and submitters are active users. If a former customer or ex-employee was created and then suspended, the import will fail with:

{
  "error": "RecordInvalid",
  "description": "Validation failed: Requester is suspended"
}

Workaround: Pre-check all users after creation. Unsuspend users before import, or remap their tickets to a placeholder account.

Empty Comment Bodies

Zammad can create articles with no body (e.g., an email with only an attachment). Zendesk's API rejects empty comment bodies with a 422. Workaround: Insert a placeholder like " [Attachment only]" for empty bodies.

Merged Tickets

Zammad supports ticket merging. There is no way to import pre-merged tickets via the Zendesk API — you cannot reserve ticket IDs to recreate cross-references. Workaround: Import merged tickets as closed tickets with an internal note or tag referencing the original ticket number (stored in external_id).

Missing Group Assignment

Zammad allows tickets to exist without an explicitly assigned group in certain configurations. Zendesk requires a group for every ticket. Assign a default fallback group in your ETL logic for orphaned tickets.

Multi-Organization Users

If a Zammad user belongs to multiple organizations, Zendesk supports this — but you must enable the multi-organization feature in Zendesk settings before running your user import. Otherwise the API will reject secondary organization mappings. (docs.zammad.org)

Semantic Flattening of Article Types

Zammad article types like email, phone, note, sms, chat, and social types will not always survive as distinct behaviors in Zendesk comments. The channel metadata gets flattened to public/private comments with an author. Consider preserving the original article type as a tag (e.g., zammad_type:phone) or in the comment body prefix (e.g., [Phone Call]) if this context matters to your team.

Duplicate Records from Re-Runs

If you run the migration script multiple times (test runs + final run), you'll create duplicate tickets. Use external_id on tickets and create_or_update for users to enable idempotent operations. Before the final run, bulk-delete all test data from the sandbox or production instance.

Very Large Tickets

Zendesk tickets can contain up to 5,000 comments. Tickets above that limit need splitting, archiving, or a partial-history strategy. In Zammad, long-running tickets with extensive back-and-forth or automated system notes can exceed this. Check your max articles-per-ticket count during the data audit. (developer.zendesk.com)

Zendesk 429 Rate Limit Response

When you hit rate limits, Zendesk returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 42

{
  "error": "TooManyRequests",
  "description": "You have exceeded the rate limit. Please retry after 42 seconds."
}

The Retry-After header value is in seconds. Build your retry logic around this header, not hardcoded sleep values.

Limitations & Constraints

Be explicit about what you will lose or compromise in this migration:

  • SLA metrics: Zendesk does not calculate SLA metrics on imported tickets. Tag imported tickets and exclude them from SLA reports. (support.zendesk.com)
  • Comment immutability: Once a comment is created in Zendesk, it cannot be edited via the API. If your transformation script injects the wrong text, you must delete the entire ticket and re-import it.
  • API throttling: Zendesk's rate limits are strictly enforced. Migrating 500,000+ tickets will take several days purely due to API speed limits. Plan your cutover window accordingly. (See time estimation section above.)
  • Side conversations: The Ticket Import API does not support side conversations. Convert them to internal comments or skip them.
  • Time tracking: Zammad's time accounting data doesn't have a native equivalent in Zendesk. Store as a custom field or internal note.
  • Ticket audit logs: Zammad's detailed change history (who changed what, when) does not import into Zendesk. Archive separately (e.g., export to CSV or a data warehouse).
  • Large attachments: Zendesk's ticket attachment limit is 50 MB per file. Oversized files need an external archive or link-out strategy. (support.zendesk.com)
  • Satisfaction ratings: Zammad doesn't have built-in CSAT. If you've built custom satisfaction tracking, it won't map to Zendesk's native CSAT.
  • Custom objects: Zendesk's Custom Object architecture is structurally different from Zammad's custom attributes and uses a separate API endpoint (/api/v2/custom_objects/records). Plan gating and field limits apply.
  • Knowledge Base translations: Zammad's KB supports translations natively with translation_ids. Zendesk Guide also supports multilingual content, but the translation model differs and requires separate article creation per locale. (See KB migration section.)

Validation & Testing

Never execute a full migration without rigorous testing.

Pre-Migration Testing

Run at least two test migrations before the final cutover:

  1. Small sample test: Migrate 50–100 tickets to a Zendesk sandbox to validate field mapping, status translation, and attachment handling
  2. Full volume test: Migrate all data to a Zendesk sandbox to validate timing, rate limit handling, and edge cases

Post-Migration Validation Checklist

  • Total ticket count matches (source vs target)
  • Total user count matches
  • Total organization count matches
  • Sample 100 tickets: verify subject, status, requester, assignee, group
  • Sample 20 tickets with attachments: verify files are accessible
  • Verify internal notes are not visible to end-users
  • Verify created_at timestamps match original Zammad timestamps
  • Check for orphaned tickets (no requester, no group)
  • Verify knowledge base articles render correctly with formatting intact
  • Confirm custom field values populated correctly
  • Verify inline images display correctly (no broken CID references)
  • Check that external_id values match original Zammad ticket numbers

Incremental Validation

Do not wait until 100,000 tickets are imported to check for errors. Validate the data after the first 100, 1,000, and 10,000 records. Automated spot-check scripts that compare source and target field values are more reliable than manual checks at scale.

Rollback Plan

Zendesk doesn't offer a one-click rollback. Plan for it:

  • Tag all imported data with a migration identifier (e.g., migration_zammad_2026)
  • Use Zendesk's bulk delete APIs to remove imported tickets if needed (note: Zendesk permanently deletes tickets after 30 days in the deleted tickets view)
  • Keep Zammad running in read-only mode until validation is complete
  • Don't cancel your Zammad subscription or decommission infrastructure until post-migration QA is fully signed off

For a detailed post-migration QA process, see our Post-Migration QA Checklist.

Post-Migration Tasks

Once data is loaded, the configuration work begins.

Rebuild Automations and Triggers

Zammad's triggers, macros, and SLAs don't migrate. You must recreate them in Zendesk:

  • Triggers: Map Zammad trigger conditions and actions to Zendesk trigger equivalents
  • Macros: Recreate agent macros for common actions
  • SLAs: Define new SLA policies based on your existing response/resolution targets
  • Business hours: Configure schedules and holidays
  • Views: Build agent views that match your Zammad overviews

Do not turn automations on blindly against partially validated data — imported tickets don't fire triggers, but newly created tickets will. For guidance on rebuilding automations, see How to Migrate Automations, Macros & Workflows.

Reconnect Channels

  • Update DNS records (MX or forwarding rules) to route support emails to Zendesk
  • Reconnect API integrations (Jira, Shopify, Salesforce, etc.)
  • Verify web widget and chat configurations
  • Update any customer-facing links that pointed to Zammad's customer portal

User Training

Zammad's UI is fundamentally different from Zendesk's workspace. Run agent training sessions on Zendesk's interface, macros, and views before cutover. Assign a migration liaison who can triage issues in the first two weeks post-go-live.

Monitoring

  • Watch for tickets still arriving in Zammad after cutover (redirect email channels)
  • Monitor Zendesk for data inconsistencies reported by agents
  • Track CSAT scores post-migration to detect service quality changes
  • Monitor API error logs for any failed imports that need manual remediation

Best Practices

  1. Back up everything before migration. Take a full database snapshot of your self-hosted Zammad instance (or export all data via API for cloud instances) before starting.
  2. Run test migrations. At least two — one small sample, one full volume against a Zendesk sandbox.
  3. Create users and organizations before tickets. Zendesk's Ticket Import API requires valid user IDs for requester, assignee, and comment authors.
  4. Use archive_immediately: true for closed tickets. This bypasses the normal ticket lifecycle and places imported closed tickets directly in the archive, reducing clutter in agent views.
  5. Tag imported tickets. Add a tag like migrated_from_zammad to all imported tickets. This lets you filter, report on, and if necessary bulk-delete imported data.
  6. Preserve original ticket numbers. Store Zammad's ticket number in Zendesk's external_id field so agents can cross-reference old ticket numbers.
  7. Handle rate limits gracefully. Build exponential backoff with jitter into your migration script. Read the Retry-After header — don't hard-code sleep timers.
  8. Validate incrementally. Don't wait until the full migration is complete to start checking data. Validate after each batch.
  9. Freeze configuration. Implement a change freeze on Zammad two weeks before migration. No new custom fields, groups, or macros should be created during this window.
  10. Use the batch import endpoint. POST /api/v2/imports/tickets/create_many accepts up to 100 tickets per request and significantly reduces total API calls compared to single-ticket imports.

Sample Data Mapping Reference

Use this as a baseline to build your crosswalk mapping sheet:

Zammad Field Zammad Type Zendesk Field Zendesk Type Notes
ticket.number string ticket.external_id string Cross-reference key
ticket.title string ticket.subject string Direct map
ticket.state.name enum ticket.status enum Translate values (see state mapping above)
ticket.priority.name enum ticket.priority enum Map 1–4 scale
ticket.group.name relation ticket.group_id integer Lookup by name
ticket.customer_id integer ticket.requester_id integer Via user crosswalk
ticket.owner_id integer ticket.assignee_id integer Via user crosswalk
ticket.created_at datetime ticket.created_at datetime ISO 8601
ticket.close_at datetime ticket.solved_at datetime Only for closed tickets
article.body HTML comment.html_body HTML Sanitize inline image URLs
article.internal boolean comment.public boolean Invert value
article.created_by_id integer comment.author_id integer Via user crosswalk
article.created_at datetime comment.created_at datetime ISO 8601
article.type string (no equivalent) Preserve as tag or body prefix
article.sender string (no equivalent) Use to determine author_id context
user.firstname + lastname strings user.name string Concatenate
user.email string user.email string Direct map
user.organization_id integer user.organization_id integer Via org crosswalk
organization.name string organization.name string Direct map
organization.domain string organization.domain_names array Wrap in array

Frequently Asked Questions

Can I migrate full ticket history from Zammad to Zendesk?
Yes, but only via API. Zendesk's Ticket Import API (/api/v2/imports/tickets) lets you create tickets with multiple comments, preserving timestamps and author information. CSV imports only work for users and organizations, not ticket conversation history.
Does Zendesk have a built-in Zammad importer?
No. There is no native migration path between Zammad and Zendesk. Zammad documents a Zendesk → Zammad migrator, but there is no official reverse. You must extract data via Zammad's REST API and load it using Zendesk's Ticket Import API, or use a managed migration service.
What are Zendesk's API rate limits for ticket imports?
Zendesk rate limits depend on your plan: 200 req/min (Team), 400 (Growth), 700 (Professional), 2,500 (Enterprise). The Ticket Import endpoint shares the account-level limit. Exceeding it returns a 429 error with a Retry-After header.
Will inline images transfer from Zammad to Zendesk?
Not automatically. Zammad stores inline images as CID (Content-ID) references. Your migration script must parse the HTML body, download the CID image attachments, upload them to Zendesk, and rewrite the HTML source links to point to the new Zendesk URLs.
Are SLA metrics preserved when importing tickets into Zendesk?
No. Zendesk does not calculate SLA metrics on imported tickets — first reply time, resolution time, and similar metrics will be blank. Zendesk recommends tagging imported tickets and excluding them from SLA reports.

More from our Blog

Zendesk Migration Checklist
Checklist/Zendesk

Zendesk Migration Checklist

Complete Zendesk migration checklist with pre-migration planning, API-based data transfer, manual setup, and post-migration validation steps

Tejas Mondeeri Tejas Mondeeri · · 4 min read
Help Desk Data Migration Playbook: What Data to Move and What to Leave Behind
Help Desk

Help Desk Data Migration Playbook: What Data to Move and What to Leave Behind

This definitive playbook answers the single most critical question: "What data do we actually need to move?". This strategic guide helps you declutter and decide what's precious and what's junk. We provide a clear breakdown of the non-negotiable Tier 1 data, like tickets , knowledge bases , and user profiles, versus the Tier 2 data that provides rich context, like automations and organizations. Use this as your strategic checklist to avoid common mistakes and ensure a flawless, functional new help desk.

Raaj Raaj · · 7 min read
Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration
Help Desk

Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration

Ensure your help desk migration is a success with this comprehensive 20-point post-migration QA checklist. This expert guide details the 20 essential tests needed to validate your data integrity, system functionality, user-friendliness, and performance . Learn exactly how to check everything from ticket data, attachments, and knowledge base articles to critical workflows, automations, and integrations before you go live. This process is your final line of defense against lost tickets, broken workflows, and unhappy customers.

Raaj Raaj · · 8 min read