Skip to content

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
Unthread to HappyFox 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 HappyFox Migration: A Technical Guide

Info

TL;DR: Migrating from Unthread to HappyFox requires translating a Slack-native, conversation-based data model into a traditional email-first ticketing system. There is no native migration path. You must extract via Unthread's REST API, resolve Slack user IDs to email addresses, convert Slack mrkdwn to HTML, and load through the HappyFox v1.1 API. Identity resolution and taxonomy mapping are the hardest parts. Slack Connect users (external workspace members) present a distinct, harder identity problem with no automatic resolution path.

Migrating from Unthread to HappyFox means moving from a platform where support happens inside Slack channels to a platform structured around traditional helpdesk tickets, categories, and email-identified contacts. Much like an Unthread to Freshdesk migration, there is no push-button migration path. HappyFox does not list Unthread as a supported import source, and Unthread has no direct export-to-HappyFox feature.

This is an API-to-API data translation project. Unthread relies on Slack user IDs, channel states, and continuous message threads. HappyFox structures data around Tickets with typed updates (staff replies, client replies, private notes), Contacts identified by email, Contact Groups, and Categories. Every Unthread message must be mapped to the correct HappyFox update type, every Slack identity must be resolved to an email address, and every attachment must be downloaded and re-uploaded.

This guide covers the full technical path: data model mapping, API constraints on both sides, migration approach selection, the step-by-step process, edge cases, error handling, and validation.

Why Teams Move from Unthread to HappyFox

Unthread is a Slack-native AI helpdesk that converts Slack 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.

HappyFox is an email-first helpdesk platform built for structured customer support. It organizes everything around tickets — each with a conversation thread of staff replies, client replies, and private notes — plus contacts, contact groups, categories, custom fields, tags, and a knowledge base.

Teams typically make this move for three reasons:

  1. Channel expansion — The team is moving beyond Slack-only support and needs multi-channel intake (email, portal, phone, chat) that HappyFox provides natively.
  2. Structured workflows — HappyFox's Smart Rules, SLA management, round-robin assignment, and custom statuses offer automation depth that exceeds what Unthread provides.
  3. Reporting and taxonomy enforcement — The team needs traditional helpdesk metrics (first response time, resolution time by category) and strict ticket categorization that is easier to mandate in a web portal than in free-form Slack channels.

Data Model Mapping: Unthread → HappyFox

The two platforms use fundamentally different structures. Map these before writing any code:

Unthread Object HappyFox Equivalent Notes
Conversation Ticket 1:1. HappyFox requires a category and name (subject) for every ticket.
Message (agent, public) Staff Reply (staff_update) Each public agent message becomes a staff reply.
Message (customer) Client Reply (user_reply) Each customer message becomes a client reply.
Internal note / private discussion Private Note (staff_pvtnote) Internal channel discussions become private notes.
Customer (Slack ID) Contact HappyFox requires an email address. Slack IDs must be resolved.
Account Contact Group Partial mapping. HappyFox Contact Groups lack some Account-level fields but support ticket-visibility rules.
Ticket Type Category HappyFox categories drive routing, ticket numbering prefixes, field visibility, and SLA rules.
Tags Tags Direct mapping. HappyFox tags are created on-the-fly when assigned.
Custom Fields Ticket Custom Fields / Contact Custom Fields Type-by-type recreation required.
Attachments Attachments Must be re-uploaded via multipart/form-data.
SLA Policies SLA Plans Cannot be migrated programmatically — rebuild manually.
Automations / AI workflows Smart Rules / Canned Actions No API export from either side — screenshot and rebuild.
Webhook/event stream Smart Rule triggers Unthread webhook subscriptions have no import path into HappyFox. Rebuild trigger logic as Smart Rules from scratch.
CSAT responses Satisfaction surveys Survey response data from Unthread does not map to HappyFox's satisfaction survey system.

The Identity Problem

The hardest technical hurdle is identity resolution. Unthread tracks users by their Slack user ID (e.g., U12345678). HappyFox requires an email address to create or look up a Contact. If you push a ticket to the HappyFox API without a valid Contact email, the request fails with HTTP 400.

