Skip to content

Unthread to Freshservice Migration: A Technical Guide

Step-by-step technical guide for migrating Unthread conversations to Freshservice tickets via API. Covers data mapping, ITIL typing, identity resolution, and edge cases.

Roopi Roopi · · 22 min read
Unthread to Freshservice 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

Unthread to Freshservice Migration: A Technical Guide

Migrating from Unthread to Freshservice means translating a Slack-native, conversation-first ticketing model into an ITIL-aligned IT service management platform. There is no push-button migration path between these two systems. Freshservice does not list Unthread as an import source, and Unthread has no direct export-to-Freshservice option.

Info

Sync ≠ migration. Unthread has a beta Freshservice integration that creates Freshservice tickets from new Slack conversations, syncs replies bidirectionally, and mirrors status changes. That handles forward flow only. It is not a historical import utility. If you need old Slack-thread history, attachments, and searchable ticket records inside Freshservice, you need an ETL migration. (docs.unthread.io)

A full historical migration is an API-to-API extraction, transformation, and load project — and a conceptual translation. Unthread tracks support as Slack conversations with message threads, Slack user IDs, and channel-based intake. Freshservice structures data around Tickets (typed as Incident or Service Request), Requesters (identified by email), Agents, Departments, and Conversations (replies and notes). Every Unthread message must be mapped to the correct Freshservice thread type, every Slack identity resolved to an email address, every attachment re-uploaded, and every Slack mrkdwn block converted to HTML.

This guide covers the full technical path: data model mapping, API constraints on both sides, step-by-step execution, identity resolution, ITIL type assignment decision rules, edge cases, failure mode taxonomy, and validation methodology. Much of this overlaps with an Unthread to Freshdesk migration, but Freshservice introduces ITIL-specific concepts — ticket typing, categories, departments, and asset associations — that Freshdesk does not require.

Why Teams Move from Unthread to Freshservice

Unthread is a Slack-native AI helpdesk that converts conversations in Slack and Microsoft Teams 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 — conversations start as Slack messages and stay there.

Freshservice is Freshworks' ITSM platform purpose-built for internal service delivery. It is structured around ITIL v4 processes: incident management, service request fulfillment, change management, problem management, and release management. It includes native IT asset management and a configuration management database (CMDB). Requesters (employees who raise tickets) are unlimited and free; only agents consume paid licenses.

Teams typically make this move for three reasons:

  1. ITIL process requirements — The organization needs formal change management, problem management, SLA policies with response and resolution targets segmented by priority, and multi-stage approval workflows. Unthread excels at conversational ticketing but has no ITIL process layer.
  2. Multi-channel intake beyond Slack — Freshservice supports email, self-service portal, phone, Microsoft Teams, and service catalog intake. Teams that need portal-based self-service across multiple departments outgrow a Slack-only intake model.
  3. Asset management and CMDB — Teams that need to link tickets to hardware assets, track software licenses, and manage configuration items find Freshservice's integrated CMDB tightly coupled with the service desk in ways no Slack-native tool replicates.
Info

Key distinction: Freshservice is an ITSM tool for internal IT and operations. Freshdesk is Freshworks' external customer support helpdesk. If your team handles external customer tickets, Freshdesk — not Freshservice — is the right target. See our Unthread to Freshdesk migration guide for that path.

Data Model Mapping: Unthread → Freshservice

The fundamental challenge is converting Slack-native conversations into ITIL-structured service desk tickets. Here is how the core entities map:

