---
title: "Zammad to Help Scout Migration: A Technical Guide"
slug: zammad-to-help-scout-migration-a-technical-guide
date: 2026-08-03
author: Roopi
categories: [Help Scout, Migration Guide, Help Desk]
excerpt: "A technical guide to migrating from Zammad to Help Scout via API — covering data mapping, rate limits, KB migration, and edge cases."
tldr: "Zammad to Help Scout migration requires custom API scripting — no native path exists. Map tickets/articles to conversations/threads, always use imported: true, and budget for the 100-thread cap."
canonical: https://clonepartner.com/blog/zammad-to-help-scout-migration-a-technical-guide/
---

# Zammad to Help Scout Migration: A Technical Guide


# Zammad to Help Scout Migration: A Technical Guide

Migrating from Zammad to Help Scout means translating between two fundamentally different data models via API. There is no native migration tool, no vendor connector, and no built-in migration path between these platforms. Zammad stores ticket messages as **articles** — discrete records attached to a ticket. Help Scout stores them as **threads** within a **conversation**. To preserve full history with timestamps, internal notes, and attachments, you must extract from Zammad's REST API and load through Help Scout's Mailbox API 2.0.

This guide covers the complete architecture: object mapping, API constraints on both sides, extraction and load strategies, knowledge base migration, resumability patterns, and the failure modes that trip up engineering teams mid-project.

**Verified against:** Zammad 6.x REST API and Help Scout Mailbox API 2.0 / Docs API v1 as of mid-2025. Zammad's API behavior can change between major versions — verify endpoints against your installed version before scripting.