You must build a complete identity mapping table before migrating any tickets. Slack Connect users require separate handling — see the Slack Connect section under Edge Cases below.

Account → Contact Group Nuances

Unthread Accounts map most naturally to HappyFox Contact Groups. In Unthread, account admins can view all tickets submitted by users within their account in the customer portal. HappyFox Contact Groups replicate this: they support domain-based auto-add, ticket visibility across group members, and private-category access. (docs.unthread.io)

One critical behavior: HappyFox's domain auto-add only applies to newly created contacts. It does not backfill existing contacts into the group. If you create contacts before creating groups with domain rules, those contacts will not be added automatically. Create Contact Groups before importing contacts. (docs.unthread.io)

Ticket Types vs. Categories

Resist the urge to turn every Unthread Ticket Type into a HappyFox Category. As we've seen in HappyFox to Zendesk migrations, HappyFox categories affect routing, ticket numbering prefixes, visibility, and field associations. In practice, most teams should use categories for major queues or departments, then represent former Unthread ticket types as a dropdown custom field with dependent fields where needed. (support.happyfox.com)

HappyFox Contact Deduplication Behavior

HappyFox does not merge contacts automatically if you POST the same email address twice. A duplicate email on a POST /contacts/ call returns HTTP 400 with an error indicating the email already exists. Your import script must first attempt a GET /contacts/?email=<address> lookup, and only call POST if no match is found. If you skip this check and your source data contains duplicate customer emails (common when multiple Slack users share a corporate address), you will generate duplicate contact records that must be manually merged through the HappyFox UI — there is no bulk merge API.

Contact deduplication strategy:

  1. Before any contact creation, build an in-memory dict of email → happyfox_contact_id by paginating through GET /contacts/.
  2. For each Unthread customer, check the dict first. If found, use the existing ID. If not, POST to create and add to the dict.
  3. Log every new contact creation with the source Unthread customer ID for rollback tracing.

API Constraints

Both platforms enforce strict rate limits and pagination rules that shape your ETL pipeline.

Unthread API (Source)

  • Auth: X-Api-Key header with a service account key from the Unthread dashboard
  • Pagination: Cursor-based, 100 records per page maximum
  • List pattern: POST requests to /conversations/list, /customers/list, etc. with select, where, order, limit, and cursor fields in the request body
  • Rate limits: Not publicly documented with specific numbers. Implement exponential backoff on 429 responses.
  • No bulk CSV export for ticket data — the API is the only complete extraction path

A typical extraction call:

POST /conversations/list
{
  "select": ["id", "title", "status", "createdAt", "closedAt", "customerId", "assigneeId", "tags.id", "tags.name"],
  "order": ["createdAt", "id"],
  "descending": false,
  "limit": 100,
  "cursor": null
}

Message payloads contain raw Slack mrkdwn, including Slack-specific formatting like <@U12345678> for user mentions and <https://example.com|Example> for links. For the full extraction playbook, see our guide on how to export data from Unthread.

Determining Message Visibility: Internal vs. Customer-Facing

Unthread does not expose a single boolean field called is_internal. Message classification requires inspecting the channel field and message metadata:

  • Messages posted in the main support channel (where the customer is present) are customer-visible.
  • Messages posted in a companion private channel or via Unthread's internal note feature are internal.
  • The source or type field on the message object indicates origin (e.g., slack_message vs. internal_note).
  • Agent replies sent via the Unthread composer with the "Reply" button are customer-facing; those sent via the "Note" button are internal.

In practice, audit a sample of 50 conversations manually before writing classification logic. The exact field names depend on your Unthread plan and configuration. Misclassifying internal notes as customer-facing replies is the most common data integrity failure in this migration.