Unthread Entity Freshservice Entity Notes
Conversation Ticket Each conversation becomes one ticket. You must assign a type (Incident or Service Request) — Unthread has no equivalent concept.
Conversation messages (agent) Reply or Note Agent messages become replies (public) or notes (private) depending on visibility.
Conversation messages (customer) Reply Customer messages become requester replies. Freshservice requires user_id for each.
Slack User (internal) Requester or Agent Resolve Slack user ID → email → Freshservice record.
Slack Connect User (external) Requester External workspace members need email lookup via Slack API — Unthread doesn't store their email directly.
Customer (Unthread) Requester or Department Company-level grouping maps to Departments.
Tags Tags Direct mapping. Create tags in Freshservice first.
Ticket Type (Unthread) Category / Subcategory Custom ticket types map to Freshservice's category taxonomy.
Priority Priority Map to Freshservice's fixed set: Low (1), Medium (2), High (3), Urgent (4).
Status Status Map to: Open (2), Pending (3), Resolved (4), Closed (5).
Attachments Attachments Download from Unthread/Slack, re-upload via multipart form. Max 15 MB per file, 40 MB per ticket.
SLA policies SLA policies No automatic migration. Rebuild manually in Freshservice.
Automations / Workflows Workflow Automator No automatic migration. Rebuild manually.
Custom fields (single-select) Custom fields (dropdown) Requires manual field creation in Admin > Field Manager before migration.
Custom fields (multi-select) No native equivalent Freshservice's default schema lacks multi-select ticket fields. Serialize as comma-delimited string in a text field, or use tags as a proxy.

ITIL Type Assignment — Decision Matrix

Freshservice requires every ticket to have a type: Incident or Service Request (and optionally Problem, Change, or Release at higher tiers). Unthread does not have this concept. This is the most consequential schema decision in the migration. Use the following decision rules:

Condition Assign Type
Unthread ticket type = "Bug Report," "Outage," "Error," or similar break/fix label Incident
Unthread ticket type = "Access Request," "New Hire," "Software Request," "Onboarding" Service Request
Unthread tag contains "incident" or "break-fix" Incident
Unthread tag contains "request" or "fulfillment" Service Request
Conversation initiator = external Slack Connect user reporting a problem Incident
No ticket type, no tags, no other signal Incident (default)

Encode this as a lookup function in your transform layer, not an ad-hoc decision during load. The function should accept ticketType, tags [], and initiatorType and return an ITIL type string.

Tip

Avoid defaulting everything to Service Request. Catalog-backed service requests require values for mandatory service-item fields even when those fields are hidden in the portal UI. They are structurally harder to replay historical conversational content into. Incident is the safer default for unmapped records. (api.freshservice.com)

This mapping requires sign-off from your ITSM process owner before migration code is written. Document the decision matrix and include it in your migration runbook.

API Constraints: Both Sides

Unthread API

Unthread's API is REST-based with JSON request/response bodies. Authentication uses an X-Api-Key header with a key generated from the Unthread dashboard.

Key endpoints for extraction:

  • POST /conversations/list — Paginated conversations with cursor-based pagination. Max 100 per page. Supports select, where, and order parameters.
  • GET /conversations/:id/messages — All messages for a conversation.
  • POST /users/list — All users in the workspace.
  • POST /customers/list — Customer records.
  • GET /tags — All tags.
  • GET /ticket-types — Ticket type definitions.
Warning

Unthread's API documentation does not publish explicit rate limits. In practice, moderate pacing (2–3 requests per second) avoids throttling. For 10,000+ conversations, build in exponential backoff and monitor for HTTP 429 responses.

The conversations list endpoint uses cursor-based pagination — each response returns a cursor token for the next page. This is more reliable than offset-based pagination for large datasets since records won't shift between pages if new conversations are created during extraction. Build resumability around the returned cursor, not an incrementing page counter.

Freshservice API

Freshservice's v2 API uses Basic Auth with the API key as username and X as password. Base URL: https://{domain}.freshservice.com/api/v2/.

Key endpoints for loading:

  • POST /api/v2/tickets — Creates a ticket. Required fields: email or requester_id, subject, description, status, priority.
  • POST /api/v2/tickets/{id}/reply — Adds a reply to a ticket.
  • POST /api/v2/tickets/{id}/notes — Adds a note (public or private).
  • GET /api/v2/requesters — Lists requesters.
  • POST /api/v2/requesters — Creates a requester.
  • POST /api/v2/departments — Creates a department.
  • GET /api/v2/ticket_form_fields — Returns ticket field definitions including custom fields.

Rate limits are plan-based and account-wide:

Plan Requests per Minute
Starter 100
Growth 200
Pro 400
Enterprise 500

For migrations, Freshservice offers a temporary rate limit increase to 700 requests per minute. Request this through a Service Request in the Freshservice support portal, specifying planned start/end dates and approximate ticket count. Freshservice typically processes these requests within 1–3 business days. A migration token is issued that you pass as an additional header (X-FW-Partner-Migration). (support.freshservice.com)

