Skip to content

Groove to Zammad Migration: A Technical Guide

A technical guide to migrating from Groove to Zammad. Covers API constraints, object mapping, extraction methods, attachments, and the edge cases that break migrations.

Rishabh Makhar Rishabh Makhar · · 24 min read
Groove to Zammad Migration: A 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

Core Architecture Differences

Before writing extraction scripts, understand how both systems store data. The gap between Groove's flat structure and Zammad's relational model dictates your entire migration logic.

Migrating from Groove to Zammad is an API-to-API data translation job. There is no native import wizard, no vendor-provided migration path, and no built-in connector between the two platforms. Zammad's documented migration options cover Freshdesk, Kayako, OTRS, and Zendesk — not Groove. Groove's admin export gives you conversations as JSON in its v1 format, while Zammad expects tickets and articles created through its REST API.

If you need ticket history, private notes, attachments, contacts, organizations, and mailbox context preserved, plan for a staged extract-transform-load workflow and a delta-sync cutover — not a weekend CSV dump.

This guide covers the data model differences, complete object-and-field mapping, API constraints on both sides, realistic migration methods with trade-offs, and the edge cases that break migrations.

Verified against: Groove REST API v1, Groove GraphQL API v2, and Zammad 6.x REST API as of mid-2025. Zammad's API can change between major versions — verify endpoint behavior against your installed version before writing migration scripts.

For related migration paths, see Groove to Zendesk Migration: Data Mapping, APIs & Rate Limits or Zammad to Zendesk Migration: The Technical Guide.

Warning

Scope check: This guide covers GrooveHQ shared inbox and helpdesk data moving into Zammad's ticket system. Knowledge base migration is handled separately because Groove has no bulk KB export and Zammad's KB API endpoints are a distinct surface. Reports, smart folders, rules, and channel-specific behavior need to be rebuilt rather than imported.

Groove's Data Model

Groove is a shared inbox platform built for small and midsize teams. Its data model is intentionally flat:

  • Conversations — the top-level container (equivalent to tickets)
  • Messages — individual replies and notes within a conversation
  • Customers — contact records with email, name, and custom fields
  • Agents — team members
  • Mailboxes — incoming email addresses
  • Folders — saved condition-based groupings of conversations
  • Groups — agent teams
  • Tags — labels on conversations
  • Attachments — files on messages

The Groove REST API exposes the Groove helpdesk's core resources — tickets, messages, customers, agents, mailboxes, folders, groups, and webhooks — for programmatic access. Groove's REST API is deprecated in favor of their newer GraphQL API, but the export feature still uses the v1 format.

Groove conversations also carry a merged_into field for tickets that were merged into another conversation. During extraction, check for this field explicitly. Merged conversations should not be migrated as standalone tickets — either skip them and preserve the merge context in the parent ticket's notes, or flatten them into a structured import note on the parent.

Zammad's Data Model

Zammad is a relational, open-source helpdesk with strict entity relationships:

  • Users — the foundational entity. Every action must be tied to a User. Zammad merges agents and customers into a single User model differentiated by role.
  • Organizations — company groupings for customers. Groove has no native equivalent.
  • Groups — permission buckets that dictate ticket visibility and routing (similar to Groove's Mailboxes, not Groove's Groups).
  • Tickets — hold metadata (status, priority, owner, group).
  • Articles — individual communications within a ticket: email, note, phone, web. Articles represent individual communications or notes within a ticket, and can contain file attachments or inline images.
  • Tags — labels on tickets.
  • Knowledge Base — categories, articles, and translations. KB articles are locale-bound — every article belongs to a specific locale, and articles created without an explicit locale are invisible in the UI.

"API First" applies — you can do anything with the REST API that can be done through the interface.

Structural Gap Summary

