Skip to content

The Complete Guide to Migrating from Help Scout to Freshdesk

Help Scout to Freshdesk migration made simple. Learn how to move conversations, customers, agents, knowledge base content, and automations correctly.

Tejas Mondeeri Tejas Mondeeri · · 12 min read
The Complete Guide to Migrating from Help Scout to Freshdesk
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

Migrating from Help Scout to Freshdesk requires moving conversations, contacts, knowledge base articles, and operational config between two systems with different data models. The bridge is the API. Migrating via API gives you granular control over what moves and how it looks when it arrives, but it requires a strict sequence to maintain referential integrity.

This guide covers how to map your data from Help Scout to Freshdesk, the exact order you must follow, the field-level mappings you need, and the edge cases that will break your migration if you ignore them.

Define Your Migration Scope

Before you write a single line of code, you need to categorize your data. Not everything can or should be moved in the same way.

  • API Migration: The vast majority of your data has a direct path from Help Scout to Freshdesk.

You will use the API to migrate your core "people" data, which includes your Users (agents), Customers, and Organizations.

You will also migrate your operational data, such as Inboxes, Teams, Conversations (tickets), and Saved Replies.

Finally, your content, specifically your Docs (knowledge base), has a clear home in Freshdesk.

  • Manual Configuration: Some logic simply does not translate through code.

Workflows in Help Scout and Automations in Freshdesk use different underlying logic structures. While Freshdesk has an API for automation rules, trying to programmatically translate conditions from one platform to another often leads to broken rules. It is safer and cleaner to rebuild these manually.

Similarly, Docs Redirects, the links that ensure old articles point to new ones, must be configured manually as there is no dedicated endpoint for this in the destination system.

  • Archive: You should also decide if you want to migrate absolutely everything.

API rate limits apply to both systems, so if you have tickets from five years ago that provide no value, consider archiving them locally rather than spending valuable time moving them.

Prepare Freshdesk for Data Import

Before you start pumping data into Freshdesk, you must prepare the environment to receive it. If you try to create a ticket for a specialized mailbox that doesn't exist yet, the API will reject it.

Start by creating your Email Mailboxes in Freshdesk to match your Help Scout Inboxes. This ensures that when you import tickets later, they land in the right queue immediately.

Next, you need to recreate your schema. Help Scout Custom Fields need to be recreated in Freshdesk before the data arrives.

Freshdesk supports custom fields for Tickets, Contacts, and Companies. If you migrate a ticket that has a custom field value, but that field hasn't been defined in Freshdesk yet, the API will silently drop that value — no error, just missing data.

Core Object Mapping

Before diving into the migration sequence, here is the complete field-level mapping between the two systems. This is the reference your migration script should be built against.

Agents and Groups

Help Scout Field Freshdesk Field Notes
User id Agent id New ID assigned by Freshdesk. Store mapping locally.
User email Agent email Used as the natural key for deduplication.
User firstName + lastName Agent name Concatenate with a space.
User role (owner, admin, user) Agent role Map owner → Account Admin, admin → Admin, user → Agent. Roles do not map 1:1 — verify permissions post-migration.
Team id Group id New ID assigned. Store mapping.
Team name Group name Direct string transfer.

Companies and Contacts

Help Scout Field Freshdesk Field Notes
Organization id Company id New ID assigned. Store mapping.
Organization name Company name Direct string transfer.
Customer id Contact id New ID assigned. Store mapping.
Customer firstName + lastName Contact name Concatenate.
Customer email Contact email Primary email becomes the Freshdesk unique identifier.
Customer organization Contact company_id Use the Organization → Company ID mapping you stored.
Customer phones [] Contact phone Freshdesk supports one phone field by default. If a customer has multiple numbers, only the primary transfers cleanly — additional numbers require custom fields.

Tickets

