Tutorial9 min readMay 14, 2025

Migrate from Zammad to FreeScout: Complete Guide (2025)

Step-by-step guide to migrate from Zammad (open-source) to FreeScout. Data export, import, and why FreeScout is simpler.

If you're on Zammad and finding it complex (Elasticsearch, PostgreSQL, Ruby), FreeScout offers a simpler alternative. This guide shows how to migrate without losing data.

Why Teams Switch from Zammad to FreeScout

Zammad is powerful but complex:

  • Requires 4GB+ RAM (Elasticsearch is memory-hungry)
  • Ruby on Rails is harder to customize than Laravel
  • More features than most teams use
  • Steeper learning curve

FreeScout is simpler:

  • Runs on 1GB RAM
  • Laravel is easier to understand
  • Modular (add features as needed)
  • Easier to customize

For teams that don't need omnichannel (SMS, WhatsApp, Twitter), FreeScout is overkill-free.


Migration Timeline

| Week | Activity | Hours | |---|---|---| | Week 1 | Set up FreeScout | 6–8 | | Week 2 | Export Zammad, import to FreeScout | 6–8 | | Week 3 | Parallel running (both systems) | 3–4 | | Week 4 | Cutover, training, Zammad shutdown | 2–3 |

Total: 3–4 weeks, 18–24 hours hands-on work.


Step 1: Set Up FreeScout (Week 1)

Choose an installation method:

Install, configure email, test. Don't add team members yet.


Step 2: Export Data from Zammad (Week 2)

Zammad stores data in PostgreSQL. You can export via API or direct database dump.

# Install Zammad API client
pip install requests

# Create export script
python3 << 'EOF'
import requests
import json

ZammadURL = "https://your-zammad.com"
Token = "your-api-token"

# Get all tickets
tickets = requests.get(
    f"{ZammadURL}/api/v1/tickets",
    headers={"Authorization": f"Token token={Token}"}
).json()

# Save to file
with open('zammad_tickets.json', 'w') as f:
    json.dump(tickets, f, indent=2)

print(f"Exported {len(tickets)} tickets")
EOF

This exports all tickets as JSON.

Option B: Database Dump (Advanced)

pg_dump -h your-db-host -U zammad_user zammad_production > zammad_backup.sql

Full database backup (recovery option if API export fails).

Option C: Manual CSV Export

Zammad can export to CSV:

  1. Go to Zammad Admin → Data Management
  2. Click "Export"
  3. Select "Tickets"
  4. Download CSV

Slowest but most reliable.


Step 3: Transform Data

Zammad and FreeScout have different data structures.

Zammad → FreeScout Mapping:

| Zammad | FreeScout | Notes | |---|---|---| | Ticket | Conversation | Direct mapping | | Article | Conversation Thread | Direct mapping | | Customer | Customer | Direct mapping | | Group | Mailbox | Partial (Zammad groups → FreeScout mailbox) | | Organization | Organization | Map if using | | Tags | Tags | Direct mapping | | Custom fields | Custom fields | Need to recreate |

Transform Script (Python)

import json
import csv
from datetime import datetime

# Read Zammad export
with open('zammad_tickets.json') as f:
    zammad_tickets = json.load(f)

# Transform for FreeScout import
freescout_conversations = []

for ticket in zammad_tickets:
    conversation = {
        'subject': ticket['title'],
        'customer_email': ticket['customer_email'],
        'customer_name': ticket['customer']['fullname'],
        'status': map_status(ticket['state']),
        'priority': map_priority(ticket['priority']),
        'created_at': ticket['created_at'],
        'updated_at': ticket['updated_at'],
        'messages': []
    }
    
    # Add ticket messages
    for article in ticket['articles']:
        conversation['messages'].append({
            'body': article['body'],
            'author': article['author']['fullname'],
            'created_at': article['created_at'],
            'is_customer': article['sender'] == 'Customer'
        })
    
    freescout_conversations.append(conversation)

# Save as JSON for import
with open('freescout_import.json', 'w') as f:
    json.dump(freescout_conversations, f, indent=2)

def map_status(zammad_status):
    mapping = {
        'new': 'open',
        'open': 'open',
        'pending': 'pending',
        'closed': 'closed',
        'merged': 'closed'
    }
    return mapping.get(zammad_status, 'open')

def map_priority(priority):
    mapping = {
        '1 low': 'low',
        '2 normal': 'medium',
        '3 high': 'high',
        '4 very high': 'urgent'
    }
    return mapping.get(priority, 'medium')

Step 4: Import into FreeScout

Create Mailbox First

  1. Settings → Mailboxes → Create Mailbox
  2. Name: "Migrated Tickets"
  3. Email: your support email
  4. Configure IMAP/SMTP

Import Script (Laravel/PHP)

<?php
// artisan tinker

$json = json_decode(file_get_contents('freescout_import.json'), true);
$mailbox_id = 1; // Your mailbox ID