Groove Entity Zammad Equivalent Notes
Conversation Ticket 1:1 mapping. Preserve Groove conversation ID in a custom field.
Message Article Messages become Articles; type must be set explicitly
Customer User (role: Customer) Zammad merges agents and customers into a single User model
Agent User (role: Agent) Same User model, different role
Mailbox Group No direct 1:1; map to Groups for routing and permissions
Folder Overview or Tag Zammad Overviews are dynamic filter-based views, not data containers
Group (Groove) Group membership Different semantics — Groove Groups are agent collections, Zammad Groups carry queue behavior
Tag Tag Direct mapping
Attachment Attachment Must be base64-encoded for Zammad's API
Company Organization Groove has no strong org entity; you may need to create these fresh
Merged conversation Import note on parent ticket Do not create as standalone ticket
KB Article KB Answer Locale-bound in Zammad; separate migration required
Info

Key distinction: Groove Folders are organizational containers that hold conversations. Zammad Overviews are dynamic, filter-based views — they don't "contain" tickets. If your team uses Groove Folders as a core workflow mechanism, convert them to Tags or a custom field in Zammad to preserve that grouping logic. The folder name becomes the tag value; apply it to every ticket that was in that folder during extraction.

Extracting Data from Groove

You have two extraction paths: the built-in export or the API.

Built-in JSON Export

Groove provides a full conversation export in JSON format through its admin settings. There is no CSV export option — all data comes as JSON matching Groove's v1 Tickets API format.

To trigger it:

  1. Go to Settings → Company → More → Exports.
  2. Click Request Export.
  3. Groove processes exports in a FIFO queue shared with other customers, so timing varies — small accounts finish in minutes, while large accounts can take up to 72 hours.
  4. You can only have one active export request at a time.

The export includes ticket metadata (status, assignee, tags, custom fields), all messages in each conversation thread, and customer contact information.

What the export does NOT include:

  • Groove's knowledge base doesn't have a bulk article export feature.
  • Agent configuration and permissions
  • Folder definitions (capture these separately via API)
  • Webhook and integration configurations

For historical migrations, this JSON export is the most reliable method. It bypasses API rate limits and provides a complete snapshot. The downside: the JSON file can be massive (often gigabytes for large accounts), requiring you to stream-parse rather than loading entirely into RAM. Use jq or a streaming JSON parser in Python/Node.js.

Groove REST API pagination limit: Some endpoints silently cap at page 50 with a maximum per_page of 50, meaning you can retrieve at most 2,500 records via paginated REST calls before hitting the ceiling. For accounts with more than 2,500 conversations, use the JSON export or the GraphQL API — do not rely solely on paginated REST calls or you will silently miss records.

Tip

For the most reliable extraction, use the built-in JSON export for bulk ticket/message/customer data, and supplement with API calls for entities not included in the export (folders, groups, mailboxes, KB articles). This hybrid approach minimizes API calls and avoids rate-limit headaches.

API-Based Extraction

Developers can integrate via the v2 GraphQL API or the legacy v1 REST API, both authenticated with an API key obtained from account settings.

REST v1 still documents tickets, messages, and attachments with page and per_page pagination (maximum page size of 50):

# List all tickets (paginated)
GET https://api.groovehq.com/v1/tickets?page=1&per_page=50
 
# Get messages for a specific ticket
GET https://api.groovehq.com/v1/tickets/{ticket_number}/messages
 
# List all customers
GET https://api.groovehq.com/v1/customers?page=1&per_page=50
 
# List all agents
GET https://api.groovehq.com/v1/agents

GraphQL v2 is the better fit when you need conversations, contacts, companies, tags, team assignments, and event metadata in a controlled schema. It is also the right tool for delta syncs — fetching only conversations created or updated since your initial export. (A delta sync is a second extraction pass that pulls only records modified after the primary migration began, used to catch tickets handled during the migration window before cutover.)

query GetConversations($cursor: String) {
  conversations(first: 50, after: $cursor) {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        id
        subject
        mergedInto {
          id
        }
        customer {
          email
          name
        }
        messages(first: 100) {
          edges {
            node {
              body
              createdAt
              author {
                ... on User {
                  email
                }
              }
            }
          }
        }
      }
    }
  }
}

Note the mergedInto field in the query above. Pull this for every conversation and filter out merged children before migration.

Be aware of query complexity limits. Requesting deeply nested messages and attachments for hundreds of conversations in a single query will time out. Groove's docs also warn that Inbox and Knowledge Base APIs are still being built, so some projects still fall back to REST v1 for parts of the workload.