Help Scout Field Freshdesk Field Notes
Conversation id Ticket id New ID assigned. Store mapping.
Conversation subject Ticket subject Direct string transfer.
Conversation status Ticket status See status mapping table below.
Conversation assignee Ticket responder_id Use the User → Agent ID mapping.
Conversation mailboxId Ticket group_id or email_config_id Depends on whether you map inboxes to groups or mailboxes.
Conversation createdAt Ticket created_at ISO 8601. Freshdesk accepts this on ticket creation.
Conversation tags [] Ticket tags [] Both systems treat tags as arrays of strings. Direct transfer. But see the Tags section below.
Conversation customFields Ticket custom fields Field names must match exactly (or be mapped). Fields must exist in Freshdesk before import.

Status Mapping

This is the mapping your script must implement. There is no automatic translation.

Help Scout Status Freshdesk Status (integer) Freshdesk Label
active 2 Open
pending 3 Pending
closed 5 Closed
spam 5 Closed (no spam status in Freshdesk — flag or discard)

Help Scout's pending status means "waiting on customer." Freshdesk's 3 (Pending) means the same. If you use Freshdesk's waiting on third party status, that is status 6 on plans that support it.

Priority Mapping

Help Scout does not have a native priority field on conversations. If you use a custom field for priority, map it to Freshdesk's integer values:

Priority Freshdesk Value
Low 1
Medium 2
High 3
Urgent 4

If no priority is set, Freshdesk defaults to 1 (Low) when you omit the field.

Knowledge Base

Help Scout Object Freshdesk Object Notes
Collection Solution Category Top-level container.
Category Solution Folder Second-level container within a Category.
Article Solution Article HTML body transfers directly. See edge cases below.
Article status (published/draft) Article status Map accordingly. Freshdesk uses 1 for draft, 2 for published.

Threads → Conversations

Help Scout Field Freshdesk Field Notes
Thread (type: customer) Conversation (reply) Use POST /api/v2/tickets/{id}/reply.
Thread (type: message) Conversation (reply) Agent-initiated thread. Same endpoint.
Thread (type: note) Conversation (note) Use POST /api/v2/tickets/{id}/notes. Set private: true.
Thread body Conversation body HTML content.
Thread createdAt Conversation created_at Freshdesk accepts backdated timestamps on replies/notes.
Thread attachments [] Conversation attachments Must be uploaded as multipart/form-data. See Attachments section.

Migrate Objects

This is the core of the operation. The order in which you move objects is critical because of dependencies. You cannot assign a ticket to an agent who doesn't exist, and you cannot link a contact to a company that hasn't been created yet.

  1. Agents and Groups: Start by migrating your people.

Help Scout Users become Freshdesk Agents. Use GET /v2/users to pull from Help Scout and POST /api/v2/agents to create in Freshdesk. Simultaneously, move your Teams over (GET /v2/teamsPOST /api/v2/groups), which map directly to Freshdesk Groups.

By establishing these first, you ensure that every subsequent object you import has a valid owner.

  1. Companies and Contacts: Next, build your customer database.

Help Scout Organizations map to Freshdesk Companies. Pull organizations via GET /v2/organizations (Help Scout uses cursor-based pagination — follow the _links.next URL in each response). Create in Freshdesk via POST /api/v2/companies.

Once companies exist, migrate Help Scout Customers (GET /v2/customers) and create them as Freshdesk Contacts (POST /api/v2/contacts), linking them to the appropriate Company ID you stored in your mapping.

  1. Knowledge Base: Your self-service content comes next.

This moves in a hierarchy. Help Scout Collections become Solution Categories. Inside those, Help Scout Categories serve as sub-containers that map to Solution Folders. Finally, your Articles are created inside those folders.

Use POST /api/v2/solutions/categories, then POST /api/v2/solutions/categories/{id}/folders, then POST /api/v2/solutions/folders/{id}/articles.

Warning

Article bodies transfer as HTML. Freshdesk's article editor may strip certain HTML tags or attributes that Help Scout's editor allowed. After migration, spot-check articles that contain tables, embedded videos, or custom CSS classes.

  1. Tickets and Conversations: Now you are ready for the heaviest lift: the tickets.