HappyFox API (Target)

  • Base URL: https://<your-subdomain>.happyfox.com/api/1.1/json/
  • Auth: HTTP Basic Authentication using API Key and Auth Code (generated under Apps → Goodies → API)
  • Rate limits: 500 GET requests/minute, 300 POST requests/minute — global, account-wide. Exceeding either returns HTTP 429 and triggers a 10-minute lockout
  • Pagination: size (max 50) and page parameters on GET endpoints
  • Bulk ticket creation: Up to 100 tickets per request, but the bulk endpoint does not support attachments
  • Attachments: multipart/form-data required; 25 MB total per request (not per file); attachment URLs in responses expire after 5 minutes
  • Timestamps: created_at can be set on ticket creation. Reply and private-note endpoints do not document a timestamp override — prepend original timestamps to message bodies as a fallback.
  • EU hosting: If your HappyFox account is EU-hosted, use .happyfox.net instead of .happyfox.com
  • Concurrency: HappyFox does not support concurrent API calls to the staff-reply endpoint for the same ticket. Serialize writes per ticket.
  • Sandbox environment: HappyFox does not provide a free self-serve sandbox. Request a staging account through your HappyFox account manager or use a dedicated test category in production with a rollback plan.
Danger

The 10-Minute Lockout Is Punishing. If you hit the 300 POST/minute ceiling during a migration run, you lose 10 full minutes — not seconds. A single burst miscalculation during a 10,000-ticket import can add hours to total runtime. Build your import loop to stay at or below 250 POST/minute with jitter.

For a deeper breakdown of HappyFox's API behavior, see our guide on how to export data from HappyFox.

HappyFox API Error Reference

The following errors are the most common during migration imports. Build explicit handling for each:

HTTP Status Error Condition Typical Cause Recovery Action
400 Bad Request email already exists Duplicate contact email on POST Use GET lookup before POST; reuse existing contact ID
400 Bad Request category is required Missing or invalid category ID on ticket create Validate category ID mapping before import run
400 Bad Request invalid priority Priority ID not in account's priority list Pre-fetch valid priority IDs from /priorities/
400 Bad Request malformed HTML Invalid HTML in ticket body or reply Sanitize mrkdwn output; strip unsupported tags
400 Bad Request size exceeds limit Attachment total > 25 MB in single request Split attachments across multiple update calls
401 Unauthorized Auth failure Wrong API key or auth code Regenerate credentials under Apps → Goodies → API
429 Too Many Requests Rate limit exceeded > 300 POST/min or > 500 GET/min Back off 10 minutes minimum; do not retry immediately
500 Internal Server Error Server-side failure Transient or malformed payload Retry with exponential backoff; log payload for inspection

Migration Approaches: CSV vs. API

HappyFox CSV Import (Limited)

HappyFox offers a CSV import path handled by their support team (support@happyfox.com). You prepare a CSV and they run the import. (support.happyfox.com)

What it supports: Contact name, email, subject, category, priority, status, a "Text" column for the first message, a "Private Note" column for bundled subsequent replies, and ticket custom fields as columns.

What it does not support: Individual conversation replies as separate updates (all correspondence is clubbed into a single private note), attachments, or timestamp preservation on individual messages.

Verdict: Only viable when conversation history fidelity is not a requirement and ticket volume is low. For most migrations, this is insufficient.

Extract from Unthread's REST API, transform in a middleware script, load through HappyFox's API. This is the only path that preserves full conversation threads with discrete staff replies, client replies, private notes, and attachments.

This approach requires engineering time (Python or Node.js recommended), Slack API access for identity resolution, and 2–4 weeks for a mid-size migration (5,000–25,000 conversations).

Step-by-Step Migration Process

Step 1: Audit and Extract from Unthread

Extract in dependency order — downstream steps depend on reference data being available first.

  1. CustomersPOST /customers/list. Collect all customer records with email, name, and custom fields.
  2. AccountsPOST /accounts/list. These map to HappyFox Contact Groups.
  3. Ticket TypesPOST /ticket-types/list. These inform HappyFox Category setup.
  4. Tags — Extract from conversation records (tags are embedded in conversation objects).
  5. ConversationsPOST /conversations/list. Pull all conversations with full field selection using cursor-based pagination.
  6. Messages per conversation — Fetch messages for each conversation. This is an N+1 call pattern. For 10,000 conversations with an average of 8 messages each, budget for 10,000+ API calls just for message bodies.
  7. Attachments — Download every attachment URL referenced in messages. Store locally or in S3 before the Unthread account is decommissioned. Slack-hosted files require a valid Slack bearer token in the Authorization header; the download URL alone is not sufficient.
Tip