API rate limits apply, so large extractions may need throttling. Groove's REST API does not publish explicit rate-limit numbers. In practice, implement exponential backoff on HTTP 429 responses and keep requests under ~60/minute.

Loading Data into Zammad

Zammad's REST API (/api/v1/) is the primary import path. The interface itself is just an API client written in JavaScript that runs in the browser. Everything the UI can do, the API can do.

Zammad Import Mode

Before running any bulk import, enable Zammad's import mode. This flag suppresses outbound email notifications and affects timestamp handling during ingestion:

# Enable import mode via Rails console (self-hosted)
bundle exec rails r "Setting.set('import_mode', true)"
 
# Disable import mode after migration is complete
bundle exec rails r "Setting.set('import_mode', false)"

Import mode does two things: it prevents Zammad from sending notification emails to customers and agents as you create historical tickets, and it relaxes certain timestamp constraints so that created_at values from the source system are respected. Without import mode enabled, your Zammad instance may send hundreds or thousands of notification emails to customers during bulk import — which is the most visible and damaging failure mode in helpdesk migrations.

Danger

Enable import mode before your first ticket POST. Disabling it partway through a migration does not retroactively suppress notifications for tickets already created. For Zammad SaaS (hosted), contact Zammad support to confirm whether import mode is accessible and how to enable it on your instance.

Authentication

Zammad's REST/JSON API supports three different authentication methods. For migration scripts, Token authentication is recommended:

curl -H "Authorization: Token token=YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     https://your-zammad.example.com/api/v1/tickets

Create a dedicated migration token with ticket.agent and admin permissions. There are special API tokens that make it possible to further restrict access rights. This minimizes the attack surface if a token is compromised. Revoke the token immediately after migration.

Creating Custom Object Attributes

If Groove conversations have custom fields, create matching Object Attributes in Zammad before importing any tickets. Attempting to set a field that does not exist as an Object Attribute will cause the API to silently drop the value or return a validation error.

POST /api/v1/object_manager_attributes
{
  "object": "Ticket",
  "name": "groove_conversation_id",
  "display": "Groove Conversation ID",
  "data_type": "input",
  "data_option": {
    "type": "text",
    "maxlength": 255,
    "null": true,
    "default": ""
  },
  "active": true,
  "screens": {
    "view": { "shown": true },
    "edit": { "shown": false }
  }
}

After creating Object Attributes via the API, you must trigger a migration to activate them:

POST /api/v1/object_manager_attributes/execute_migrations

Create at minimum: groove_conversation_id, groove_mailbox, and any Groove custom field you need to preserve for search or reporting.

Creating Users and Organizations

Your first ingestion step is translating Groove Customers and Agents into Zammad Users.

Zammad requires a login or email for every user and strictly enforces unique email addresses. Groove allows duplicate customer records with the same email in certain edge cases. Your script must deduplicate before import or the Zammad API will return 422 Unprocessable Entity errors.

For agents, assign the correct roles array (e.g., ["Agent"]). If your agents' emails have changed between systems, build a mapping table before starting — unmatched agents will cause tickets to be unassigned or assigned to the API token's user. Groove Lite Users (who can view conversations and add notes but cannot reply or be assigned) should not automatically become full Zammad agents — create them as Customer-role users or omit them from the migration entirely.

Groove has no native organization entity. If Groove customers have a company_name field, create Organizations in Zammad first and associate users. Zammad supports primary and secondary organizations, but keep secondary-org usage reasonable — a few dozen at most per user.

Use guess:{email address} to save an API call if you don't know the user's ID or want to create the user in question. This auto-creates customer records on the fly during ticket import.

Translating Mailboxes to Groups

Groove Mailboxes (e.g., "Support", "Billing") map to Zammad Groups. Create these Groups before migrating tickets — you need the Zammad group_id to route imported tickets correctly. In Zammad, groups can be nested using :: in the name, and the group object is where email address linkage and follow-up behavior live.

Tip

Preserve source IDs explicitly. Store Groove conversation IDs, contact IDs, company IDs, and mailbox names in Zammad custom Object Attributes from the first import batch. Do not rely on subject lines or ticket numbers to find records later.

Creating Tickets with the First Article