Help Scout Conversations map to Freshdesk Tickets. Pull conversations via GET /v2/conversations (cursor-paginated) and create tickets via POST /api/v2/tickets.

When you create the ticket, you must map the status and priority fields in your payload using the mapping tables above. Do not rely on string matching — Freshdesk expects integer values.

  1. Threads and Attachments: A ticket is just a shell without its history. After creating the ticket, you must immediately migrate the Threads.

Pull threads via GET /v2/conversations/{id}/threads. Help Scout notes and replies map to Freshdesk Conversations (replies via POST /api/v2/tickets/{id}/reply, notes via POST /api/v2/tickets/{id}/notes). During this step, you must also handle Attachments.

You will need to download the file from Help Scout's attachment URL and upload it via a multipart/form-data request to the specific Freshdesk ticket or note. You cannot pass a URL — Freshdesk requires the file binary.

  1. The Finishing Touches: Finally, bring over your efficiency tools.

Saved Replies in Help Scout become Canned Responses in Freshdesk.

You should also migrate Ratings to Freshdesk Satisfaction Ratings to preserve your customer satisfaction history.

Handling Tags

Both Help Scout and Freshdesk treat tags as arrays of strings, which makes this look like a simple transfer. It mostly is, but there are two things to watch for:

  • Character limits: Freshdesk tag names have a maximum length. If your Help Scout tags are long or contain special characters, they may be silently truncated.
  • Case sensitivity: Help Scout tags are case-insensitive. Freshdesk treats tags as case-insensitive as well, but if you have duplicates that differ only by case (e.g., Urgent and urgent), they will collapse into a single tag on import.
  • Creating vs. referencing: When you include a tag in a POST /api/v2/tickets request, Freshdesk creates the tag if it does not already exist. No pre-creation step is needed, but this means typos in your migration script silently create junk tags.

Handling Attachments

Attachments are the slowest part of any Help Scout to Freshdesk migration. You are downloading a file from one server and uploading it to another, and every attachment is a separate HTTP round trip.

  • Download from Help Scout: Each thread's attachment includes a URL. Fetch the binary content.
  • Upload to Freshdesk: Freshdesk requires multipart/form-data encoding. You cannot pass a URL reference. The request looks like this:
POST /api/v2/tickets/{id}/notes
Content-Type: multipart/form-data

--boundary
Content-Disposition: form-data; name="attachments[]"; filename="invoice.pdf"
Content-Type: application/pdf

<binary content>
--boundary--
  • Size limits: Freshdesk enforces attachment size limits that vary by plan (typically 20MB per file on most plans). Help Scout does not enforce the same limits. If you have oversized attachments, the upload will fail with a 400 error.
  • Performance: For large migrations, attachments can represent 80%+ of the total migration time. Consider parallelizing attachment uploads (within rate limit bounds) while keeping ticket and thread creation sequential.

Pagination

Both APIs paginate their list endpoints, but they do it differently.

Help Scout uses cursor-based pagination. Each response includes a _links.next href. You follow it until _links.next is absent. There is no page number parameter — you cannot skip ahead or parallelize page fetches.

Freshdesk uses offset-based pagination with a page parameter and a hard ceiling: you can only access the first 300 pages (at 30 results per page, that is 9,000 records per filtered query). If you have more than 9,000 tickets, you must use the updated_since or created_since filters to partition your queries into smaller windows.

Rate Limits

Both systems enforce rate limits, and your migration script must handle them or risk being blocked mid-run.

Help Scout: Rate limits vary by plan. The standard limit is 400 requests per minute for most API endpoints. When you exceed the limit, Help Scout returns a 429 response with a Retry-After header (value in seconds).

Freshdesk: Rate limits also vary by plan. On the Growth plan, the limit is typically around 1,000 API calls per hour. Estate and Forest plans have higher limits. When exceeded, Freshdesk returns a 429 response with a Retry-After header.

Your script must implement a retry loop that respects the Retry-After value:

import time
import requests
 