Tip

Always request the migration rate limit increase before starting. The migration token also whitelists your API key from Freshservice's built-in spam detection ("spamwatcher"), which automatically blocks agents who create tickets rapidly via API. Without this token, bulk imports trigger spamwatcher and can result in temporary agent account lockouts mid-migration.

Rate math: At 700 req/min, a ticket with an average of 8 conversation notes and 2 attachments requires approximately 11 API calls. That yields ~63 tickets per minute of pure API time. For 5,000 tickets: ~79 minutes of API calls plus ~20% overhead for retries and validation = approximately 95 minutes. Use this formula to set realistic expectations before starting: (ticket_count × avg_calls_per_ticket) / rate_limit × 1.2.

Bulk migration APIs: If you qualify for Freshservice's migration partner program, bulk endpoints accept up to 50 tickets or 50 notes per request, expect attachments as public URLs, and suppress end-user email notifications. These require an X-FW-Partner-Migration token. (support.freshservice.com)

Pagination: Offset-based using page and per_page (max 100). The filter endpoint (/api/v2/tickets/filter) has a hard ceiling of 30 results per page, capped at 10 pages — a maximum of 300 results per filter query. Filter queries must be URL-encoded, wrapped in double quotes, and stay within 512 characters. For migration validation, use the standard list endpoint instead.

Attachment limits: Individual uploads cap at 15 MB per file. Cumulative attachments per ticket are capped at 40 MB total. (api.freshservice.com)

Field length limits: Freshservice's description field accepts up to 65,535 characters. The subject field caps at 255 characters. Long Unthread conversation titles or message bodies that exceed these limits must be truncated and the overflow appended as a note.

Common API error codes during migration:

HTTP Status Cause Resolution
400 – Requester not found email doesn't match any Freshservice requester Create requester first, then retry ticket creation
400 – Field validation failed Mandatory custom field missing value Check ticket_form_fields for required fields; provide defaults
403 – Access denied on note creation Agent token lacks tickets.create_note permission Use an admin-scoped API key
404 – Ticket not found Note posted before ticket creation confirmed Add confirmation check after ticket creation before posting conversations
429 – Too Many Requests Rate limit exceeded Respect Retry-After header; implement exponential backoff

Step-by-Step Migration Process

Step 1: Audit Your Unthread Data

Before writing any code, inventory what you have:

  • Total conversation count (open, closed, archived)
  • Unique users (internal Slack users + external Slack Connect users)
  • Unique customers/accounts
  • Ticket types and tags in use
  • Custom field types (note any multi-select fields — these require special handling)
  • Attachment volume (count and total size — flag any individual files over 15 MB)
  • Conversations with no resolvable requester email

Use the conversations list endpoint with appropriate filters to get counts. This audit drives your time estimates and surfaces data quality issues early — missing emails, orphaned threads, oversized attachments, multi-select field types with no Freshservice equivalent.

Step 2: Set Up the Freshservice Target Environment

  1. Create Departments — Map Unthread customer accounts to Freshservice departments.
  2. Configure Categories — Build your category/subcategory tree matching the ITIL type mapping you defined.
  3. Create Custom Fields — Build matching fields via Admin > Field Manager before any data load begins. Add fields for the original Unthread id and friendlyId for deduplication and audit trail. For Unthread multi-select fields with no Freshservice equivalent, create a text field and document the serialization convention (comma-delimited, or use tags as a proxy).
  4. Create Agent Accounts — Every agent who needs attribution in historical tickets must have an active Freshservice account. Agents cannot be created via the standard API without sending an invitation email, and they consume a paid license. Pre-create them before migration begins.
  5. Create Tags — Pre-create any tags you want to migrate. Tags that don't exist in Freshservice at import time will be silently dropped.
  6. Verify sandbox vs. production API behavior — Some Freshservice API behaviors differ between sandbox and production environments, particularly around email suppression and department creation. Test your full pipeline against a sandbox (available on Pro and Enterprise plans) before production cutover.

Step 3: Resolve Identities — The Hard Part

Every Unthread conversation is tied to Slack user IDs. Freshservice requires email addresses for requesters. This step is where most migrations either succeed or produce garbage data.