You must perform the API import using a 2-step process — first create the ticket, then create all the articles. Zammad's ticket creation endpoint accepts one initial article. Every subsequent message from Groove must be added as a separate article.

Step 1: Create the ticket with the first message

POST /api/v1/tickets
{
  "title": "Help with billing issue",
  "group": "Support",
  "customer_id": "guess:customer@example.com",
  "state": "closed",
  "priority": "2 normal",
  "tags": "groove-import,billing",
  "created_at": "2024-03-15T10:00:00Z",
  "updated_at": "2024-03-16T12:00:00Z",
  "groove_conversation_id": "12345",
  "article": {
    "subject": "Help with billing issue",
    "body": "<p>I need help updating my credit card.</p>",
    "content_type": "text/html",
    "type": "email",
    "sender": "Customer",
    "from": "customer@example.com",
    "internal": false,
    "created_at": "2024-03-15T10:00:00Z"
  }
}
Warning

Timestamp preservation: To maintain original creation dates, authenticate against the Zammad API using an Admin token. This lets you pass created_at and updated_at timestamps. Without Admin privileges, Zammad stamps all imported records with the current date. Prove this works in a sandbox on your exact Zammad version before starting the production import.

Step 2: Add remaining articles in chronological order

For every reply or private note in the Groove conversation, make a separate POST to /api/v1/ticket_articles:

POST /api/v1/ticket_articles
{
  "ticket_id": 123,
  "body": "<p>Agent reply from Groove</p>",
  "content_type": "text/html",
  "type": "email",
  "sender": "Agent",
  "internal": false,
  "from": "agent@company.com",
  "origin_by_id": 456,
  "created_at": "2024-03-15T10:30:00Z"
}

Article Authorship: origin_by_id vs. created_by_id

This distinction matters for historical fidelity and is a common source of import errors.

  • created_by_id — the Zammad user who made the API call. In a migration, this will always be your migration service account. Setting this field explicitly is not supported via the public API — it reflects who performed the write.
  • origin_by_id — the Zammad user ID of the person the article represents (i.e., the original author). Set this to the migrated agent's Zammad user ID to make the article appear as authored by that agent in the UI.

If an agent existed in Groove but has since left and you did not create a Zammad user for them, origin_by_id cannot be set to a non-existent ID — Zammad will return a 422 error. Options: create a deactivated Zammad user for every departed agent before migration, or fall back to populating the from field with their name and email as a string while leaving origin_by_id pointing to a generic "Former Agent" user.

Mapping Groove message types to Zammad article types:

Groove Message Type Zammad type internal sender
Customer reply (inbound) email false Customer
Agent reply (outbound) email false Agent
Private note note true Agent
System/automation note note true System
Danger

Never test historical article replay against a live outbound mailbox. Zammad's docs warn that internal: true does not make an email-type article silent — if you create an article with type: email, that email can still be sent. Do test imports with outbound mail disabled or use type: note during history replay to avoid generating duplicate customer mail. Import mode (described above) suppresses this, but verify it is active before proceeding.

Field-Level Mapping Reference

Ticket / Conversation Mapping

Groove Field Zammad Field Transform
number Custom field groove_conversation_id Store as reference; Zammad auto-generates ticket numbers
summary / subject title Direct map
status (opened, pending, closed, spam) state (new, open, pending reminder, closed) Translate enum values; no native spam state — map to closed + tag spam
priority priority_id Groove has no native priority; map all to Zammad's 2 normal as default
assigned_agent owner_id Look up agent's Zammad user ID; null if agent not found
mailbox group Map mailboxes to Zammad Groups pre-migration
tags tags Direct map; pass as comma-separated string
created_at created_at ISO 8601; requires admin token + import mode
updated_at updated_at ISO 8601
customer.email customer_id Use guess:{email} for auto-resolution
merged_into Skip (note on parent) Do not migrate merged children as standalone tickets

Message → Article Mapping

Groove Field Zammad Field Transform
body body HTML content; sanitize before import
author.email from + origin_by_id Look up Zammad user ID for origin_by_id; use email string for from
note (boolean) internal true if private note; use type: note not type: email for internals
Message direction sender "Customer" for inbound, "Agent" for outbound
Message direction type "email" for email threads, "note" for internal notes
created_at created_at Preserve original timestamp; null → parent ticket created_at
attachments attachments Download from Groove, base64-encode, attach to article