Pull metadata first. Extract Customers, Accounts, Ticket Types, and Tags before touching conversations. This prevents broken mappings later and gives you the reference IDs needed for the transformation step.

Step 2: Resolve Slack Identities to Email Addresses

Identity resolution has three distinct user populations, each requiring different handling:

Internal workspace users (standard):

  1. Call the Slack users.info API to get the user's profile.email. This is a Tier 4 method — Slack's standard rate limit for Tier 4 is 100 requests per minute with burst capacity. Identity resolution for even large workspaces completes quickly.
  2. If the email field is empty (common for bot users), check your internal directory (LDAP, Okta, Google Workspace).

Deactivated workspace users: The Slack users.info endpoint returns profile data for deactivated users, but the profile.email field may be empty if the user was deprovisioned in your identity provider before being deactivated in Slack. Check your HRIS or identity provider for former-employee email addresses. If unresolvable, create a placeholder contact with a convention like former-employee-<slackId>@yourcompany.internal and flag for manual cleanup.

Slack Connect users (external workspace members): As we've noted in our Unthread to Help Scout migration guide, Slack Connect channels allow external organizations' users to participate in your workspace. These users have Slack IDs in your workspace, but users.info returns their profile from their organization — including an email address that belongs to their employer's domain, not yours. The challenge: you cannot call users.info for external users if they have left their organization or if your Slack app lacks the correct scopes.

Resolution strategy for Slack Connect users:

  1. Identify Slack Connect users by checking is_email_confirmed and enterprise_user fields in the Slack user object, or by matching user IDs against channel membership in channels flagged as Slack Connect.
  2. For users where profile.email is available, use it directly — this is the external customer's actual email address.
  3. For users where email is unavailable, cross-reference your CRM or account records using the company name from profile.real_name or profile.team.
  4. As a last resort, create a placeholder using the Slack workspace domain: slack-connect-<userId>@external.placeholder and add a tag needs-identity-resolution for manual follow-up.

Store the completed map as Slack_User_ID → HappyFox_Contact_ID for use during ticket loading.

Step 3: Build the Target Schema in HappyFox

Set up the receiving environment before importing any tickets:

  1. Contact Groups — Create groups mirroring Unthread Accounts. Do this before creating contacts so domain auto-add rules take effect.
  2. Contacts — Create or match contacts using resolved email addresses. Use the deduplication lookup strategy described above. HappyFox enables portal login by default for new contacts — review this setting before bulk-importing external customers. (support.happyfox.com)
  3. Categories — Create one HappyFox Category for each major queue. Map Unthread Ticket Types to categories or custom fields as appropriate. Note the category IDs returned by the API.
  4. Statuses — Map Unthread statuses (open, in_progress, on_hold, closed) to HappyFox status IDs fetched from /statuses/. Create custom statuses for anything without a native equivalent.
  5. Priorities — Build an explicit priority crosswalk. Fetch valid priority IDs from /priorities/ — do not hardcode assumed values. Unthread priority scores (e.g., 3, 5, 7, 9) must map to HappyFox priority IDs.
  6. Custom Fields — Recreate each Unthread custom field with the correct HappyFox type (text, dropdown, number, date, multi-select, checkbox). HappyFox supports user-selector-type fields only as text; file-upload type fields must become ticket attachments. Multi-level dependent dropdowns in HappyFox are parent-child relationships — restructure any flat Unthread dropdowns that should be hierarchical before import. Fetch field IDs from API metadata endpoints — do not copy IDs from admin UI URLs. (support.happyfox.com)
  7. Staff/Agents — Create or invite all agents. Map Unthread assignee IDs to HappyFox staff IDs.
  8. Tags — No pre-creation needed. HappyFox creates tags on-the-fly when assigned to tickets.

Step 4: Transform and Load

Your transformation script handles the structural translation between the two data models.

Slack mrkdwn → HTML conversion is mandatory. If you push raw Slack markdown into HappyFox, it renders as plain text with visible asterisks and brackets. Your translation layer must handle:

  • *text*<b>text</b>
  • _text_<i>text</i>
  • <http://url|text><a href="http://url">text</a>
  • <@U12345678> → resolved display name (use your identity map)
  • <#C12345|channel-name> → plain text channel reference
  • `code`<code>code</code>
  • Triple backtick blocks → <pre><code>block</code></pre>
  • :emoji_name: → Unicode character or strip and replace with text
  • \n line breaks → <br> in HTML context
  • &amp;, &lt;, &gt; entity encoding (Slack encodes these in mrkdwn; decode before re-encoding for HTML)