foreach ($json as $conv_data) {
    // Create or get customer
    $customer = Customer::where('email', $conv_data['customer_email'])->first();
    if (!$customer) {
        $customer = Customer::create([
            'email' => $conv_data['customer_email'],
            'first_name' => explode(' ', $conv_data['customer_name'])[0],
            'last_name' => explode(' ', $conv_data['customer_name'])[1] ?? '',
        ]);
    }
    
    // Create conversation
    $conversation = Conversation::create([
        'mailbox_id' => $mailbox_id,
        'customer_id' => $customer->id,
        'subject' => $conv_data['subject'],
        'status' => $conv_data['status'],
        'priority' => $conv_data['priority'],
        'created_at' => $conv_data['created_at'],
        'updated_at' => $conv_data['updated_at'],
    ]);
    
    // Add messages
    foreach ($conv_data['messages'] as $msg) {
        ConversationThread::create([
            'conversation_id' => $conversation->id,
            'type' => $msg['is_customer'] ? 'customer' : 'agent',
            'body' => $msg['body'],
            'created_at' => $msg['created_at'],
        ]);
    }
}

echo "Import complete!";
?>

Run in FreeScout:

cd /var/www/freescout
php artisan tinker < import_script.php

Step 5: Validate Import

Check FreeScout for:

  • [ ] All tickets imported (count matches Zammad)
  • [ ] Subjects correct
  • [ ] Messages in order
  • [ ] Customers created
  • [ ] Dates preserved
  • [ ] Status mapping correct

Sample 20–30 tickets and compare to Zammad.


Step 6: Parallel Running (Week 3)

Run both systems for 1–2 weeks:

  1. Zammad: Read-only (no new tickets)
  2. FreeScout: All new tickets
  3. Temporary email: Point new requests to FreeScout

Step 7: Cutover (Week 4)

Pre-Cutover

  • [ ] All team trained on FreeScout
  • [ ] Email working (SMTP/IMAP tested)
  • [ ] Backups created
  • [ ] Queue workers running

Cutover Steps

  1. Export final tickets from Zammad (anything created during parallel period)
  2. Import to FreeScout
  3. Update email addresses everywhere (website, docs, auto-responder)
  4. Disable temporary email address
  5. Cancel Zammad

Cost Analysis

Zammad Annual Cost:

  • Hosting (4GB RAM VPS): $40–$80/month = $480–$960/year
  • Elasticsearch cluster (if managed): $50+/month = $600+/year
  • Database backups: $20–$50/month = $240–$600/year
  • Total: $1,320–$2,160/year

FreeScout Annual Cost:

  • Setup: $150 one-time
  • Hosting (1GB RAM VPS): $6–$12/month = $72–$144/year
  • Total: $222–$294/year

Year 1 Savings: $1,098–$1,938 Year 2+ Savings: $1,320–$2,160/year

Even if migration takes 30 hours at $50/hour ($1,500 cost), you break even in 1 year.


Zammad → FreeScout Decision Matrix

| Factor | Zammad | FreeScout | Winner | |---|---|---|---| | Omnichannel (SMS, WhatsApp) | ✅ Built-in | ❌ Limited | Zammad | | Simplicity | ❌ Complex | ✅ Simple | FreeScout | | Resource usage | ❌ 4GB+ | ✅ 1GB | FreeScout | | Cost | ❌ $1,320+/yr | ✅ $294/yr | FreeScout | | Email + chat only | ❌ Overkill | ✅ Perfect | FreeScout | | Customization | ⚠️ Ruby | ✅ Laravel | FreeScout |

Choose FreeScout if: You use email + chat, want simplicity and cost savings Stay on Zammad if: You need omnichannel support


Common Issues

| Issue | Cause | Fix | |---|---|---| | "Import fails" | JSON format wrong | Check script output, validate JSON | | "Emails don't match" | Formatting differences | Normalize: lowercase, trim whitespace | | "Status wrong" | Mapping incorrect | Update map_status() function | | "Messages out of order" | Timestamp issues | Verify created_at format |


Summary

Migrating from Zammad to FreeScout is straightforward:

  1. Set up FreeScout (6–8 hours)
  2. Export Zammad data (1–2 hours)
  3. Transform and import (2–4 hours)
  4. Test thoroughly (1–2 hours)
  5. Parallel run (1–2 weeks)
  6. Cutover (1 day)

Total effort: ~20 hours Payoff: $1,000+/year in savings

Want expert help migrating from Zammad to FreeScout?

We handle the full FreeScout installation on your server — SSL, email, security hardening, and a 1-hour onboarding call. Done in 24 hours.

One-time fee · 30-day support · Money-back guarantee

The complexity of Zammad might be worth it for omnichannel. For email + chat, FreeScout wins.

Resources

Need FreeScout Installed Professionally?

Skip the complexity. We install and configure FreeScout on your server in 24 hours — SSL, email, security, and a full onboarding call included.

Get It Done for $100

One-time fee · 30-day support · Money-back guarantee

Related Articles