Internal Slack users:

  • Call the Slack users.list API (requires a bot token with users:read and users:read.email scopes) to build a slack_user_id → email lookup table.
  • Match emails to Freshservice requester records. Create requesters that don't exist yet via POST /api/v2/requesters.

Slack Connect users (external):

  • Slack's API may not return email addresses for users from external workspaces unless your Slack app has the users:read.email scope and the external user's admin has allowed profile visibility. The API returns a null or missing email field for these users.
  • Resolution paths, in order of preference:
    1. Request the email from the customer directly before migration begins.
    2. Check Unthread's customer records (POST /customers/list) — the customer contact email may be stored separately from the Slack user record.
    3. Create a placeholder requester (e.g., unresolved-{slack_user_id}@yourdomain.com) and prepend the raw Slack display name and user ID to the ticket body for agent context.
    4. If none of the above are feasible, quarantine the conversation in a separate report as explicitly unmigrated.
Warning

Slack Connect identity resolution is the single hardest part of any Unthread migration—a structural challenge common to other email-first targets, as noted in our Unthread to HappyFox migration guide. Budget extra time for it. For a deeper look at extracting Unthread data, see our guide to exporting data from Unthread.

Agent mapping:

  • Build a unthread_user_id → freshservice_agent_id map. Agent-sent messages need to be attributed to the correct Freshservice agent.
  • If an agent no longer exists in Freshservice, attribute their messages to a designated "Migration Bot" placeholder agent and document the mapping in your runbook.

Step 4: Extract All Conversations and Messages

Paginate through all conversations using POST /conversations/list. For each conversation, fetch its messages via the messages endpoint.

Store everything locally (JSON files or a staging database) before attempting any writes to Freshservice. Never stream directly from source to target — if the load fails halfway, you need to resume without re-extracting.

import requests
import json
import time
 
UNTHREAD_API_KEY = "your-api-key"
BASE_URL = "https://api.unthread.io/api"
 
def list_conversations(cursor=None):
    payload = {
        "select": ["id", "title", "status", "priority", "createdAt",
                   "closedAt", "tags.id", "tags.name", "assignee",
                   "customerId", "ticketTypeId"],
        "order": ["createdAt", "id"],
        "limit": 100
    }
    if cursor:
        payload["cursor"] = cursor
    resp = requests.post(f"{BASE_URL}/conversations/list",
                         headers={"X-Api-Key": UNTHREAD_API_KEY},
                         json=payload)
    resp.raise_for_status()
    return resp.json()
 
def get_messages(conversation_id):
    resp = requests.get(f"{BASE_URL}/conversations/{conversation_id}/messages",
                        headers={"X-Api-Key": UNTHREAD_API_KEY})
    resp.raise_for_status()
    return resp.json()
 
def extract_all(output_path="conversations.jsonl"):
    cursor = None
    with open(output_path, "w") as f:
        while True:
            page = list_conversations(cursor)
            for convo in page.get("conversations", []):
                convo["messages"] = get_messages(convo["id"])
                f.write(json.dumps(convo) + "\n")
                time.sleep(0.4)  # ~2.5 req/sec to avoid throttling
            cursor = page.get("nextCursor")
            if not cursor:
                break
Warning

Unthread's API may not expose the full historical thread if the underlying Slack messages have been purged due to Slack workspace retention policies. Verify your Slack retention settings — particularly for free-tier workspaces that cap history at 90 days — before guaranteeing a complete historical migration.

Step 5: Transform Data

This is where the schema translation happens.

Map conversation → ticket fields:

  • titlesubject (truncate to 255 characters; append overflow to description)
  • First message body → description (convert Slack mrkdwn to HTML; cap at 65,535 characters)
  • Map priority values to Freshservice's 1–4 scale
  • Map status to Freshservice's integer codes: Open (2), Pending (3), Resolved (4), Closed (5)
  • Assign ticket type using the decision matrix above
  • Set category / subcategory based on Unthread ticket type
  • Resolve customerId → requester email → Freshservice requester_id or email

Convert Slack mrkdwn to HTML — Complete Reference:

Unthread stores message content in Slack's mrkdwn format. Freshservice expects HTML. Raw mrkdwn pushed into Freshservice renders as plain text with visible syntax characters. Apply these transformations in the order listed — order matters because some patterns overlap:

Slack mrkdwn Syntax HTML Equivalent Notes
*bold* <strong>bold</strong>
_italic_ <em>italic</em>
~strikethrough~ <del>strikethrough</del>
`code` <code>code</code>
```code block``` <pre>code block</pre> Strip leading/trailing newlines
> quoted text <blockquote>quoted text</blockquote> Match ^> at line start
• item / - item <ul><li>item</li></ul> Collapse consecutive items
1. item <ol><li>item</li></ol> Collapse consecutive items
<https://url|display text> <a href="https://url">display text</a>
<https://url> <a href="https://url">https://url</a> No display text
<@U12345> Display name from user lookup Replace with @firstname lastname
<#C12345|channel-name> #channel-name Use channel name portion directly
<!here> @here Strip angle brackets
<!channel> @channel Strip angle brackets
<!everyone> @everyone Strip angle brackets
:emoji_name: Unicode character or remove Map common emoji codes; strip unknowns
\n (line break) <br> Within paragraphs; wrap paragraphs in <p>

Apply these as sequential regex replacements. Process code blocks first to avoid accidentally transforming content inside them. Resolve <@U12345> mentions using the Slack user lookup table built in Step 3.

Classify messages as Reply or Note:

  • Messages from the requester → ticket reply (attributed to the requester)
  • Messages from agents, publicly visible → ticket reply (attributed to the agent)
  • Internal-only messages / triage notes → private note
Tip

For historical replay, notes are often safer than replies. Freshservice's bulk migration endpoints support tickets and notes, not replies. Notes let you control private and incoming without the migration being treated as live outbound mail. Use private=false, incoming=true for requester-visible historical messages and private=true for internal-only content. (support.freshservice.com)

A useful pattern: prepend each migrated note with machine-readable provenance so agents can identify migrated records:

{
  "body": "<div><strong>Migrated from Unthread</strong> · 2024-06-01T14:22:00Z · author=jane@example.com · channel=#acme-support · unthread_id=conv_abc123</div><div>Original Slack content rendered to HTML</div>",
  "private": false,
  "incoming": true
}

Step 6: Load into Freshservice

Load order matters. Follow this sequence:

  1. Supporting entities — Departments, groups, locations, custom fields, and any service catalog items.
  2. Requesters — Create or verify all requester records.
  3. Tickets — Create each ticket with description, requester, status, priority, type, category, and tags. Confirm the returned ticket ID before proceeding.
  4. Conversations — Add replies/notes to each ticket in strict chronological order. Out-of-order uploads produce jumbled threads.
  5. Attachments — Upload to the correct ticket or conversation entry using multipart/form-data.
import requests
import time
from base64 import b64encode
 
FS_DOMAIN = "yourdomain"
FS_API_KEY = "your-freshservice-api-key"
FS_BASE = f"https://{FS_DOMAIN}.freshservice.com/api/v2"
AUTH = b64encode(f"{FS_API_KEY}:X".encode()).decode()
 
def create_ticket(ticket_data):
    resp = requests.post(f"{FS_BASE}/tickets",
                         headers={"Authorization": f"Basic {AUTH}",
                                  "Content-Type": "application/json"},
                         json=ticket_data)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 30))
        time.sleep(retry_after)
        return create_ticket(ticket_data)
    if resp.status_code == 400:
        # Log field validation errors for later remediation
        print(f"Validation error: {resp.json()}")
        return None
    resp.raise_for_status()
    return resp.json()
 
def add_note(ticket_id, body_html, private=True, incoming=False):
    resp = requests.post(f"{FS_BASE}/tickets/{ticket_id}/notes",
                         headers={"Authorization": f"Basic {AUTH}",
                                  "Content-Type": "application/json"},
                         json={"body": body_html, "private": private,
                               "incoming": incoming})
    resp.raise_for_status()
    return resp.json()

Attachments hosted behind authenticated Slack URLs cannot be passed as links — Freshservice cannot fetch them. You must download the file from Slack using a Bearer token, store it temporarily, and upload to Freshservice using multipart/form-data:

curl -v -u apikey:X \
  -F "body=<div>Reply with attachment</div>" \
  -F "attachments[]=@/path/to/local/file.png" \
  -X POST 'https://domain.freshservice.com/api/v2/tickets/1234/notes'