After conversion, run the HTML through a sanitizer that allows only b, i, a, br, pre, code, ul, ol, li, and p tags. HappyFox will reject or display incorrectly any payloads containing unsupported tags or malformed HTML structure.

Loading sequence per conversation:

  1. Create the ticket via POST /api/1.1/json/tickets/ with the first customer-visible message as the ticket body. Set created_at, category, status, priority, assignee, tags, and required custom fields. Capture the returned ticket_id.
  2. Add subsequent messages in chronological order. Use staff_update for public agent replies, staff_pvtnote for internal notes, and user_reply for customer messages. Sort all messages by created_at before loading — Slack threads can be updated out of chronological order.
  3. Upload attachments per message using multipart/form-data. Keep total payload under 25 MB per request. If a single message's attachments exceed 25 MB, split across multiple update calls and note the split in each update body.
  4. Set the final status after all updates are loaded.
import time
import random
 
# Rate limiting: stay at 250 POST/min with jitter
POST_INTERVAL = 60.0 / 250  # 0.24 seconds per request
last_request_time = 0
 
def rate_limited_post(url, **kwargs):
    global last_request_time
    elapsed = time.time() - last_request_time
    sleep_time = POST_INTERVAL - elapsed
    if sleep_time > 0:
        # Add jitter to avoid synchronized bursts
        time.sleep(sleep_time + random.uniform(0, 0.05))
    
    response = requests.post(url, **kwargs)
    last_request_time = time.time()
    
    if response.status_code == 429:
        print("Rate limit hit. Sleeping 10 minutes.")
        time.sleep(610)  # 10 min + buffer
        return rate_limited_post(url, **kwargs)
    
    response.raise_for_status()
    return response
 
def migrate_conversation(conversation, category_map, priority_map, agent_map, contact_map):
    contact = ensure_happyfox_contact(conversation.requester_email, contact_map)
    
    ticket_resp = rate_limited_post(
        f"{HAPPYFOX_BASE}/tickets/",
        json={
            "subject": (conversation.title or conversation.first_message.text[:100])[:255],
            "text": convert_mrkdwn_to_html(conversation.first_message.text),
            "category": category_map[conversation.ticket_type_id],
            "email": contact.email,
            "created_at": conversation.created_at,
            "priority": priority_map.get(conversation.priority),
            "tags": ",".join([t["name"] for t in conversation.tags]),
            "update_customer": False,  # Suppress notifications during import
        },
        auth=HAPPYFOX_AUTH,
    )
    ticket = ticket_resp.json()
 
    for message in sort_by_timestamp(conversation.messages[1:]):
        html_body = convert_mrkdwn_to_html(message.text)
        # Prepend original timestamp since reply endpoints don't support created_at
        timestamp_prefix = f"<p><em>[Originally sent: {message.created_at}]</em></p>"
        html_body = timestamp_prefix + html_body
        
        if message.is_internal:
            endpoint = f"{HAPPYFOX_BASE}/tickets/{ticket['id']}/staff_pvtnote/"
        elif message.author_type == "agent":
            endpoint = f"{HAPPYFOX_BASE}/tickets/{ticket['id']}/staff_update/"
        else:
            endpoint = f"{HAPPYFOX_BASE}/tickets/{ticket['id']}/user_reply/"
        
        rate_limited_post(
            endpoint,
            json={
                "text": html_body,
                "staff": agent_map.get(message.author_id),
                "update_customer": False,
            },
            auth=HAPPYFOX_AUTH,
        )
    
    return ticket["id"]

If a historical agent no longer exists in HappyFox, import through a dedicated migration user and stamp the original author and timestamp into the message body so the audit trail stays readable.

Warning

