Unthread to Freshdesk Migration: A Technical Guide
Complete technical guide for migrating from Unthread to Freshdesk. Covers API constraints, data model mapping, Slack identity resolution, rate limits, and step-by-step process.
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
Unthread to Freshdesk Migration: A Technical Guide
Migrating from Unthread to Freshdesk means translating a Slack-native, conversation-first ticketing model into a traditional email-first helpdesk built around tickets, contacts, companies, and agent groups. There is no push-button migration path between these two systems. Freshdesk does not list Unthread as an import source, and Unthread has no direct export-to-Freshdesk option. Every migration requires extracting data through Unthread's REST API, transforming it to fit Freshdesk's data model, and loading it through the Freshdesk v2 API.
This is an API-to-API data translation project. Unthread relies on Slack user IDs, channel states, and continuous message threads. Freshdesk structures data around Tickets, Contacts (identified by email), and discrete conversation threads (Replies and Notes). Every Unthread message must be mapped to the correct Freshdesk thread type, every Slack identity must be resolved to an email address, and every attachment must be re-uploaded.
This guide covers the full technical path: data model mapping, API constraints on both sides, known edge cases, step-by-step process, rollback procedures, and validation.
Why Teams Move from Unthread to Freshdesk
Unthread is a Slack-native (and Microsoft Teams-native) AI helpdesk that converts conversations into tracked, routed, and resolved tickets. It handles internal support (IT, HR, Finance) and B2B customer support via Slack Connect channels. Its data model is inherently tied to Slack's architecture.
Freshdesk is Freshworks' email-first helpdesk platform built for external customer support at scale. It offers multi-channel ticket intake (email, portal, phone, chat, social), SLA management, workflow automation, a knowledge base, and a mature reporting stack. Pricing runs from Free (up to 10 agents) through Growth, Pro, and Enterprise tiers.
Teams typically make this move for three reasons:
- Channel expansion — The team is moving from Slack-centric support to a broader omnichannel strategy. Freshdesk's email handling, web portal, phone, and social integrations are far more mature for external-facing operations. (If your team is moving strictly to an email-first shared inbox, see our Unthread to Help Scout migration guide).
- Scaling past Slack — As ticket volume grows, Freshdesk's multi-channel routing, SLA engine, and automation rules become necessary. Standard helpdesk metrics (First Contact Resolution, SLA breaches by tier) are easier to extract from a structured ticket system than a conversational UI.
- Consolidation — The company is standardizing on the Freshworks suite (Freshdesk, Freshchat, Freshsales) and deprecating standalone tools, or needs audit-grade data retention and built-in CSAT surveys that Unthread's analytics cannot match at scale.
Unthread vs Freshdesk: Data Model Comparison
The two platforms have fundamentally different architectures. Understanding the mapping is the hardest part of this migration.
| Unthread Object | Freshdesk Equivalent | Translation Notes |
|---|---|---|
| Conversation | Ticket | 1:1 mapping. Unthread's title → Freshdesk subject. Initial message → description. |
| Message (in conversation) | Conversation (reply or note) | Each subsequent message becomes a Freshdesk reply or private note. Direction and visibility matter. |
| Customer | Contact + Company | Unthread's customer maps to a Freshdesk Contact. The customer's organization maps to a Company. Freshdesk groups contacts by Company domains. |
| User (agent) | Agent | Critical: Slack user IDs must be resolved to email addresses. Freshdesk Agents consume licenses — map carefully. |
| Tag | Tag | Direct mapping. Freshdesk tags are plain strings, max 32 characters each. |
| Ticket Type | Ticket Type (custom field) | Freshdesk uses a dropdown. Pre-create matching values before import. |
| Custom Fields | Custom Fields | Must be pre-created in Freshdesk before import. Field types may not match 1:1. |
| Attachments | Attachments | Must be downloaded from Unthread/Slack CDN and re-uploaded to Freshdesk. Size limits apply. |
| Priority | Priority | Map Unthread's priority levels to Freshdesk's 4-level system (1=Low, 2=Medium, 3=High, 4=Urgent). |
Status Mapping: Unthread to Freshdesk
Unthread conversations carry a status string field. Freshdesk tickets use integer status codes. The mapping is:
| Unthread Status String | Freshdesk Status Integer | Notes |
|---|---|---|
"open" |
2 (Open) |
Active, unresolved conversations |
"closed" |
5 (Closed) |
Fully resolved and closed |
"snoozed" |
3 (Pending) |
Best approximation; Freshdesk Pending = waiting on requester or scheduled follow-up |
"archived" |
5 (Closed) |
No direct equivalent; treat as closed |
Unthread may also return "triage" conversations, which are pre-qualification threads routed via inbox channels before assignment. These have no direct Freshdesk equivalent. Options: skip them entirely, import as status: 2 (Open) with a tag of unthread-triage, or import as status: 5 (Closed) with a note indicating they were triage threads. Decide before you run the extract phase.
Unthread's data model is inherently tied to Slack. Every conversation has Slack thread timestamps (ts), Slack user IDs, and channel references. None of these exist in Freshdesk. You must resolve every Slack identity to an email address before import.
API Constraints: What You're Working With
Unthread API (Source)
Unthread's API is RESTful, JSON-based, and authenticated via an X-Api-Key header using a service account key from the Unthread dashboard.
Key extraction endpoints:
POST /conversations/list— Returns conversations with cursor-based pagination (max 100 per page). Supports field selection viaselect, filtering viawhere, and ordering.GET /conversations/:id/messages— Returns all messages for a given conversation.POST /users/list— Lists all users (agents) in the workspace.POST /customers/list— Lists all customer records.GET /tags— Lists all tags.
// Example: List conversations with specific fields
POST https://api.unthread.io/api/conversations/list
Headers: { "X-Api-Key": "your-key" }
Body: {
"select": ["id", "title", "status", "customerId", "assigneeUserId", "tags", "createdAt"],
"order": ["createdAt"],
"limit": 100
}Unthread does not publicly document hard rate limits in its API reference. Empirically, conservative pacing at 1–2 requests/second avoids 429 responses during bulk extraction. Implement exponential backoff starting at 5 seconds, doubling on each retry, capped at 60 seconds.
Freshdesk API (Target)
Freshdesk's v2 API uses HTTP Basic Auth with your API key as the username and any string as the password. Rate limits are per account (not per key) and scale by plan (for a deeper dive into how these limits affect large-scale data movement, see our Freshdesk to Zendesk migration guide). Source: Freshdesk API Rate Limiting documentation.
| Plan | Total RPM | Ticket Create/min | Ticket Update/min |
|---|---|---|---|
| Growth | 100 | 50 | 50 |
| Pro | 400 | 160 | 160 |
| Enterprise | 700 | 280 | 280 |
Key import endpoints:
POST /api/v2/contacts— Create a contact (deduplicates on email; HTTP 409 if duplicate).POST /api/v2/companies— Create a company (deduplicates on name).POST /api/v2/tickets— Create a ticket.POST /api/v2/tickets/[id]/notes— Add a private or public note.POST /api/v2/tickets/[id]/reply— Add a reply (triggers email notification by default — see edge cases).
Freshdesk rate limits are account-wide. Every script, integration, and marketplace app running against your Freshdesk instance draws from the same bucket. Even failed requests (400, 401) count toward the limit. Monitor X-RateLimit-Remaining on every response.
Edge Cases and Hard Constraints
These are the issues that break naive migration scripts. Handle them before you start writing import code.
1. Timestamps cannot be set on standard ticket creation
Freshdesk's POST /api/v2/tickets does not accept created_at or updated_at in the request body by default. Every ticket you create gets the current timestamp, which destroys historical reporting.
Workaround: Store the original Unthread createdAt in a custom date field (e.g., cf_original_created_at). This preserves the historical date for reporting, even though Freshdesk's native created_at will show the migration date. Freshdesk does offer a Migration API for some accounts that allows timestamp overrides, but availability is not documented publicly. It requires a direct request to Freshworks support and is typically granted for Enterprise plan customers with a documented migration case. Do not build your migration timeline around this being available.
2. Replies trigger email notifications
When you use POST /api/v2/tickets/[id]/reply, Freshdesk sends an email to the requester by default. During a bulk migration, every historical reply could blast your customers with old messages.
Workaround: Use POST /api/v2/tickets/[id]/notes with "private": false instead of the reply endpoint. Public notes appear in the ticket timeline without triggering outbound email. Also disable all Dispatch'r rules, Observer rules, and email notification settings before importing.
Verification checklist before starting import:
- Navigate to Admin → Email Notifications → disable all outbound templates
- Navigate to Admin → Dispatch'r → disable all rules (confirm each shows "Inactive")
- Navigate to Admin → Observer → disable all rules
- Navigate to Admin → SLA Policies → pause or disable
- Confirm by creating one test ticket and verifying no email is sent
3. Slack identity resolution
Unthread stores users by Slack user ID (format: U followed by alphanumerics, e.g., U01AB2CD3EF). Freshdesk requires email addresses. Resolution requires two potential sources:
Source 1: Unthread's own user/customer endpoints — Unthread stores email addresses for agents (POST /users/list) and customers (POST /customers/list). This covers most cases for managed workspaces.
Source 2: Slack's users.info API — For gaps, call Slack directly:
import requests
def resolve_slack_email(slack_user_id, slack_bot_token):
"""
Resolves a Slack user ID to an email address.
Requires a bot token with users:read and users:read.email scopes.
"""
url = "https://slack.com/api/users.info"
headers = {"Authorization": f"Bearer {slack_bot_token}"}
params = {"user": slack_user_id}
resp = requests.get(url, headers=headers, params=params)
data = resp.json()
if not data.get("ok"):
return None # User not found or token insufficient
user = data.get("user", {})
profile = user.get("profile", {})
# email field may be absent if enterprise grid hides it
email = profile.get("email")
return email
def get_email_or_fallback(slack_user_id, slack_bot_token):
"""
Returns resolved email or a deterministic fallback for unresolvable IDs.
Fallback creates a valid-format address that won't bounce.
"""
email = resolve_slack_email(slack_user_id, slack_bot_token)
if email:
return email, False # (email, is_fallback)
# Generate deterministic fallback — same ID always maps to same address
fallback = f"unresolved-{slack_user_id.lower()}@slack-import.local"
return fallback, True # Flag for manual reviewEnterprise Grid email obfuscation: In Slack Enterprise Grid deployments, external users in Slack Connect channels may have their email addresses hidden by the grid admin policy. The profile.email field returns null even with users:read.email scope. In this case, the users.info response will include profile.real_name and profile.display_name but no email. Use the deterministic fallback pattern above and flag these contacts for manual email correction post-migration.
Multi-workspace scenarios: If your Unthread instance spans multiple Slack workspaces (common in Slack Connect setups), the same Slack user ID format can exist in different workspaces with different underlying identities. Tag each extracted user record with its source workspace ID (team_id from Unthread's user objects or Slack's API) and resolve against the correct workspace's bot token. Without workspace scoping, you risk mapping the wrong email to a user ID.
4. Attachment re-upload and size limits
Unthread message attachments are hosted on Unthread/Slack CDN. Freshdesk expects file uploads via multipart/form-data. You must download each attachment, buffer it locally, and re-upload to the corresponding Freshdesk ticket or note.
Freshdesk imposes size limits on attachments — up to 20MB per file depending on the endpoint and plan. Files exceeding the limit cause a 413 Request Entity Too Large error. The standard fallback is to upload oversized files to a secure cloud storage bucket (e.g., AWS S3) and insert a text link into the Freshdesk ticket: [Attachment too large for migration: view file here](https://s3.url...).
Slack CDN URLs for attachments are time-limited and require authentication. Extract and download all attachments during the extraction phase (Step 3), not during the load phase. If you defer download to load time, CDN URLs may have expired.
5. Slack markdown to HTML conversion
Unthread data inherits Slack's specific flavor of markdown. The five most common patterns and their HTML equivalents:
| Slack Markdown Pattern | Example | HTML Output |
|---|---|---|
| Bold | *bold text* |
<strong>bold text</strong> |
| Italic | _italic text_ |
<em>italic text</em> |
| Strikethrough | ~struck~ |
<del>struck</del> |
| Hyperlink | <https://example.com|Link Text> |
<a href="https://example.com">Link Text</a> |
| User mention | <@U01AB2CD3EF> |
Replace with resolved name/email or @username |
| Code block | `code` |
<code>code</code> |
| Multiline code | ```block``` |
<pre><code>block</code></pre> |
Recommended library: Use slack-markdown (npm) for Node.js or implement the regex map above in Python. The slack-markdown package handles the full Slack mrkdwn spec including channel references (<#C01234>), user mentions, and URL formatting. If you're working in Python, the mrkdwn package or a custom regex pipeline based on the table above covers the common cases. Raw Slack markdown pushed to Freshdesk's API without conversion renders as plain text with visible asterisks, angle brackets, and pipes.
6. Agent attribution on notes
When creating notes or replies, the user_id field attributes the message to a specific agent in Freshdesk. However, Freshdesk's API will attribute the note to the API key owner if the user_id belongs to an agent without their own API key being used. Test attribution with a small batch of 5–10 notes before running the full migration. If attribution defaults to the API key owner, you have two options: use each agent's own API key per note (operationally complex), or attribute all notes to a dedicated "Migration Bot" agent account and preserve the original author name in the note body (e.g., prepend "Originally from: Sarah Chen —" to the note text).
7. Pagination limits on Freshdesk reads
When listing tickets to verify import, Freshdesk caps at 300 pages at 30 results per page (9,000 tickets via standard listing). For larger datasets, use the updated_since filter or the Filter Tickets API. The Filter Tickets API (GET /api/v2/search/tickets) is capped at 10 pages of 30 results per page (300 results) per query. For full dataset verification at scale, query by cf_unthread_id ranges or use Freshdesk's bulk export feature from the admin UI.
8. System messages and bot activity
Unthread conversations often contain system messages (e.g., "Ticket assigned to Sarah", "SLA Warning"). These are identifiable by the message type field or a userId matching Unthread's internal bot user ID. Decide whether these hold business value. If they do, migrate them as Private Notes authored by a generic "System" agent. If they don't, filter them out during extraction to save API calls and reduce clutter. System messages typically account for 15–30% of total message volume in active Unthread workspaces — filtering them reduces API call count meaningfully.
Step-by-Step Migration Process
Step 1: Audit and Scope
Before writing any code, inventory your Unthread data:
- Total conversation count (open, closed, all statuses)
- Total customers and their email coverage
- Custom fields and ticket types in use
- Tags in active use
- Attachment volume (count and total size)
- Agent count and Slack-to-email mapping completeness
- Presence of multi-workspace user IDs
Decide what to migrate. Closed conversations older than 2 years may not justify the effort. Internal triage conversations (Unthread's type: "triage") may have no equivalent in a customer-facing Freshdesk instance.
Step 2: Prepare Freshdesk
Before importing any data:
- Create custom fields — Add any custom ticket fields, contact fields, or company fields that map from Unthread. Required fields:
cf_original_created_at(date type),cf_unthread_id(single-line text). These two fields are mandatory for traceability and rollback. - Create groups — Map Unthread's team/project structure to Freshdesk groups.
- Provision agents — Ensure all agents exist in Freshdesk with matching email addresses.
- Configure ticket types — Add any Unthread ticket types as choices in Freshdesk's Type dropdown.
- Disable automations — Use the verification checklist in Edge Case #2 above to confirm all Dispatch'r rules, Observer rules, email notifications, and SLA policies are inactive before starting any import.
Step 3: Extract from Unthread
Extract in this order (respecting dependencies):
- Users →
POST /users/list - Customers →
POST /customers/list - Tags →
GET /tags - Conversations →
POST /conversations/list(paginate through all) - Messages →
GET /conversations/:id/messages(per conversation) - Attachments — Download from URLs in message payloads immediately during this phase
Store everything as JSON files on disk, organized by conversation ID. This gives you a local staging layer you can re-process without hitting Unthread's API again. Do not attempt to pipe data directly from Unthread's API to Freshdesk's API in real-time — network timeouts or rate limits will cause data loss.
# Pseudocode: Extract all conversations
import requests
import time
import json
import os
API_KEY = "your-unthread-api-key"
BASE = "https://api.unthread.io/api"
headers = {"X-Api-Key": API_KEY, "Content-Type": "application/json"}
cursor = None
all_conversations = []
while True:
body = {
"select": ["id", "title", "status", "customerId", "assigneeUserId",
"tags", "createdAt", "closedAt", "priority", "type"],
"order": ["createdAt", "id"],
"limit": 100
}
if cursor:
body["cursor"] = cursor
resp = requests.post(f"{BASE}/conversations/list", json=body, headers=headers)
if resp.status_code == 429:
time.sleep(10)
continue
data = resp.json()
conversations = data.get("data", [])
if not conversations:
break
all_conversations.extend(conversations)
# Save each batch to disk immediately
for conv in conversations:
with open(f"staging/conversations/{conv['id']}.json", "w") as f:
json.dump(conv, f)
cursor = data.get("cursor")
if not cursor:
break
time.sleep(0.6) # ~1.6 req/secStep 4: Transform
For each Unthread conversation:
- Resolve the requester — Look up the Unthread
customerId, find their email using the process in Edge Case #3. If no email exists, flag for manual review or assign a fallback address. - Map the agent — Resolve
assigneeUserIdto a Freshdesk agent ID via email lookup. - Map the status — Use the status mapping table above. Flag
"triage"conversations for separate handling. - Convert Slack markdown to HTML — Parse all message bodies using the table and library guidance in Edge Case #5.
- Build the ticket payload:
subject← conversationtitle(or first message text, truncated to 255 chars)description← first message body (HTML-converted)email← requester's emailstatus← mapped integer from status tablepriority← mapped to 1–4 scaletags← array of tag names (max 32 chars each)custom_fields.cf_original_created_at← originalcreatedAttimestampcustom_fields.cf_unthread_id← Unthread conversation ID (critical for rollback)
- Build note payloads for each subsequent message in chronological order. Map internal/private messages to
"private": true; customer-visible messages to"private": false. Use notes endpoint for all to avoid triggering email notifications.
Step 5: Load into Freshdesk
Load in dependency order:
- Companies →
POST /api/v2/companies - Contacts →
POST /api/v2/contacts(handle 409 duplicates gracefully — on 409, retrieve existing contact ID and continue) - Tickets →
POST /api/v2/tickets(one per conversation) - Notes →
POST /api/v2/tickets/[id]/notes(chronological order per ticket, using"private": falsefor public messages to avoid triggering emails) - Attachments → Upload with the note/ticket creation as
multipart/form-data
Implement a rate limiter that reads X-RateLimit-Remaining from every response and pauses when the bucket is low. Log every failed record to a structured failure log:
import time
import json
from datetime import datetime
# Failure log schema
def log_failure(record_type, source_id, error_code, error_body, payload):
"""
Writes failed records to a structured log for retry.
record_type: "ticket" | "note" | "contact" | "company" | "attachment"
source_id: Unthread conversation/message ID
"""
entry = {
"timestamp": datetime.utcnow().isoformat(),
"record_type": record_type,
"source_id": source_id,
"http_status": error_code,
"error": error_body,
"payload": payload # Store full payload for retry
}
with open("migration_failures.jsonl", "a") as f:
f.write(json.dumps(entry) + "\n")
def safe_request(method, url, **kwargs):
while True:
resp = method(url, **kwargs)
remaining = int(resp.headers.get("X-RateLimit-Remaining", 1))
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 60))
time.sleep(retry_after)
continue
if remaining < 5:
time.sleep(2) # ease off before hitting the wall
return respRetry strategy for failed records: After the full migration run completes, process migration_failures.jsonl in a separate pass. Filter by record_type and http_status. Permanent failures (4xx except 409, 429) indicate data problems and need manual review. Transient failures (5xx, 429) can be retried directly. Use source_id to check whether a ticket with matching cf_unthread_id was already created before retrying, to prevent duplicates.
Step 6: Validate
After import, verify:
- Ticket count matches source conversation count (minus any intentionally excluded triage or old records)
- Note/reply count per ticket matches source message count minus 1 (first message became the description)
- Requester attribution is correct (spot-check 20+ tickets — did they default to the API key owner?)
- Agent assignment is correct
- Tags are present and spelled correctly
- Custom field values are populated (
cf_original_created_atandcf_unthread_id) - Attachments are accessible and not corrupted zero-byte files
- Thread order — Do notes appear in chronological order?
- Fallback emails — Pull all contacts where email contains
slack-import.localand flag for manual correction
Build a reconciliation script that queries Freshdesk's Filter Tickets API for all tickets with cf_unthread_id set and compares against your local staging directory of extracted Unthread conversation IDs. Any Unthread ID present in staging but absent in Freshdesk indicates a failed or missed import.
Step 7: Rollback Procedure
If the migration produces unacceptable data quality or triggers automations unexpectedly, rollback requires bulk-deleting imported tickets. Freshdesk does not provide a native bulk delete API endpoint. The rollback procedure:
- Query Freshdesk's Filter Tickets API for all tickets where
cf_unthread_idis not blank:GET /api/v2/search/tickets?query="cf_unthread_id:'*'"— note this API returns max 300 results per query, so paginate bycf_unthread_idvalue ranges. - Collect all Freshdesk ticket IDs from the query results.
- Call
DELETE /api/v2/tickets/[id]for each ticket. Rate limits apply. - Contacts and companies created during migration must be deleted separately via
DELETE /api/v2/contacts/[id]— filter bycf_unthread_idon contact custom fields if you added it, or by creation date range matching the migration window.
Rollback time estimate: Deleting 5,000 tickets at Pro plan rate limits (160 deletes/min) takes approximately 32 minutes for tickets alone, plus additional time for contacts and companies. Plan the rollback window accordingly. Do not run a production migration without a tested rollback script ready.
Step 8: Re-enable Automations and Go Live
Once validation passes:
- Re-enable SLA policies
- Re-enable Dispatch'r and Observer rules
- Re-enable email notifications
- Close or resolve all migrated tickets that should be in a final state (use
PUT /api/v2/tickets/[id]with"status": 5for closed)
Timing and Throughput Estimates
Real-world throughput depends on your Freshdesk plan, attachment volume, and message density per ticket.
| Scenario | Freshdesk Plan | Effective Ticket Creates/hr | Est. Time for 5,000 Tickets |
|---|---|---|---|
| Tickets only (no notes) | Pro (400 RPM) | ~9,600 | ~30 min |
| Tickets + avg 5 notes each | Pro (400 RPM) | ~1,600 | ~3 hrs |
| Tickets + avg 5 notes + attachments | Pro (400 RPM) | ~800 | ~6 hrs |
| Tickets + avg 5 notes each | Growth (100 RPM) | ~400 | ~12.5 hrs |
Attachment uploads are the bottleneck — each one requires a separate multipart/form-data request and consumes one API call from the rate limit bucket. System message filtering (Edge Case #8) reduces note volume by an estimated 15–30%, which proportionally improves throughput.
If you're on Freshdesk Growth and migrating more than 5,000 tickets with conversation history, request a temporary rate limit increase from Freshdesk support before starting. They can often bump you to higher RPM for the migration window. Frame the request as a one-time data migration with a defined start and end date.
What You Lose in This Migration
Be explicit with stakeholders about what does not transfer:
- Slack thread context — The threaded, real-time conversational feel of Slack is gone. Messages become flat notes on tickets.
- Original timestamps on tickets — Freshdesk's native
created_atwill reflect the migration date unless you get a Migration API enabled. Thecf_original_created_atcustom field workaround preserves the data but Freshdesk's native date filters, SLA calculations, and trend reports will treat all migrated tickets as created on the migration date. - Slack reactions and emoji data — No equivalent in Freshdesk.
- Unthread AI classification history — AI-generated tags, auto-responses, and routing decisions from Unthread do not transfer as structured data. They can be migrated as free-text private notes if preserving the information matters.
- CSAT survey responses — Unthread's survey data and Freshdesk's CSAT system are completely separate. Historical satisfaction scores won't migrate.
- Slack user mentions in message bodies — Unless resolved to names during transform,
<@U01AB2CD3EF>patterns will appear as raw Slack IDs in Freshdesk note text. The transform step must replace all<@UXXXXXXX>patterns with resolved display names.
When to Use a Managed Migration Service
DIY works well for datasets under 2,000 conversations with low attachment counts and a developer who can dedicate 2–3 days to building and monitoring the ETL pipeline. Beyond that, the edge cases compound:
- Slack identity resolution across multiple workspaces
- Attachment CDN URLs that expire before download completes
- Rate limit management across multi-hour import windows
- Notification suppression across all Freshdesk automation layers
- Slack markdown to HTML conversion
- Reconciliation and rollback planning
For teams without a dedicated migration engineer, or those running production support during the migration, a managed service eliminates the risk.
For more context on data export from Unthread, see our guide on How to Export Data from Unthread. If you're evaluating Freshdesk as a target, our Freshdesk Migration Checklist covers the preparation steps in detail.
Frequently Asked Questions
- Can I import Unthread data directly into Freshdesk using a native tool?
- No. Freshdesk does not offer a native import tool for Unthread. The migration requires custom API scripting to extract conversations from Unthread's REST API, transform them into Freshdesk's ticket schema, and load them through Freshdesk's v2 API.
- Does Freshdesk's API let you set the created_at date when importing tickets?
- No. Freshdesk's POST /api/v2/tickets endpoint does not accept created_at or updated_at by default. Every API-created ticket gets the current timestamp. The reliable workaround is storing the original date in a custom field like cf_original_created_at. You can also ask Freshdesk support about enabling a Migration API, but availability is not guaranteed.
- How do I prevent Freshdesk from emailing customers during a migration?
- Avoid the reply endpoint (POST /tickets/[id]/reply) as it triggers outbound email. Use the notes endpoint (POST /tickets/[id]/notes) with private set to false for public-facing messages instead. Also disable all Dispatch'r rules, Observer rules, and email notification settings before importing.
- What are Freshdesk's API rate limits for migration?
- Rate limits are per account: Growth gets 100 requests/minute, Pro gets 400/min, and Enterprise gets 700/min. Sub-limits apply per endpoint (e.g., Pro allows 160 ticket creates/min). Contact Freshdesk support to request a temporary increase for large migrations.
- How long does an Unthread to Freshdesk migration take?
- It depends on volume and plan. On Freshdesk Pro (400 RPM), 5,000 tickets with an average of 5 notes each takes roughly 3 hours for API calls alone. Add attachments and that doubles. On Growth (100 RPM), the same migration takes 12+ hours.