Customer → User Mapping

Groove Field Zammad Field Transform
email email Direct map; deduplicate first
first_name firstname Direct map
last_name lastname Direct map
phone_number phone Direct map
company_name organization Create Organization in Zammad first
Custom fields Custom Object Attributes Must create and activate matching attributes before import

Handling Attachments and Inline Images

Attachments are consistently the highest-risk component of any helpdesk migration.

Standard Attachments

Groove stores attachments as URLs. Zammad expects them as base64-encoded strings in the article payload. For every file attached to a Groove message:

  1. Make a GET request to the Groove attachment URL.
  2. Download the file into memory.
  3. Base64 encode the buffer.
  4. Append it to the attachments array in the Zammad article payload.
"attachments": [
  {
    "filename": "invoice.pdf",
    "data": "JVBERi0xLjQK...",
    "mime-type": "application/pdf"
  }
]

Watch the mime-type field name — it is hyphenated, not underscored. Using the wrong format for this field causes errors like undefined method 'each_with_index' for "mime-type":String. Send the payload as proper JSON with Content-Type: application/json, not form-encoded.

If you are hitting Zammad's hosted version, check the maximum request body size. Large attachments (>10 MB) may need special handling. Self-hosted limits depend on your nginx/Apache configuration and Rails settings.

The Inline Image Problem

Standard attachments are straightforward. Inline images — images pasted directly into the body of an email — are much harder.

Groove's message body will contain HTML like <img src="https://groovehq.com/attachments/12345">. If you push this raw HTML into Zammad, the images will initially render because the browser loads them from Groove's servers. Once you cancel your Groove account, those URLs will 404, and your historical tickets will be filled with broken images.

To fix this, your script must:

  1. Parse the HTML body of every Groove message.
  2. Extract all src URLs pointing to Groove's domains.
  3. Download those images and base64 encode them.
  4. Upload them to Zammad as standard attachments on the same article.
  5. Rewrite the src attribute in the HTML body to reference the new Zammad attachment ID or use cid: Content-ID mapping for inline rendering.
Danger

Do not skip inline image processing. It is computationally expensive and slows down the migration script, but failing to rewrite inline image URLs results in permanent data loss once the source system is decommissioned.

HTML Sanitization

Zammad's sanitizer will strip or escape certain HTML constructs. Known problematic patterns based on production migrations:

  • <script> tags — always stripped
  • style attributes with position: absolute or position: fixed — stripped
  • <iframe> elements — stripped
  • Embedded data: URIs in <img src> — behavior varies by Zammad version; test explicitly
  • Non-standard attributes on standard elements — may be stripped silently

Before running the full migration, extract 20–30 representative message bodies from Groove, push them through the Zammad API, and compare the output using a diff tool. Sanitization issues are easy to catch early and impossible to fix at scale without re-importing.

Rate Limits, Performance, and API Constraints

Zammad API Limits

Zammad has hard limits for the maximum returned objects. You can't raise these limits. The default page size is 100 objects. For import, this mainly affects lookups when you need to find or verify users, organizations, or groups before creating tickets.

For self-hosted Zammad, rate limits are configurable and throughput is bound by server CPU, RAM, and database I/O. For Zammad SaaS, Zammad applies rate limits to API requests to protect against abuse and ensure fair usage. Implement retry logic with exponential backoff for HTTP 429 responses.

Bulk Import Endpoint

Zammad's /api/v1/import endpoint exists but is scoped to specific vendor import formats (Zendesk, Freshdesk, OTRS) and does not accept Groove's data format. Do not attempt to use it for a Groove migration — it will reject the payload. The ticket + article creation approach via /api/v1/tickets and /api/v1/ticket_articles is the correct path.