Timestamp Preservation. HappyFox's ticket creation endpoint supports created_at for backdating the ticket itself. Reply and private-note endpoints do not document a historical timestamp override. Some enterprise accounts may have access to this through HappyFox support. If unavailable, prepend original timestamps to message bodies as shown in the code above. Validate this behavior in a staging category before promising exact chronology to stakeholders. (support.happyfox.com)

Set update_customer=false on all staff replies during import. This suppresses email notifications to contacts for historical messages — a critical setting to verify before running any import batch. The default behavior may vary by account configuration.

Stay at or below 250 POST/minute to avoid the 10-minute lockout. For tickets without attachments, the bulk create endpoint (up to 100 tickets per request) can speed up the initial ticket creation phase — but you must still add thread updates and attachments individually.

Step 5: Handle Attachments

Attachments need their own pipeline:

  1. Download each file from Unthread by file ID or attachment URL. If files are hosted on Slack's servers (common — Unthread attachments are typically stored in the connected Slack workspace), include Authorization: Bearer <slack_bot_token> in the download request. The URL alone will return 302 or 403 without the auth header.
  2. Re-upload to HappyFox as multipart/form-data during ticket or update creation.
  3. If an image must render inline in the ticket body (not just as an attachment), use HappyFox's inline-attachment upload endpoint, which returns a temporary URL for embedding in the HTML body.
  4. HappyFox enforces a 25 MB total per request, not per file. A single Unthread message with many files may require splitting across multiple HappyFox updates.
  5. HappyFox attachment URLs returned in API responses expire after 5 minutes. If your validation script fetches ticket data and then checks attachments, do both in a single pass — do not store the URL and check it later. (support.happyfox.com)

If an attachment exceeds HappyFox's limit, drop it and append a note to the message body: [Attachment omitted: filename.ext — exceeded 25 MB request size limit].

Step 6: Validate

Validation is not optional. Run these checks against every migration batch:

Check Method Pass Criteria
Total ticket count Compare Unthread conversation count vs. HappyFox ticket count Exact match
Message count per ticket Sample 50 tickets, compare message counts Exact match
Message type distribution Sample 20 tickets, verify staff/client/private split Matches source classification
Attachment presence Sample 30 tickets with attachments, verify files open within 5 min of fetch All files accessible
Contact assignment Verify ticket requester matches Unthread customer Email match
Contact deduplication Query HappyFox contacts for known duplicate emails Zero duplicates
Category mapping Spot-check 20 tickets per category Correct category
Custom field values Sample 30 tickets with custom fields Values preserved
Tag assignment Sample 20 tagged tickets All tags present
Status accuracy Check resolved/closed tickets Correct status
Contact Group visibility Log in as a test contact, verify group-level ticket visibility in portal Expected behavior
Slack Connect tickets Sample 10 tickets from Slack Connect channels Correct external contact assignment

Automate what you can. Build a comparison script that fetches HappyFox tickets via the API and diffs update counts, custom field values, and tag lists against your source extraction data.

Step 7: Delta Sync and Cutover

  1. Historical sync: Run the full migration pipeline for all closed conversations and older open tickets. This can take days depending on volume and rate limits.
  2. UAT: Have your support team verify categories, message formatting, and attachments in HappyFox.
  3. Freeze: Stop new intake on the Unthread side. Unthread allows turning automatic conversation tracking off, which gives you a clean way to stop Slack-side ticket creation without deleting historical data. (docs.unthread.io)
  4. Delta sync: Run the script one final time, querying Unthread for conversations created or updated since the historical sync began. Filter using createdAt or updatedAt in your /conversations/list where clause.
  5. Cutover: Route all new support requests into HappyFox. Update support addresses and portal links.

Do not assume old threads will continue naturally after the move. HappyFox threads email replies using ticket IDs in the subject line and email headers. Imported historical tickets do not carry forward Slack thread behavior. Communicate a clear cutover point and expect some straggler replies to land outside the migrated history. (support.happyfox.com)

For a detailed zero-downtime cutover playbook, see our zero-downtime migration guide.

Edge Cases and Failure Modes

