---
title: "Unthread to HappyFox Migration: A Technical Guide"
slug: unthread-to-happyfox-migration-a-technical-guide
date: 2026-08-18
author: Raaj
categories: [Unthread, HappyFox, Migration Guide]
excerpt: "A step-by-step technical guide for migrating from Unthread to HappyFox. Covers API extraction, data model mapping, rate limits, edge cases, and validation."
tldr: "No native migration path exists. Extract via Unthread's API, resolve Slack IDs to emails, transform Slack mrkdwn to HTML, and load through HappyFox's v1.1 API."
canonical: https://clonepartner.com/blog/unthread-to-happyfox-migration-a-technical-guide/
---

# Unthread to HappyFox Migration: A Technical Guide


# Unthread to HappyFox Migration: A Technical Guide

> [!NOTE]
> **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](https://clonepartner.com/blog/blog/unthread-to-freshdesk-migration-a-technical-guide/), 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](https://docs.unthread.io/docs/in-app-support/customer-portal-overview?utm_source=openai))

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](https://docs.unthread.io/docs/global-workspace-settings/accounts?utm_source=openai))

### 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](https://clonepartner.com/blog/blog/how-to-migrate-happyfox-to-zendesk-the-technical-guide/), 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](https://support.happyfox.com/kb/article/955-how-to-create-a-category-/?utm_source=openai))

### 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:

```json
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](https://clonepartner.com/blog/blog/how-to-export-data-from-unthread-methods-api-limits-formats/).

### 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.

> [!CAUTION]
> **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](https://clonepartner.com/blog/blog/how-to-export-data-from-happyfox-methods-api-limits-gaps/).

### 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](https://support.happyfox.com/kb/article/519-ticket-migration-csv-import/?section_id=108))

**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.

### API-to-API Migration (Recommended)

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. **Customers** — `POST /customers/list`. Collect all customer records with email, name, and custom fields.
2. **Accounts** — `POST /accounts/list`. These map to HappyFox Contact Groups.
3. **Ticket Types** — `POST /ticket-types/list`. These inform HappyFox Category setup.
4. **Tags** — Extract from conversation records (tags are embedded in conversation objects).
5. **Conversations** — `POST /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](https://clonepartner.com/blog/blog/unthread-to-help-scout-migration-the-technical-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](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))
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](https://support.happyfox.com/kb/article/360-api-for-happyfox/?section_id=131&utm_source=openai))
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.

```python
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](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))

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](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))

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](https://docs.unthread.io/docs/setting-up-your-account/tracking-new-conversations?utm_source=openai))
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](https://support.happyfox.com/kb/article/954-ticket-threading/?section_id=115))

For a detailed zero-downtime cutover playbook, see our [zero-downtime migration guide](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/).

## 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](https://docs.unthread.io/docs/conversational-ticketing/how-conversations-are-tracked))

**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](https://docs.unthread.io/docs/api-docs/api-reference?utm_source=openai))
- **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.

> Get a free 30-minute call with our engineers. We'll review your Unthread setup, map the data model to HappyFox, and give you an honest assessment of scope, timeline, and whether you actually need us.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## 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.