For a broader pre-migration framework, see the [Help Scout Migration Checklist](https://clonepartner.com/blog/blog/help-scout-migration-checklist/). For details on extracting data from Help Scout (useful if you're running a parallel test), see [How to Export Data from Help Scout](https://clonepartner.com/blog/blog/how-to-export-data-from-help-scout-methods-api-limits-formats/).

> [!WARNING]
> Neither Zammad nor Help Scout provides an official migration tool for this direction. Zammad documents inbound migrations (e.g., Zendesk → Zammad), but every production Zammad → Help Scout path requires custom API scripting.

## Why Teams Move from Zammad to Help Scout

The most common reasons we see in migration projects:

- **Managed hosting vs. self-hosted overhead.** Zammad can run self-hosted or on Zammad's hosted offering. Teams that don't want to manage infrastructure, updates, and Elasticsearch clusters move to a fully managed SaaS platform like Help Scout.
- **Simplicity over configurability.** Zammad's depth — custom object attributes, role-based permissions, knowledge base granularity — is powerful but adds operational complexity. Help Scout's mailbox-centric model is deliberately simpler.
- **Shared inbox workflow.** Help Scout was designed around the shared inbox metaphor. Teams that primarily work through email, without heavy use of Zammad's state machines, SLA timers, or complex escalation rules, find Help Scout's UX faster for agents.
- **Ecosystem integrations.** Help Scout has native integrations with HubSpot, Shopify, Jira, and Slack. Zammad's integration ecosystem relies more heavily on its REST API for custom connections.
- **Data residency note for EU teams.** Zammad can be self-hosted in any region. Help Scout is a US-hosted SaaS (AWS us-east-1). Teams with GDPR or data residency obligations should evaluate whether Help Scout's [Data Processing Agreement](https://www.helpscout.com/company/legal/dpa/) and Standard Contractual Clauses satisfy their requirements before migration.

## Core Data Model: Zammad vs. Help Scout

Understanding these structural gaps is the single most important step before writing any migration code.

| Concept | Zammad | Help Scout |
|---|---|---|
| Ticket / Conversation | **Ticket** (title, state, priority, group, owner) | **Conversation** (subject, status, mailboxId, assignee) |
| Message | **Article** (body, content_type, internal, sender, type) | **Thread** (text, type: customer/reply/note/phone/chat) |
| Customer | **User** (with role `Customer`) | **Customer** (with emails, phones, social handles) |
| Agent | **User** (with role `Agent`) | **User** (team member) |
| Organization | **Organization** (shared_organization, domain) | **Company** field on Customer — no first-class entity |
| Routing | **Group** | **Mailbox** |
| Knowledge Base | **KB Category → Answer** (multi-language, visibility per article) | **Docs: Collection → Category → Article** (public/private per collection) |
| Tags | **Tags** (per ticket) | **Tags** (per conversation) |
| Custom Fields | **Object Attributes** (per ticket, user, org) | **Custom Fields** (per mailbox, limited types) |

### Key Structural Gaps

**Organizations don't exist as a first-class object in Help Scout.** Zammad's Organization groups customers by company, with shared organization visibility settings. Help Scout has a `company` field on the Customer object, but no Organization entity with its own hierarchy or permissions. Flatten organization names into the customer's company field and optionally add a tag for filtering.

**Zammad article types are richer.** Zammad tracks article types like `email`, `phone`, `note`, `web`, `twitter`, `facebook`, `telegram`, and system-generated types. Help Scout threads support `customer`, `reply`, `note`, `phone`, and `chat`. Social channel articles from Zammad must be mapped to the closest Help Scout equivalent — typically `note` — with the original channel preserved in the thread body.

**User/Customer separation works differently.** Zammad treats agents and customers as `Users` with different role assignments. Help Scout enforces a strict separation between internal agents (`Users`) and external contacts (`Customers`). Your extraction script must evaluate the Zammad user's role and route them to the correct Help Scout endpoint.

**Zammad's internal flag maps cleanly.** Zammad's `internal: true` on articles maps directly to Help Scout's `note` thread type for internal-only visibility.

**Merged tickets in Zammad.** When Zammad merges one ticket into another, the source ticket gets state `merged` and articles are not automatically re-attached to the target ticket — they remain on the source ticket record. When extracting, treat merged tickets as independent records. Import them as closed conversations with a `merged` tag and add a note to each referencing the target ticket number. Do not attempt to re-merge conversation histories in Help Scout.

## API Architecture: Extraction and Load

### Zammad Extraction (Source)

Zammad follows an "API First" philosophy — the UI is itself an API client. All operations available in the interface are accessible via the REST API at `/api/v1/`.

**Authentication:** Token-based (created under Your Profile → Token Access) or HTTP Basic Auth. For migration scripts, token auth is preferred.

**Key extraction endpoints:**

```
GET /api/v1/tickets?page={n}&per_page={n}&expand=true
GET /api/v1/ticket_articles/by_ticket/{ticket_id}
GET /api/v1/ticket_attachment/{ticket_id}/{article_id}/{attachment_id}
GET /api/v1/users?page={n}&per_page={n}
GET /api/v1/organizations?page={n}&per_page={n}
GET /api/v1/tags?object=Ticket&o_id={ticket_id}
GET /api/v1/knowledge_bases/{kb_id}/categories
GET /api/v1/knowledge_bases/{kb_id}/answers/{id}
```

The `expand=true` parameter on ticket requests returns resolved names instead of just IDs for groups, priorities, and states, saving you secondary lookup calls during the transform phase.

**Pagination constraints:** Zammad uses `page` and `per_page` query parameters. There is a configurable server-side limit for the number of returned objects (typically 500). If you request `per_page=1000` and the server limit is 500, the server limit wins. Iterate through pages until an empty array is returned.

**Rate considerations:** Zammad does not enforce a hard, global rate limit by default, though your underlying infrastructure (Nginx/Apache) might. If you are extracting from a hosted Zammad instance, keep concurrent API requests under 10–15 per second to avoid degrading application performance for active agents. For the Help Scout load side, use a single-threaded sequential loader with exponential backoff rather than parallel workers — Help Scout's rate limit is account-wide, not per-connection, so parallel workers share the same quota and amplify backoff collisions.

> [!NOTE]
> The `/api/v1/ticket_articles/by_ticket/{id}` endpoint does **not** support pagination — it returns all articles for a ticket in a single response. For tickets with hundreds of articles, this can produce very large payloads. Plan your memory allocation accordingly.

### Help Scout Load (Target)

Help Scout's Mailbox API 2.0 runs at `https://api.helpscout.net/v2/` and uses OAuth 2.0 authentication. The Docs API for knowledge base content is a separate surface at `https://docsapi.helpscout.net/v1/` and uses API key auth.

**Key load endpoints:**

```
POST /v2/customers
POST /v2/conversations
POST /v2/conversations/{id}/customer
POST /v2/conversations/{id}/reply
POST /v2/conversations/{id}/note
POST /v2/conversations/{id}/phone
POST /v2/conversations/{id}/chat
POST /v2/conversations/{id}/threads/{tid}/attachments
```

**OAuth token management:** Access tokens expire after 48 hours. Your migration script must handle token refresh automatically. Use the client credentials flow for server-to-server migration scripts.

**Rate limits:** Help Scout rate-limits at the account level, not per user. Published limits are approximately 200 requests/minute on Standard plans and 400 requests/minute on Plus plans — verify your current limits against the `X-RateLimit-Limit` response header on your first authenticated request, as these values can change. Response headers `X-RateLimit-Remaining` and `Retry-After` tell you exactly where you stand. Build exponential backoff into your loader from day one.

**Webhook behavior with `imported: true`.** When `imported: true` is set on a conversation or thread creation request, Help Scout suppresses outbound email delivery. Based on API behavior testing, webhook delivery is also suppressed for imported conversations — however, Help Scout does not formally document this guarantee. If your account has active webhooks and suppression is critical, disable them in the Help Scout UI before starting your migration run and re-enable after validation.

## Step-by-Step Migration Workflow

### Step 1: Audit and Scope Your Zammad Data

Before writing any code, answer these questions:

- **How many tickets?** This determines total API calls and migration duration.
- **How many articles per ticket?** Tickets with more than 100 articles will hit Help Scout's thread limit.
- **Are you using Zammad's Knowledge Base?** This requires the separate Docs API.
- **Custom object attributes?** Map which Zammad custom fields have equivalents in Help Scout's custom fields (limited to text, number, dropdown, date types).
- **Organization data?** Decide how to flatten it.
- **Multi-language KB content?** Help Scout Docs has no native multi-language structure — you must choose a resolution strategy before scripting (see KB section below).

Run a quick count via the Zammad API:

```bash
curl -s -H "Authorization: Token token=YOUR_TOKEN" \
  "https://your-zammad.example.com/api/v1/tickets?page=1&per_page=1" \
  | jq '.[] | .id' | tail -1
```

Or use the search endpoint with `only_total_count=true` for precise counts.

### Step 2: Set Up the Help Scout Target Environment

1. **Create mailboxes** that correspond to your Zammad Groups. Each Zammad Group maps to one Help Scout Mailbox. If you have a highly granular group structure in Zammad, you may need to consolidate into fewer Mailboxes and use Help Scout Tags or Custom Fields for categorization.
2. **Invite agents.** Help Scout does not support user creation via the REST API — agents must be invited through the UI or via SCIM provisioning (Pro plan only). Use matching email addresses so your migration script can resolve Zammad agent emails to Help Scout User IDs.
3. **Create custom fields** on each mailbox to capture Zammad-specific metadata that doesn't have a native Help Scout equivalent (e.g., Zammad ticket number, priority level, original group name).
4. **Create an OAuth app** under Your Profile → My Apps. Record the App ID and App Secret for the client credentials flow.
5. **Disable active webhooks** in Help Scout Settings → Webhooks before starting migration. Re-enable after validating the import is complete.

### Step 3: Extract Data from Zammad

Build an extraction pipeline that:

1. Paginates through all tickets using `/api/v1/tickets?page={n}&per_page=100&expand=true`
2. For each ticket, fetches all articles via `/api/v1/ticket_articles/by_ticket/{ticket_id}`
3. For each article with attachments, downloads attachment content from `/api/v1/ticket_attachment/{ticket_id}/{article_id}/{attachment_id}`
4. Resolves customer and agent IDs to full user records using `/api/v1/users/{id}`
5. Fetches tags per ticket using `/api/v1/tags?object=Ticket&o_id={ticket_id}`
6. Stores everything in a local intermediate format (JSON files or a staging database like PostgreSQL) for transformation

Do not attempt to pipe data directly from Zammad to Help Scout in memory. Network drops will ruin your state management. Store raw data locally, then transform and load as a separate step.

### Step 4: Transform Data

This is where the real complexity lives. For each Zammad ticket, build a Help Scout conversation payload.

**Map ticket status:**

| Zammad State | Help Scout Status | Notes |
|---|---|---|
| `new` | `active` | |
| `open` | `active` | |
| `pending reminder` | `pending` | Requires `waitUntil` timestamp — see below |
| `pending close` | `pending` | Requires `waitUntil` timestamp — see below |
| `closed` | `closed` | |
| `merged` | `closed` | Add tag `merged`; see merged ticket handling above |
| `removed` | — | Skip, or map to `closed` with a tag |

> [!CAUTION]
> **`pending` status requires a `waitUntil` field.** If you map any Zammad ticket to Help Scout's `pending` status without including a `waitUntil` ISO 8601 timestamp in the conversation payload, the API returns `400 Bad Request`. Zammad's `pending reminder` state stores a `pending_time` field on the ticket — use that value. For `pending close` tickets where no reminder time is set, default to a reasonable future timestamp (e.g., 24 hours from migration time) or map to `active` instead.
>
> ```json
> {
>   "status": "pending",
>   "waitUntil": "2025-09-01T09:00:00Z"
> }
> ```

**Map article type to thread type:**

| Zammad Article Type | Zammad Sender | Help Scout Thread Type |
|---|---|---|
| `email` | `Customer` | `customer` |
| `email` | `Agent` | `reply` |
| `note` | `Agent` | `note` |
| `phone` | `Agent` | `phone` |
| `web` | `Customer` | `customer` |
| `twitter`, `facebook`, `telegram` | any | `note` (with original channel in body) |
| `system` | `System` | `note` (or skip if no useful content) |

**Handle the `imported` flag:** Every conversation and thread creation request must include `imported: true`. This is non-negotiable.

> [!CAUTION]
> If you omit `imported: true`, Help Scout will treat the import as live activity. It will send actual emails to customers for every migrated thread, update reporting metrics for the current day, trigger active webhooks, and reopen closed conversations. There is no bulk undo. Always set `imported: true`.

**Preserve timestamps:** When `imported: true` is set, you can override `createdAt` on conversations and threads to match the original Zammad timestamps. This is essential for maintaining accurate conversation chronology.

```json
{
  "imported": true,
  "mailboxId": 12345,
  "type": "email",
  "subject": "Original Zammad Ticket Subject",
  "customer": {
    "email": "customer@example.com"
  },
  "createdAt": "2023-10-12T08:30:00Z",
  "status": "closed",
  "threads": [
    {
      "type": "customer",
      "customer": {
        "email": "customer@example.com"
      },
      "text": "I need help with my account.",
      "imported": true,
      "createdAt": "2023-10-12T08:30:00Z"
    }
  ]
}
```

**Handle CC and BCC fields.** Zammad handles CCs and BCCs natively within its email routing. Help Scout's API requires you to explicitly define CC and BCC arrays on the conversation object. If you fail to map these from Zammad's article headers, historical context of who was looped into a conversation will be lost.

### Step 5: Load into Help Scout

For each transformed ticket:

1. **Create or find the customer** via `POST /v2/customers` (or look up by email). Help Scout auto-creates customers if you reference them by email in a conversation create call.
2. **Create the conversation** with the first thread using `POST /v2/conversations`. Include `imported: true`, the mapped status, subject, mailboxId, tags, and custom fields.
3. **Add remaining threads** in chronological order (oldest first) using the appropriate thread endpoint (`/customer`, `/reply`, `/note`, `/phone`). Always include `imported: true` and the correct `createdAt`. If you import out of order, the conversation timeline in Help Scout will be garbled.
4. **Upload attachments** either inline with the thread (base64-encoded in the `attachments` array) or after thread creation via the attachment upload endpoint.

> [!CAUTION]
> **100-thread limit per conversation.** Help Scout enforces a hard cap of 100 threads per conversation. If you try to add a thread to a conversation that already has 100 threads, the API returns HTTP 412. For Zammad tickets exceeding 100 articles, you must either split into multiple conversations (with cross-reference tags and notes) or consolidate system-generated articles into a single summary note.

### Step 6: Implement Resumability

Long-running migrations will encounter network failures, token expiry, or rate-limit backoffs that interrupt a batch mid-run. Never restart from ticket zero. Implement a checkpoint pattern from the beginning.

**Checkpoint file approach (simple):**

```python
import json, os

CHECKPOINT_FILE = "migration_checkpoint.json"

def load_checkpoint():
    if os.path.exists(CHECKPOINT_FILE):
        with open(CHECKPOINT_FILE) as f:
            return json.load(f)
    return {"last_zammad_ticket_id": 0, "imported_count": 0}

def save_checkpoint(zammad_ticket_id, helpscout_conversation_id, imported_count):
    data = {
        "last_zammad_ticket_id": zammad_ticket_id,
        "last_helpscout_conversation_id": helpscout_conversation_id,
        "imported_count": imported_count
    }
    with open(CHECKPOINT_FILE, "w") as f:
        json.dump(data, f)
```

**Idempotency strategy:** Before creating a conversation, check whether a conversation with the Zammad ticket ID already exists in Help Scout by searching your custom field that stores the original Zammad ticket number. If found, skip creation and proceed to thread validation. This prevents duplicate conversations from accumulating across retry runs.

**Database approach (recommended for large migrations):** Use a local SQLite or PostgreSQL table with columns `(zammad_ticket_id, helpscout_conversation_id, status, imported_at)`. On each successful conversation creation, write the Help Scout conversation ID and mark status `complete`. On restart, `SELECT` only records where `status != 'complete'`. This is more robust than a flat file for migrations exceeding 10,000 tickets.

### Step 7: Migrate the Knowledge Base

Zammad's Knowledge Base uses a **Category → Answer** structure with optional multi-language support. Help Scout Docs uses a three-tier **Collection → Category → Article** model.

**Mapping approach:**

1. Export Zammad KB categories and answers via `/api/v1/knowledge_bases/{kb_id}/categories` and answer endpoints.
2. Map Zammad top-level categories to Help Scout **Collections**. Map Zammad subcategories to Help Scout **Categories**.
3. Create articles via the Docs API at `POST https://docsapi.helpscout.net/v1/articles` with `collectionId`, `categories`, article `text` (HTML), and `status`.

The Docs API uses API key authentication (not OAuth) and has its own rate limits based on the number of Docs sites on the account.

**Multi-language KB content.** Zammad KB answers support multiple language variants per article (e.g., an answer exists in English, German, and French as separate `translation` records). Help Scout Docs has no native multi-language structure — each article exists once, in one language. You must choose a resolution strategy:

- **Single primary language:** Import only the language variant that matches your primary Help Scout Docs audience. Tag articles with the original Zammad KB language for reference. This is the simplest path.
- **Separate collections per language:** Create one Help Scout Collection per language (e.g., "Support Articles — DE", "Support Articles — FR"). Import each language variant as a separate article into its corresponding collection. Adds collection sprawl but preserves all content.
- **Language suffix in title:** Import all variants into a single collection and append the language code to each article title (e.g., "How to Reset Your Password [DE]"). Searchable but visually noisy.

There is no automated way to link translated variants in Help Scout Docs post-migration. Choose your strategy before scripting.

**Article visibility mapping:**

| Zammad KB State | Help Scout Docs Status |
|---|---|
| `published` | `published` |
| `internal` | `notpublished` |
| `archived` | `notpublished` (add tag `archived`) |

> [!NOTE]
> Zammad KB articles can have `internal`, `published`, and `archived` states. Help Scout Docs articles support `published` and `notpublished`. Map Zammad's `internal` articles to `notpublished` in Help Scout, or omit them entirely if they're intended for agent-only use and you'd prefer to maintain them in a separate internal wiki tool.

### Step 8: Validate the Migration

After loading, verify:

- [ ] Total conversation count matches expected ticket count from Zammad
- [ ] Thread counts per conversation match article counts per ticket (accounting for any consolidation)
- [ ] Timestamps on threads match original Zammad article timestamps
- [ ] Internal notes are not visible to customers
- [ ] Attachments open and render correctly
- [ ] Customer records resolve correctly (no orphaned conversations)
- [ ] Tags transferred accurately
- [ ] Knowledge base articles are published and categorized correctly
- [ ] No emails were sent to customers during import (check Help Scout's outgoing email logs)
- [ ] `pending` conversations have correct `waitUntil` values set
- [ ] Webhook events were not fired for imported conversations (check webhook delivery logs if webhooks were active)

## Attachment Migration Pitfalls

Attachments are the most common source of migration failures.

- **Help Scout's attachment limit is 10 MB per file.** The attachment data must be sent as a base64-encoded string, which adds approximately 33% size overhead. That means your effective raw file size limit is roughly **7.5 MB** to stay within the encoded limit.
- **Zammad has no default attachment size limit at the API level** (it depends on your web server config). Attachments larger than 7.5 MB raw must be uploaded to external storage (like AWS S3) with a download link injected into the thread body.

**Attachment payload format:**

```json
"attachments": [
  {
    "fileName": "error_log.txt",
    "mimeType": "text/plain",
    "data": "YmFzZTY0IGVuY29kZWQgc3RyaW5nIGhlcmU="
  }
]
```

**Inline images.** Zammad users frequently paste images directly into the editor. These inline images are stored as standard attachments but are referenced via HTML `cid:` (Content-ID) tags in the message body. When migrating to Help Scout, you must parse the HTML body, upload the attachment, and rewrite the `src` attribute of the `<img>` tag to point to the new attachment URL. If you skip this step, inline images render as broken links. Internal Zammad URLs embedded in HTML bodies will also break post-migration — your transform step must detect and handle these.

## Rate Limits and Migration Duration Estimates

For every Zammad ticket, the minimum Help Scout API calls are:

1. 1 call to create the conversation (with first thread)
2. N−1 calls to add remaining threads
3. 1 call per attachment (if uploaded separately)

A ticket with 5 articles and 2 attachments = ~7 API calls.

**At 400 requests/minute (Plus plan):**

| Ticket Volume | Avg Articles/Ticket | Est. API Calls | Est. Duration |
|---|---|---|---|
| 5,000 | 4 | ~20,000 | ~50 minutes |
| 25,000 | 5 | ~125,000 | ~5.2 hours |
| 100,000 | 6 | ~600,000 | ~25 hours |
| 250,000 | 8 | ~2,000,000 | ~83 hours |

These are optimistic estimates. Factor in token refresh cycles, retry backoff on 429 responses, attachment upload time, and occasional 504 timeouts. Real-world migrations typically run 1.5–2x these numbers. On the **Standard plan at approximately 200 requests/minute**, double all duration estimates. Verify your actual limit from the `X-RateLimit-Limit` header returned on your first authenticated request.

## Common Failure Modes

**Forgetting `imported: true`.** The single most expensive mistake. Help Scout sends actual emails to your customers for every migrated thread and reopens closed conversations. There is no bulk undo.

**Missing `waitUntil` on pending conversations.** Any conversation imported with `status: pending` that does not include a `waitUntil` ISO 8601 timestamp returns `400 Bad Request`. This is not documented prominently in Help Scout's API reference. Zammad's `pending_time` field on the ticket is the correct source value.

**OAuth token expiry mid-migration.** Access tokens last 48 hours. Long-running migrations must implement automatic refresh. Implement your checkpoint file or database pattern (see Step 6) so that a token expiry producing 401 responses triggers a token refresh and resumes from the last successfully imported ticket — not a restart from zero.

**Orphaned tickets without customer emails.** Help Scout requires every conversation to have a valid `customer` object with an email address. Zammad allows tickets to be created via phone or social channels without a strict email requirement. If you POST a conversation without a customer email, the API rejects it with a `400 Bad Request`. Implement fallback logic: assign a generic placeholder (e.g., `unknown-customer@yourdomain.com`) and append the original Zammad user's name to the top of the thread text.

**Thread ordering.** Threads must be imported in chronological order (oldest first). If you import out of order, the conversation timeline will be garbled. Sort articles by `created_at` ascending before loading.

**Duplicate customers.** Help Scout matches customers by email address. If the same person exists in Zammad with multiple email addresses across different tickets, you may end up with duplicate Help Scout customer records. Pre-deduplicate on the Zammad side.

**HTML sanitization.** Help Scout's thread body accepts HTML but applies its own sanitization. Complex HTML from Zammad — especially from rich text editors or forwarded emails with deep nesting — may render differently. Test a sample batch of 50–100 tickets before running the full migration.

**Missing agent mapping.** Help Scout requires agents to exist as Users before they can be assigned to conversations. If an agent was deleted from Zammad but appears on historical tickets, you need a fallback assignment strategy — assign to a "Migration" user or leave unassigned.

**Merged ticket articles.** Zammad merged tickets retain their articles on the source ticket record. If you extract all tickets including merged ones, you will import those articles as part of the source ticket conversation. This is the correct behavior — do not attempt to reconstruct the merge in Help Scout.

## What to Rebuild Manually

These Zammad features do not migrate via API and must be reconfigured in Help Scout by hand:

- **Triggers and automations** → Help Scout Workflows
- **Text modules (canned responses)** → Help Scout Saved Replies
- **SLA configurations** → Help Scout doesn't have native SLA timers; use tags and workflows to approximate
- **Macros** → No direct equivalent; Workflows or Saved Replies partially cover this
- **Role-based permissions** → Help Scout's permission model is simpler (Administrator, Owner, User); rebuild access controls accordingly
- **Channel configurations** (email, social) → Set up mailboxes, forwarding rules, and connected channels independently

## Structuring Your Migration Timeline

Do not attempt a "big bang" cutover where you run the entire script on a Friday night and hope for the best. A professional migration follows a phased approach:

1. **Data Profiling:** Query Zammad to get total counts of tickets, articles, users, and attachments. Identify multi-language KB content, tickets with 100+ articles, and attachments over 7.5 MB. Calculate your estimated API runtime using the duration table above.
2. **Sandbox Run:** Map a subset of data (e.g., 1,000 closed tickets) into a Help Scout trial account. Verify formatting, attachments, timestamps, thread ordering, and that no emails were sent to customers.
3. **Full Historical Sync:** Run the script for all closed tickets. This can take days depending on volume and should be done while your team is still actively working in Zammad. Use the checkpoint pattern from Step 6 throughout.
4. **Delta Sync & Cutover:** On cutover day, run a delta script that only queries Zammad for tickets updated since the Historical Sync began (`updated_at > {sync_start_timestamp}`). This takes minutes, enabling near-zero downtime.

A **delta sync** queries only records modified after a known timestamp — in Zammad, filter via `updated_at` on the ticket search endpoint. This limits cutover-day API load to hours-old changes rather than your full ticket history.

For more on delta syncs, see [Zero-Downtime Help Desk Data Migration](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/).

## When to DIY vs. When to Get Help

**DIY is reasonable if:**
- You have fewer than 5,000 tickets
- Your Zammad setup is standard (no heavy custom object attributes or multi-language KB)
- You have a developer who can commit 2–3 weeks to scripting, testing, and cutover
- You're comfortable with OAuth 2.0 token management, checkpoint patterns, and rate-limit handling

**Get help if:**
- You have 25,000+ tickets or complex data (organizations, custom fields, multi-language KB)
- You need zero downtime during cutover
- You've hit edge cases — tickets with 100+ articles, attachments over 7.5 MB, social channel data, GDPR data residency requirements
- Your team can't absorb 2–3 weeks of engineering time for a one-time project

> ClonePartner handles the custom scripting, attachment parsing, knowledge base migration, and delta syncs for Zammad to Help Scout migrations. Book a free 30-minute scoping call — no obligation.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Is there a native migration tool from Zammad to Help Scout?

No. Neither Zammad nor Help Scout provides a built-in migration tool or connector for this direction. You must use API-to-API scripting: extract via Zammad's REST API and load through Help Scout's Mailbox API 2.0 with the imported: true flag.

### Will Help Scout send emails to customers during migration?

Only if you forget the imported: true flag on thread creation requests. When imported is set to true, Help Scout suppresses all outgoing emails, prevents closed conversations from being re-opened, and stops data from skewing current-day reporting metrics.

### What happens to Zammad organizations in Help Scout?

Help Scout has no first-class Organization object. You must flatten Zammad organization names into the customer's company field and optionally use tags for organization-based filtering or grouping.

### How do I handle attachments larger than 7.5 MB?

Help Scout enforces a 10 MB limit per attachment, but base64 encoding adds ~33% overhead, making the effective raw file size limit about 7.5 MB. For files exceeding this, upload them to external storage (like AWS S3) and inject a download link into the Help Scout thread body.

### How long does a Zammad to Help Scout migration take?

It depends on ticket volume and your Help Scout plan's rate limits. At 400 requests/minute (Plus plan), 25,000 tickets with 5 articles each takes roughly 5–10 hours of API runtime. Total project time including scripting, testing, and validation is typically 1–3 weeks.