Slack Connect Users (External Identity). B2B customers using Slack Connect channels are the hardest identity resolution case. These users belong to external Slack workspaces. Their Slack user IDs exist in your Unthread data, but they may not appear in your Slack workspace member directory, or their profile email may point to their employer's domain rather than a domain you recognize. Approach: identify Slack Connect channels explicitly using the Slack conversations.info API (which returns is_ext_shared: true), flag all user IDs from those channels as external, and resolve email addresses from your CRM using the account/company association rather than the Slack API. If no CRM match is available, fall back to the profile email from users.info and verify it matches an expected customer domain before creating the contact.

Deactivated Slack Users. If an Unthread conversation involves a Slack user who has been deactivated or removed from the workspace, the Slack API may fail to return their email. Your script needs a fallback — assign these to a placeholder contact like former-employee@yourdomain.com and flag for manual cleanup.

Conversation Titles. Unthread auto-generated titles from the first message. If that message is long, the title may be unhelpful. Truncate HappyFox ticket subjects to 255 characters and append the Unthread conversation ID as a reference tag for traceability.

Emoji Reactions. Unthread captures Slack emoji reactions. HappyFox has no native equivalent for message-level reactions. Strip reactions entirely (recommended) or append them as text at the bottom of the message body.

Custom Field Type Mismatches. Unthread ticket-type fields can include user selectors and file uploads. HappyFox ticket fields cover text, number, dropdown, multiple options, date, and currency. User-selector values must be flattened into text (store the resolved display name), and file-upload fields must become ticket attachments rather than field values.

Webhook and Event Stream Data. If your team built custom integrations against Unthread's webhook/event stream (e.g., automated escalations, CRM sync triggers, real-time dashboards), those workflows have no migration path. Unthread webhook subscriptions cannot be exported or converted to HappyFox Smart Rules automatically. Audit all active webhook endpoints before migration, document the trigger conditions and downstream actions, and rebuild each as a HappyFox Smart Rule or Smart Action after cutover.

Slack Thread Grouping. Unthread can group top-level messages from the same user by time interval and split threaded replies into separate conversations. HappyFox's model is tickets plus updates. If your team relies on channel names, thread timestamps, or Slack URLs during investigations, preserve that context in a private note on each imported ticket: include the original Slack channel name, thread timestamp (ts), and Slack deep-link URL in the format https://yourworkspace.slack.com/archives/<channel_id>/p<ts_without_decimal>. (docs.unthread.io)

Contact Group Permissions. HappyFox Contact Groups carry visibility permissions — contacts can view their own tickets or all tickets in the group. Unthread Accounts don't have an equivalent permission model. Decide on visibility settings before migration to avoid exposing ticket data unexpectedly.

Customer Notifications. Verify that update_customer=false is set on every staff reply and private note during the import run. HappyFox's default notification behavior can vary by account configuration. A misconfigured import that sends notification emails for every historical message replay will trigger hundreds or thousands of unwanted emails to customers simultaneously.

What Cannot Be Migrated

Some data and configuration will not transfer through any automated path:

  • Smart Rules and Automations — HappyFox automations are UI-configured, not API-importable. Screenshot your Unthread automations and rebuild manually.
  • SLA Policies — Must be reconfigured in HappyFox manually.
  • Knowledge Base Articles — Unthread exposes KB article APIs, but HappyFox's public API is read-only for KB content. Articles must be created through the HappyFox UI or a separate process. (docs.unthread.io)
  • AI Workflows — Unthread's AI responses and auto-classification rules have no import path. Rebuild using HappyFox's automation features.
  • Slack Channel Associations — Per-channel routing rules do not translate. Configure HappyFox's intake channels from scratch.
  • CSAT Responses — Survey data from Unthread does not map to HappyFox's satisfaction survey system and cannot be imported via API.
  • Webhook Subscriptions — Unthread webhook endpoint registrations and event subscriptions have no export or import path.
  • Message-level Emoji Reactions — HappyFox has no equivalent data structure.
  • Slack Thread Permalinks — Can be preserved only as text in private notes, not as functional deep-links.

Rollback Planning