If using the bulk migration partner APIs, attachments must be staged on publicly accessible URLs before the API call.

Step 7: Validate

Post-migration validation is not optional. Run these checks programmatically, not by manual spot-check alone:

  • Ticket count — Does total in Freshservice match total extracted from Unthread?
  • Conversation count — For a random sample of 50–100 tickets, does the number of replies + notes match the original message count?
  • Requester attribution — Are tickets attributed to the correct requesters, not a generic fallback? Calculate the percentage attributed to placeholder requesters and document it.
  • Attachment integrity — Can you retrieve (HTTP 200) and open attachments on migrated tickets?
  • Status accuracy — Are closed tickets actually closed? Are open tickets actually open?
  • Timestamp ordering — Are conversation threads in correct chronological order?
  • Tag and category mapping — Spot-check that tags were not silently dropped.
  • Subject truncation — For tickets where subject exceeded 255 characters, confirm the overflow is in the description.
  • mrkdwn residue — Search for *text*, `text`, and <@U patterns in Freshservice ticket bodies to catch untransformed syntax.
# Count tickets in Freshservice tagged as migrated
GET /api/v2/tickets/filter?query="tag:'migrated-unthread'"
 
# Retrieve all conversations for a specific ticket
GET /api/v2/tickets/{id}/conversations
 
# Check attachment presence on a ticket
GET /api/v2/tickets/{id}
# → look for attachments[] array in response

Build a validation report script that compares source and target counts programmatically and outputs a per-ticket diff for any mismatches.

Failure Mode Taxonomy

Classify migration failures by severity and recovery path rather than handling them as an undifferentiated list:

Class 1: Silent Data Loss (highest risk)

No API error is returned, but data is wrong or absent. Requires active validation to detect.

  • Tags silently dropped — Tag exists in Unthread but was not pre-created in Freshservice. The API accepts the ticket without error and drops the tag.
  • Slack mrkdwn not converted — Raw syntax stored in Freshservice; renders incorrectly in UI but no API error is thrown.
  • Messages out of chronological order — Thread is jumbled; no error returned.
  • External Slack user mapped to wrong requester — Duplicate email, identity collision; ticket attributed incorrectly.

Recovery: Detect via validation queries and regex scans. Re-run the affected transformation and use PUT /api/v2/tickets/{id} to update or POST /api/v2/tickets/{id}/notes to add corrected content.

Class 2: Hard Failures (migration halts)

API returns an error; the record is not created.

  • 400 – Requester not found — Requester email not in Freshservice. Create requester, retry.
  • 400 – Mandatory field missing — Custom field marked required has no value. Set a default and retry.
  • 403 – Access denied — API key lacks required scope. Switch to admin-scoped key.
  • 429 – Rate limit exceeded — Retry after Retry-After seconds with exponential backoff.
  • 413 – Attachment too large — File exceeds 15 MB limit. Compress, split, or link externally.

Recovery: Log the failed record's Unthread ID, resolve the cause, and re-run only the failed records. Never reprocess successfully created tickets.

Class 3: Degraded Fidelity (migrates but loses structure)

Record migrates but loses structural information. Acceptable in some cases; not in others.

  • Original timestamps lost — Migration date shown instead of conversation date (see timestamp section below).
  • Multi-select custom fields serialized as text — Value is present but queryable field filtering is lost.
  • Merged Unthread conversations become separate Freshservice tickets — Relationship is lost.
  • Slack emoji codes not resolved:white_check_mark: appears as literal text.

Recovery: Document as known degradation in migration runbook. For timestamp loss, see the workaround below.

Edge Cases

Timestamp Preservation

Danger

Known limitation. The standard Freshservice v2 API does not allow setting created_at on tickets. Migrated tickets will show the migration date as their creation date.

Workaround: Request migration-mode API access from Freshservice support at the same time you request the rate limit increase. When migration mode is enabled, POST /api/v2/tickets accepts a created_at field in ISO 8601 format that sets the ticket's original creation timestamp. Typical approval time is 1–3 business days alongside the rate limit request.