Performance Tuning for Self-Hosted Zammad

  • Disable Elasticsearch indexing during import. Indexing every ticket upon creation during a bulk import will heavily tax your server. Temporarily pause indexing, run the migration, and trigger a full reindex via the Rails console once data is loaded:

    bundle exec rails searchkick:reindex CLASS=Ticket
    bundle exec rails searchkick:reindex CLASS=TicketArticle
    bundle exec rails searchkick:reindex CLASS=User
  • Batch user creation. While tickets must be created sequentially to maintain timeline integrity, Users and Organizations can be created in parallel.

  • Handle 429s gracefully. If Zammad returns a 429 Too Many Requests, sleep for the duration specified in the Retry-After header before retrying.

  • Run migration off-peak. Database I/O during bulk insert can degrade live agent performance on shared infrastructure. Schedule the primary sync for overnight or weekend hours.

Groove API Limits

Groove's REST API does not publish explicit rate-limit numbers. Keep requests under ~60/minute and implement exponential backoff on HTTP 429 responses. REST v1 silently caps at page 50 (2,500 records max) — use GraphQL or the JSON export for accounts above this threshold.

Edge Cases and Failure Modes

Migration failures fall into three categories: data integrity failures (source data does not map cleanly), API constraint failures (the target system rejects the payload), and configuration failures (the Zammad instance is not set up to receive the data correctly). Each has different remediation.

Data Integrity Failures

  1. Duplicate customers. Groove allows duplicate customer records with the same email. Zammad enforces email uniqueness. Deduplicate before import or rely on guess:{email} to collapse duplicates — but verify the data that gets collapsed.

  2. Messages with no created_at. Some very old Groove accounts have messages with null timestamps. Zammad requires a timestamp — default to the parent ticket's created_at as a fallback.

  3. Merged conversation chains. Groove's merged_into field identifies conversations that were merged into another. Do not migrate these as standalone tickets — flatten into a structured import note on the parent ticket with the original conversation ID and message summary preserved.

  4. Event history beyond messages. Groove exposes event types beyond simple messages — merges, snoozes, tag changes, team changes, ratings, follows. Zammad's core imported timeline unit is the article. Create a single internal type: note article per ticket that records the structured event history (as JSON or formatted text) rather than trying to fake every source event as an article.

  5. Agent matching failures. Groove identifies agents by email. If agents' emails have changed, build a mapping table before starting. For departed agents, create deactivated Zammad users so origin_by_id references remain valid.

API Constraint Failures

  1. Conversation state mapping. Groove uses opened, pending, closed, and spam. Zammad's default states are new, open, pending reminder, pending close, and closed. There is no native spam state in Zammad — map it to closed and tag with spam for filtering.

  2. HTML sanitization stripping content. Zammad's sanitizer removes <script>, <iframe>, position: absolute/fixed styles, and certain non-standard attributes. Run a sample diff before full import.

  3. Attachment size limits. On Zammad's hosted version, check the maximum request body size. Self-hosted limits depend on nginx/Apache and Rails max_body_size configuration.

  4. Custom Object Attributes not yet migrated. Attempting to set a field that does not exist as an activated Object Attribute will cause the API to silently drop the value. Create and activate all attributes before importing tickets.

Configuration Failures

  1. Import mode not enabled. Zammad sends live notification emails for every ticket and article created unless import mode is active. Enable it before the first POST.

  2. Ticket number collision. Zammad auto-generates ticket numbers. Do not try to force Groove's ticket numbers into Zammad's numbering system — store them as a custom field for cross-reference.

  3. Folders don't exist in Zammad. Groove Folders are static containers. Zammad's closest equivalent is Overviews (dynamic filter-based views) or Tags. Convert folders to tags during migration so the grouping survives.

  4. Groove Lite Users. Groove Lite Users can view conversations, add notes, and use mentions, but cannot reply or be assigned conversations. Do not automatically create them as full Zammad agents — create as Customer-role users or deactivated agents.

  5. KB locale not specified. Zammad KB articles are locale-bound. Articles created without specifying a locale are invisible in the UI. Always pass the locale code (e.g., "locale": "en-us") when creating KB articles via the API.

Knowledge Base Migration

Groove's knowledge base doesn't have a bulk article export feature. If you've built help articles in Groove's knowledge base, copy each article's content manually or use the API to retrieve articles programmatically.