Before running the production migration:

  1. Tag all migrated tickets with a consistent tag like unthread-migration-2026 so they can be identified and bulk-deleted if needed.
  2. Keep Unthread active until validation is complete. Do not decommission the source until data integrity is confirmed.
  3. Run on a test category first. Create a temporary HappyFox category, import 100 conversations covering your most complex cases (attachments, custom fields, Slack Connect contacts, internal notes), validate thoroughly, then delete the test category and run the full migration.
  4. Maintain a migration ledger outside both systems. Store Unthread conversation IDs, source message IDs, target HappyFox ticket IDs, and target HappyFox contact IDs in a separate database or spreadsheet. This is what makes rollback, delta syncs, contact deduplication checks, and audit validation practical.
  5. Document rollback triggers. Define in advance what failure rate (e.g., > 2% ticket count mismatch, > 5% message count mismatch) triggers a halt and rollback, rather than making that call under pressure during a live migration.

Timeline Estimates

Migration Size Conversations Estimated Duration Notes
Small < 2,000 1–2 weeks Straightforward with minimal custom fields
Medium 2,000–15,000 2–4 weeks Identity resolution and custom field mapping add time
Large 15,000–50,000 4–6 weeks Rate limits become the dominant constraint; plan staged imports
Very Large 50,000+ 6–8+ weeks Consider parallel import streams and dedicated HappyFox support engagement

These estimates include planning, schema setup, script development, test runs, production migration, and validation. The API runtime itself is a fraction of the total — most time goes to mapping decisions, edge case handling, and validation.

API runtime calculation: A 10,000-ticket migration with an average of 5 updates per ticket means approximately 60,000 POST calls. At 250/minute safe throughput, that is 240 minutes (4 hours) of write-phase runtime alone. Add attachment uploads — assume 2 attachments per ticket average, each requiring a separate multipart POST — and total runtime extends to 10–12 hours. Plan for an overnight import window with monitoring in place.

When to DIY vs. When to Get Help

DIY makes sense when:

  • Dataset is under 2,000 conversations
  • Minimal custom fields (fewer than 5)
  • All contacts have email addresses — no Slack Connect identity resolution needed
  • No deactivated users in conversation history
  • You have a developer comfortable with Python or Node.js ETL scripts
  • Conversation history is nice-to-have, not mission-critical

Bring in help when:

  • Dataset exceeds 10,000 conversations with full thread history
  • Slack Connect channels are in scope (external identity resolution required)
  • You need zero-downtime cutover with a reconciliation pass
  • Attachments are heavy (average > 2 per ticket, or total attachment storage > 10 GB)
  • Custom field mapping is complex (10+ fields, dependent dropdowns, type transformations)
  • Timestamp preservation is a hard requirement
  • You cannot afford a failed migration attempt that damages data integrity or triggers mass customer notifications

At ClonePartner, we have built custom Unthread extraction and HappyFox import pipelines across multiple engagements. We handle identity resolution (including Slack Connect), field mapping, rate limit management, contact deduplication, and validation — typically completing mid-size migrations in days rather than the weeks it takes to build and debug scripts from scratch.

Frequently Asked Questions

Is there a direct migration tool from Unthread to HappyFox?
No. HappyFox does not list Unthread as a supported import source, and Unthread has no export-to-HappyFox feature. You must extract data via the Unthread REST API, transform it, and load it through the HappyFox API.
Can I use HappyFox's CSV import for an Unthread migration?
Only for a lightweight archive. HappyFox's CSV import supports the first ticket message and a bundled private note for subsequent replies, but it cannot import individual conversation updates as separate entries. Attachments are also excluded. For full thread fidelity, use the API.
How do I handle Slack user IDs when migrating to HappyFox?
HappyFox requires email addresses for every Contact. Use the Slack users.info API to resolve each Slack user ID to an email address. For unresolvable IDs (deactivated users, bots), create placeholder contacts and flag them for manual cleanup post-migration.
What are HappyFox's API rate limits for migration?
HappyFox allows 500 GET requests/minute and 300 POST requests/minute. Exceeding either triggers a 10-minute lockout. For a 10,000-ticket migration with conversation threads, expect 4+ hours of API write time at safe throughput (250 POST/minute).
Will HappyFox preserve original reply timestamps on imported tickets?
Ticket creation time can be set with created_at, but HappyFox's reply and private-note endpoints do not document a historical timestamp override. Some enterprise accounts may have access through HappyFox support. Test in a sandbox before promising exact chronological parity.

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