If migration mode is denied or unavailable: Create a custom date field named "Original Creation Date" and populate it with the Unthread createdAt value. This preserves the data even though the system created_at timestamp will be the migration date. Add this field to your Freshservice reports to restore historical accuracy for trend analysis.

Slack User Mentions in Message Bodies

Slack user mentions (<@U12345>) embedded in messages render as raw text in Freshservice unless resolved during transformation. Apply a regex replacement pass on every message body using the Slack user lookup table built in Step 3. Handle these patterns: <@UXXXXXX> (user mention), <#CXXXXXX|channel-name> (channel mention), <!here>, <!channel>, <!everyone> (workspace mentions).

Conversations With No Resolvable Email

Freshservice requires either email or requester_id to create a ticket. For conversations where email cannot be resolved, apply the four-path resolution strategy from Step 3 in order. Track all conversations routed to placeholder requesters in a separate report — these need manual requester correction after migration.

Merged or Linked Conversations

Unthread supports conversation linking and merging. Freshservice supports parent/child ticket relationships via parent_id but not arbitrary linking. Decision rules:

  • Merged conversations (single thread): Combine message threads into a single ticket in chronological order.
  • Linked conversations (separate threads, cross-referenced): Migrate as separate tickets and add a note to each referencing the other's Freshservice ticket ID. Automate this by resolving the link during transformation.

Attachment Size Conflicts

Unthread caps a single file at 20 MB. Freshservice limits individual uploads to 15 MB per file with a 40 MB total per ticket. For files between 15–20 MB:

  1. Attempt lossless compression (zip archives, optimized images).
  2. If compression is insufficient, upload to a team-accessible storage location (S3, Google Drive) and add a reference link in the ticket body.
  3. Document these tickets in the validation report as "attachment externalized."

Multi-Workspace Freshservice Tenants

If you use Freshservice workspaces (Enterprise feature), omitting workspace_id during load defaults to the primary workspace. Tickets can land in the wrong workspace and disappear from validation queries scoped to a different workspace. Add workspace_id explicitly to every ticket creation call. Test workspace routing in sandbox before production cutover.

Timing Estimates

These estimates derive from the rate math formula: (ticket_count × avg_api_calls_per_ticket) / 700 req/min × 1.2 overhead factor. Average API calls per ticket = 1 (ticket create) + avg messages (notes) + avg attachments.

Volume Avg Messages Avg Attachments API Calls/Ticket Estimated Duration at 700/min
1,000 tickets 5 1 7 ~15 minutes
5,000 tickets 8 2 11 ~95 minutes
10,000 tickets 10 2 13 ~3 hours
20,000 tickets 10 3 14 ~6.5 hours
50,000 tickets 10 3 14 ~16 hours

Attachment-heavy datasets take significantly longer — each attachment upload is a separate API call and typically takes 2–5 seconds per file regardless of rate limit. Factor attachment count separately: attachment_count × 3 seconds / 3600 = additional hours.

What Doesn't Migrate

Some Unthread data has no Freshservice equivalent and must be rebuilt manually:

  • Workflow automations — Unthread's automation rules do not translate to Freshservice's Workflow Automator. Document each rule and rebuild against the Freshservice condition/action model.
  • SLA policies — Freshservice SLAs are structurally different (ITIL-aligned with response + resolution targets per priority level). Reconfigure from scratch in Admin > SLA Policies.
  • Slack channel configuration — Freshservice can integrate with Slack but the model differs. It creates tickets from Slack messages but doesn't replace Slack as the primary interface the way Unthread does.
  • AI agent configuration — Unthread's AI auto-resolution rules, knowledge base connections, and trained models don't carry over. Freshservice's Freddy AI requires separate configuration, much like when migrating from Ada to Freshservice.
  • Webhook and event log history — Audit trails generated by Unthread webhooks exist only in the external systems that received them. Freshservice's audit log starts at go-live.
  • Analytics and historical reports — Historical analytics must be rebuilt using Freshservice's reporting tools against the migrated data. Pre-migration metrics (resolution time, volume by channel) will need recalculation using the original timestamps stored in your custom date field.
  • Canned responses / macros — Recreate manually in Freshservice Admin > Canned Responses.
  • Knowledge base articles — Unthread does not have a native KB; if KB content was stored externally, migrate to Freshservice's Solution Articles separately.