The practical path:

  1. Extract from Groove using the GraphQL API or REST API to pull each KB article's title, body (HTML), category, and locale metadata.
  2. Create Zammad KB structure — enable the knowledge base, create categories via the API at /api/v1/knowledge_bases/{kb_id}/categories.
  3. Create articles with explicit locale — Zammad KB articles are locale-bound. Every article must be created under a specific locale (e.g., en-us). Use the KB article endpoint and always pass "locale": "en-us" (or your target locale). Articles created without a locale specifier are not displayed in the UI and cannot be found via the KB search.
  4. Migrate images separately — download from Groove, re-upload as attachments in Zammad KB articles.

To create an API token, go to your Zammad profile, open Token Access, and create a token with the Knowledge Base reader or editor permission. This is a separate permission scope from ticket management.

Info

If you have fewer than ~50 KB articles, manual copy-paste into Zammad's KB editor is often faster than scripting it. For larger KB volumes, script the extraction and creation, but treat KB migration as its own workstream with its own validation pass — do not bundle it into the ticket migration script.

Migration Methods: Honest Trade-offs

Write a script in Python, Ruby, or Node.js that reads the Groove JSON export (or pulls via API) and pushes to Zammad's REST API.

Pros:

  • Full control over field mapping and transformation logic
  • Can handle edge cases specific to your data
  • Repeatable — run against a test Zammad instance first

Cons:

  • Requires engineering time (typically 2–5 days depending on volume and complexity)
  • You own the error handling, retry logic, and validation
  • Attachment migration is slow due to base64 encoding overhead

Third-Party Migration Tools

Services like Help Desk Migration offer Groove as a source. Zammad is less commonly supported as a target due to its self-hosted nature and smaller market footprint. Verify target support before purchasing.

Pros:

  • Lower upfront engineering effort
  • Built-in field mapping UI

Cons:

  • Limited customization of transform logic
  • May not support Zammad's full data model (Organizations, KB, custom Object Attributes)
  • Self-hosted Zammad instances may require network access configuration
  • Import mode and Elasticsearch indexing pauses typically cannot be coordinated through third-party tools

Direct Database Import (Self-Hosted Only)

If you run Zammad on your own infrastructure, you can write directly to PostgreSQL/MySQL. This bypasses the API entirely.

Danger

We strongly advise against direct database imports. Zammad's internal ID relationships, caching layers, and Elasticsearch indexing depend on records being created through the application layer. Direct DB writes produce orphaned records in the search index, break ticket counters maintained in Redis, and corrupt the histories table that powers Zammad's timeline view. The API is the supported path and the only one that produces a consistent system state.

Engineer-Led Migration Service

Hand the project to a team that has done this before. This is the right call when your data is messy, volume is large, timeline is tight, or you cannot afford to get it wrong.

The Cutover Strategy

Migrating helpdesks is not a one-time script execution. It requires an orchestrated cutover to keep support running.

  1. Primary sync: Run the migration script to move your historical data. This can take days depending on attachment volume. Your team continues working in Groove during this phase.
  2. Freeze configuration: Stop making changes to Groups, Tags, and Users in both systems.
  3. DNS and email routing: Lower the TTL on your DNS records (to 60–300 seconds) at least 48 hours before cutover. Update email forwarding rules to route incoming mail to Zammad.
  4. Delta sync: Run a final script using Groove's GraphQL API to fetch only conversations created or updated since the primary sync began. This catches tickets handled during the primary migration window.
  5. Validation: Spot-check complex tickets. Verify private notes remained internal (type: note, internal: true), attachments open correctly, and timestamps reflect the original interactions. Compare ticket and article counts between source and target using the APIs, not just the UI.
  6. Enable live outbound: Only after validation should you enable live outbound email behavior on Zammad and disable import mode.
  7. Elasticsearch reindex: Trigger a full reindex after migration completes and import mode is off:
    bundle exec rails searchkick:reindex CLASS=Ticket
    bundle exec rails searchkick:reindex CLASS=TicketArticle

For a deeper look at zero-downtime strategies, see Zero-Downtime Help Desk Data Migration.

Time and Cost Estimation

Data Volume Script Development Migration Runtime Total Elapsed
< 5,000 tickets 2–3 days 1–4 hours ~1 week
5,000–50,000 tickets 3–5 days 4–24 hours 1–2 weeks
50,000–200,000 tickets 5–8 days 1–3 days 2–4 weeks
> 200,000 tickets 8+ days 3+ days 4+ weeks