def api_request(method, url, **kwargs):
    while True:
        response = requests.request(method, url, **kwargs)
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
        response.raise_for_status()
        return response

Do not simply add a fixed time.sleep() between every request. That is either too slow (wasting time when you have headroom) or too fast (hitting the limit anyway). Read the 429 response and react.

ID Mapping

You cannot force Freshdesk to accept Help Scout IDs. Every object you create in Freshdesk gets a new ID. Your migration script must maintain a persistent mapping between old and new IDs for every object type.

A simple approach:

# id_mapping.py
import json
 
class IDMap:
    def __init__(self, filepath="id_map.json"):
        self.filepath = filepath
        try:
            with open(filepath) as f:
                self.data = json.load(f)
        except FileNotFoundError:
            self.data = {}
 
    def set(self, object_type, old_id, new_id):
        self.data.setdefault(object_type, {})[str(old_id)] = new_id
        self._save()
 
    def get(self, object_type, old_id):
        return self.data.get(object_type, {}).get(str(old_id))
 
    def _save(self):
        with open(self.filepath, "w") as f:
            json.dump(self.data, f)
 
# Usage during migration:
id_map = IDMap()
 
# After creating an agent in Freshdesk:
id_map.set("agents", helpscout_user_id, freshdesk_agent_id)
 
# When creating a ticket and assigning it:
freshdesk_responder = id_map.get("agents", helpscout_assignee_id)

Persist this to disk (not just in-memory). If your script crashes at ticket 4,000 of 10,000, you need to resume without re-creating the first 4,000.

Post-Migration Validation

Once the scripts have finished, you need to verify data integrity before cutting over. Do not assume success based on the absence of errors.

Record count reconciliation: Compare counts for every object type. Pull the total from Help Scout and compare against Freshdesk.

Object Help Scout Count Source Freshdesk Count Source
Agents GET /v2/users (paginate to end, count) GET /api/v2/agents (paginate to end, count)
Companies GET /v2/organizations GET /api/v2/companies
Contacts GET /v2/customers GET /api/v2/contacts
Tickets GET /v2/conversations GET /api/v2/tickets (use filters for all statuses)
KB Articles Docs API Solutions API

If the counts do not match, check your migration logs for 4xx errors that you may have caught but not retried.

Spot-check tickets: Pick 10–20 tickets at random (not just recent ones). For each, verify:

  • Subject and body match
  • Correct assignee
  • Correct status and priority
  • All threads present and in the correct order
  • Attachments accessible (download and verify file size is non-zero)
  • Tags present
  • Custom field values populated

Attachment URL validation: After migration, attempt to load a sample of attachment URLs from Freshdesk. Freshdesk-hosted attachment URLs should return 200. If you see 404 responses, the upload failed silently.

Timestamp integrity: Verify that created_at timestamps on migrated tickets match the original Help Scout createdAt values. Timezone mismatches (especially UTC vs. local time) are a common source of subtle data corruption.

Post-Migration Configuration

Once validation passes, you have a few manual tasks to complete.

Go into your Freshdesk Admin panel and rebuild your Workflows and Automations. Use the logic from your old Help Scout workflows, but take advantage of Freshdesk's specific automation features, like "Ticket Creation" or "Time Triggers".

If you used the Docs feature in Help Scout and have set up Redirects to manage traffic from old URLs, you will need to set these up manually in your Freshdesk portal settings, as the API does not support migrating these configurations.

At ClonePartner, we've completed numerous Help Scout to Freshdesk migrations. Each project is handled by a dedicated engineer who customizes the data mapping to match your setup. If you want to avoid the technical complexity, ClonePartner can manage the entire migration for you, from planning and field mapping to validation and go-live.

Further reading:

More from our Blog

Enchant to Desk365 Migration: A Technical Guide
Enchant/Migration Guide/Help Desk

Enchant to Desk365 Migration: A Technical Guide

Technical guide for migrating from Enchant to Desk365. Covers API constraints, object mapping, label-to-category conversion, rate limits, and dependency order.

Raaj Raaj · · 20 min read