Rollback Plan

Before migrating to production:

  1. Run the full migration against a Freshservice sandbox first. Sandbox environments are available on Pro and Enterprise plans. Confirm API behaviors match production — particularly email suppression and custom field behavior.
  2. Record the ID range of all created tickets, requesters, and departments during migration. Store these in a migration log alongside their source Unthread IDs.
  3. If rollback is needed, use the Freshservice API to delete migrated tickets by ID. Requesters created during migration can be deactivated (not deleted) via PUT /api/v2/requesters/{id} with "active": false. Agents cannot be deleted via API.
  4. Keep Unthread active until you've validated the migration and your team has operated in Freshservice for at least 1–2 weeks. Don't cancel Unthread the day you migrate.

Using the Beta Integration for Cutover

If the business cannot stop support flow during migration, use Unthread's beta Freshservice integration as a bridge for net-new tickets while your historical load runs. New conversations flow into Freshservice, replies sync both ways, and statuses sync back into Slack.

Published limitations of the beta integration: attachments do not sync from Slack to Freshservice; assignee, tags, and priority are not bidirectionally synced. (docs.unthread.io)

Cutover sequence:

  1. Enable the beta integration — new conversations begin creating Freshservice tickets.
  2. Run historical extraction and load in parallel.
  3. Once historical load is validated, disable Unthread-side intake and redirect agents to Freshservice as the system of record.

For full cutover planning, see Zero-Downtime Help Desk Data Migration.

When to Build vs. When to Buy

Build your own migration script if:

  • Fewer than 2,000 tickets with minimal Slack Connect usage
  • Dedicated engineering time available (budget 40–80 hours for build, test, and debug)
  • No strict compliance or audit trail requirements

Bring in a managed migration service if:

  • 10,000+ tickets — The long tail of edge cases (encoding issues, missing emails, attachment failures, multi-select fields) at scale makes DIY migrations fragile.
  • Slack Connect is heavily used — External user identity resolution is tedious, error-prone, and often requires manual intervention customer by customer.
  • Strict compliance requirements — Audit trails, chain-of-custody documentation, or zero data loss guarantees require validation infrastructure that takes as long to build as the migration itself.
  • No dedicated engineering time — A 40–80 hour engineering project with a non-trivial debugging tail is a real cost. Evaluate it against managed service pricing.

Frequently Asked Questions

Is there a native Unthread to Freshservice migration tool?
There is a beta Unthread integration that syncs new conversations, replies, and statuses to Freshservice, but it only handles forward flow — not historical import. Full backfills of old tickets, attachments, and conversation threads require an API-based ETL migration.
How long does an Unthread to Freshservice migration take?
With Freshservice's migration rate limit (700 requests/min), 1,000 tickets take 1–2 hours, 5,000 tickets take 4–6 hours, and 10,000+ tickets take 8–12 hours or more. Attachment-heavy datasets add significant time since each upload is a separate API call.
Does Freshservice preserve original ticket creation dates during migration?
Not through the standard v2 API — tickets get the API call timestamp as their created_at date. To preserve original timestamps, you need migration-mode API access from Freshservice support, which allows setting created_at on imported records.
How do I handle Slack user IDs when migrating to Freshservice?
Freshservice requires an email address for all requesters. You must resolve Unthread's Slack user IDs to email addresses using the Slack API. For external Slack Connect users where emails may be hidden, use a placeholder requester email and append the original Slack ID to the ticket body for agent context.
What Unthread data cannot be migrated to Freshservice?
Workflow automations, SLA policies, AI agent configuration, Slack channel settings, canned responses, and analytics/reports do not migrate. These must be rebuilt manually in Freshservice. Only ticket data, conversations, attachments, tags, and user records transfer via API.

More from our Blog

Unthread to Freshdesk Migration: A Technical Guide
Unthread/Freshdesk/Migration Guide

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.

Nachi Nachi · · 17 min read
Unthread to HappyFox Migration: A Technical Guide
Unthread/HappyFox/Migration Guide

Unthread to HappyFox Migration: A Technical Guide

A step-by-step technical guide for migrating from Unthread to HappyFox. Covers API extraction, data model mapping, rate limits, edge cases, and validation.

Raaj Raaj · · 22 min read