These estimates assume a single engineer working with the API at moderate throughput (~60–120 requests/minute against Zammad). Attachment-heavy accounts run slower due to base64 encoding and payload size. Inline image rewriting adds 30–50% to migration runtime compared to text-only ticket data.

Migration Checklist

  • Audit Groove data: Count tickets, messages, customers, KB articles, attachment volume, and merged conversations
  • Export from Groove: Request the JSON export; supplement with API pulls for missing entities; use GraphQL for accounts with >2,500 conversations
  • Set up Zammad: Create Groups (mapped from Mailboxes), Organizations, agent User accounts
  • Create and activate custom Object Attributes: groove_conversation_id, groove_mailbox, and any Groove custom fields before importing tickets
  • Enable import mode: bundle exec rails r "Setting.set('import_mode', true)"
  • Disable Elasticsearch indexing on self-hosted instances before bulk import
  • Build the mapping table: Agent email → Zammad user ID, Groove status → Zammad state, Groove folder → Zammad tag, departed agents → deactivated Zammad users
  • Write and test the migration script: Run against a test Zammad instance first — never import directly into production on the first run
  • Validate sample: Push 50–100 representative tickets; diff article bodies for sanitization issues; verify attachments, timestamps, and origin_by_id authorship
  • Run full migration: Primary sync with Groove import mode on
  • Delta sync: Pull Groove conversations updated since primary sync started
  • Migrate knowledge base: Extract from Groove, create category structure in Zammad with explicit locale codes, push articles
  • Cut over: Point support email to Zammad, disable Groove channels, verify incoming ticket flow
  • Re-enable Elasticsearch indexing and run full reindex
  • Disable import mode: bundle exec rails r "Setting.set('import_mode', false)"
  • Clean up: Revoke the migration API token, archive the Groove export, document the mapping

When to Build vs. When to Outsource

Build it yourself if your data is clean, volume is under 50,000 tickets, your engineering team has API migration experience, and all agents are still active with unchanged email addresses. This is a tractable problem for a mid-level backend engineer with 3–5 days of focused time.

Bring in help if:

  • Your Groove data has years of history with inconsistent formatting or significant merged ticket chains
  • You need to merge data from multiple Groove accounts into one Zammad instance
  • Departed agents need deactivated Zammad users created and mapped before import
  • Downtime is not an option and you need a tested cutover plan
  • Your team does not have bandwidth to build, test, and validate a migration script

At ClonePartner, we have completed 1,500+ help desk migrations. Groove-to-Zammad is a pattern we handle from flat data extraction through validated import with full timestamp, attachment, and article-authorship fidelity. If you would rather hand this off and focus on configuring Zammad for your team, we can typically scope and complete it within days.

Frequently Asked Questions

Is there a native Groove to Zammad migrator?
No. Zammad's official migration sources are Freshdesk, Kayako, OTRS, and Zendesk. Groove projects require a custom migration script using Groove's JSON export or API and Zammad's REST API, or an engineer-led migration service.
Does Groove export data as CSV or JSON?
Groove exports conversation data as JSON only, matching its v1 Tickets API format. There is no CSV export option. The export includes ticket metadata, messages, and customer information but does not include knowledge base articles.
How do I preserve timestamps when importing tickets into Zammad?
Pass the original created_at value from Groove in ISO 8601 format when creating tickets and articles via Zammad's REST API. An admin-level API token is required to set custom timestamps. Omitting this field causes Zammad to stamp records with the current server time.
How long does a Groove to Zammad migration take?
For accounts under 5,000 tickets, expect about one week including script development and testing. For 50,000+ tickets, plan for 2–4 weeks. Attachment-heavy accounts take longer due to base64 encoding overhead in Zammad's API.
Can I migrate Groove knowledge base articles to Zammad?
Yes, but there is no bulk export or import path. Extract articles from Groove via API, create the category structure in Zammad's knowledge base, and push articles through Zammad's KB API endpoints. For fewer than 50 articles, manual copy-paste is often faster than scripting.

More from our Blog