---
title: "Freshchat to HappyFox Migration: A Technical Guide"
slug: freshchat-to-happyfox-migration-a-technical-guide
date: 2026-07-22
author: Roopi
categories: [Migration Guide, HappyFox]
excerpt: "A technical guide to migrating from Freshchat to HappyFox — covering API extraction, conversation-to-ticket mapping, rate limits, and validation."
tldr: "Freshchat stores conversations as message streams; HappyFox uses tickets. Migration requires API extraction, message-to-reply transformation, and batch loading under HappyFox's strict rate limits with a 10-minute lockout on breaches."
canonical: https://clonepartner.com/blog/freshchat-to-happyfox-migration-a-technical-guide/
---

# Freshchat to HappyFox Migration: A Technical Guide


# Freshchat to HappyFox Migration: A Technical Guide

> [!NOTE]
> **TL;DR: Freshchat to HappyFox Migration**
>
> Freshchat is a conversation-first messaging platform; HappyFox is a ticket-first help desk. There is no native migration path between them. You must extract data through the Freshchat REST API (v2), transform conversations into ticket-compatible payloads, and load them into HappyFox via its REST API (v1.1). The hardest part is not the data volume — it is the structural mismatch. Each Freshchat conversation contains a stream of individual messages that must be flattened into a single HappyFox ticket with ordered staff and contact updates. HappyFox accepts bulk ticket creation at up to 100 tickets per request, but enforces a global rate limit of 300 POST requests per minute with a 10-minute lockout on 429 errors. At a conservative throttle of 200 POST/min, a mid-size dataset of 25,000 conversations averaging 8 messages each yields roughly 200,000 total API calls — expect 3–7 calendar days of continuous runtime for the load phase alone, plus 2–5 days each for extraction, transformation, and validation. Bot interactions, CSAT data, automations, and smart assignments cannot be migrated programmatically and must be rebuilt.
>
> *Last updated: June 2025. API behaviors verified against Freshchat API v2 and HappyFox API v1.1 as of this date.*

## What This Migration Actually Involves

**Freshchat** is Freshworks' modern messaging and customer engagement platform. It handles real-time conversations across web chat, WhatsApp, Facebook Messenger, Instagram, Apple Business Chat, and email through a unified agent inbox. Its data model is conversation-centric: every interaction is a conversation containing a stream of messages, each with its own sender, timestamp, and message type.

**HappyFox** is a traditional, ticket-based help desk. It organizes support requests into categories, assigns them to staff, and tracks them through status workflows. Its data model is ticket-centric: every request is a ticket with a subject, description, and a series of staff and contact updates.

The migration is a structural translation, not a copy. You are converting a real-time messaging data model into a sequential ticketing data model.

> [!WARNING]
> **Terminology check:** Accounts, Leads, Opportunities, and Activities are not native objects in Freshchat's core chat API. The documented APIs focus on users, conversations, messages, groups, channels, reports, and conversation properties, with some property behavior available only for accounts bundled with Freshsales Suite. If you depend on CRM-side data from the wider Freshworks stack, run that as a separate migration lane. ([developers.freshchat.com](https://developers.freshchat.com/api/))

### Why Teams Move from Freshchat to HappyFox

The most common triggers:

- **Ticket-centric workflow needs.** Teams that outgrow live-chat-only workflows and need structured ticket management with categories, SLAs, and formal status tracking.
- **Pricing model.** HappyFox offers unlimited-agent plans (Enterprise and Enterprise Plus tiers) — attractive for large teams where per-agent pricing compounds quickly.
- **Simpler tooling.** Teams that find Freshworks' fragmented product suite (Freshchat, Freshdesk, Freshsales) overlapping and expensive, and want a single help desk.
- **Multi-brand segmentation.** HappyFox provides strict multi-brand and multi-tenant categorization, which benefits managed service providers and agencies.
- **Knowledge base consolidation.** HappyFox bundles a built-in knowledge base with hierarchical sections, articles, and a customer-facing support center.

## Core Data Model Differences

Before mapping anything, understand where the two platforms diverge structurally:

| Concept | Freshchat | HappyFox |
|---|---|---|
| Primary record | Conversation (message stream) | Ticket (subject + updates) |
| Contact record | User (`/v2/users`) | Contact (`/api/1.1/json/users/`) |
| Agent record | Agent (`/v2/agents`) | Staff (`/api/1.1/json/staff/`) |
| Routing unit | Channel / Group / Topic | Category |
| Message thread | Individual messages in a conversation | Staff updates + contact replies on a ticket |
| Custom data | Conversation properties | Ticket custom fields + contact custom fields |
| Labels / Tags | Labels on conversations | Tags on tickets |
| Organization | Not native (from Freshsales Suite) | Contact Groups (grouped by domain) |
| Knowledge base | Not native to Freshchat (separate Freshdesk product) | Built-in KB with Sections → Articles hierarchy |
| Authentication | Bearer token (API key) | HTTP Basic Auth (API key + auth code) |
| API version | v2 | v1.1 |

The most consequential difference: Freshchat conversations are flat streams of messages. HappyFox tickets expect a description (the initial message) plus a series of structured updates. Each Freshchat message must be classified as either a contact reply or a staff update and written to the correct HappyFox endpoint.

## Migration Approaches

### 1. Native CSV Export + Manual Import

**How it works:** Export conversation data from Freshchat's built-in reporting as CSV, then manually create tickets in HappyFox via the UI. Freshchat's Chat-Transcript report is constrained to **24-hour windows**, and raw report extraction cannot start more than **15 months** before the current date. HappyFox's native spreadsheet imports support contacts and contact groups, not full ticket history. ([developers.freshchat.com](https://developers.freshchat.com/api/))

**When to use it:** Only for archival purposes on very small datasets (under 500 conversations) where you do not need full conversation history.

**Pros:**
- No code required
- Quick for tiny datasets

**Cons:**
- Freshchat's CSV export flattens the entire chat transcript into a single description field — individual messages are not separated
- No conversation threading, per-message timestamps, or sender attribution
- HappyFox has no native CSV import for tickets
- Custom fields, tags, and agent assignments are lost
- History window is capped at 15 months

**Complexity:** Low
**Scalability:** Not viable beyond 500 records
**Risk:** High data loss. This is an archive, not a migration.

### 2. API-Based Migration (Recommended)

**How it works:** Extract all data from Freshchat's REST API v2 (conversations, messages, users, agents, groups), transform into HappyFox-compatible payloads, and load via HappyFox's REST API v1.1.

**When to use it:** Any migration where you need full conversation history, timestamps, and agent attribution.

**Pros:**
- Full fidelity: every message, every timestamp, every sender
- Custom fields, tags, and assignments preserved
- Repeatable and testable
- Handles large datasets with proper batching

**Cons:**
- Requires dedicated engineering time (see throughput calculation below for sizing)
- Must handle rate limits on both sides
- Attachments require separate handling (multipart/form-data for HappyFox)

**Complexity:** High
**Scalability:** Suitable for datasets of any size with proper pagination and throttling
**Risk:** Medium — manageable with test runs and validation

### 3. Middleware Platforms (Zapier, Make)

**How it works:** Configure a Zapier or Make workflow to trigger on Freshchat events and create corresponding HappyFox tickets. Make documents Freshchat modules for monitoring, creating, and retrieving conversations. Zapier documents a HappyFox app with a new ticket trigger and create ticket action. ([apps.make.com](https://apps.make.com/freshchat?utm_source=openai))

**When to use it:** Only for ongoing sync of **new** conversations after migration, not historical backfill.

**Pros:**
- No custom code for basic flows
- Good for ongoing forwarding after migration

**Cons:**
- Cannot migrate historical data in bulk
- Zapier's Freshchat trigger is limited to new conversations only
- No control over message threading or ordering
- Per-task pricing becomes expensive at scale

**Complexity:** Low
**Scalability:** Not suitable for historical migration
**Risk:** High for full migration; acceptable for post-migration sync

### 4. Custom ETL Pipeline

**How it works:** Build a dedicated extract-transform-load pipeline using Python, Node.js, or a data integration tool (e.g., Airbyte, dlt) to pull Freshchat data into a staging database, transform it, and push to HappyFox.

**When to use it:** Enterprise migrations with complex transformation requirements, multi-source consolidation, or compliance-driven auditing needs.

**Pros:**
- Full control over transformation logic
- Staging layer supports validation before loading
- Supports deduplication, enrichment, and custom business rules
- Audit trail in the staging layer

**Cons:**
- Highest engineering investment
- Requires maintaining infrastructure (staging DB, job scheduler)
- Over-engineered for most mid-market migrations

**Complexity:** High
**Scalability:** Enterprise-grade
**Risk:** Low if properly tested, but high opportunity cost

### 5. Managed Migration Service

**How it works:** A specialist migration team handles extraction, transformation, loading, and validation end-to-end.

**When to use it:** When engineering bandwidth is limited, timelines are tight, or the migration involves complex data relationships you cannot afford to get wrong.

**Pros:**
- Fastest time to completion
- Risk transferred to experienced practitioners
- Handles edge cases (attachments, bot conversations, CSAT data)
- Zero engineering distraction

**Cons:**
- External cost
- Requires vendor trust and data access

**Complexity:** Low (for your team)
**Scalability:** Any size
**Risk:** Lowest, assuming a qualified provider

### Comparison Table

| Approach | Best For | Complexity | Historical Data | Full Threading | Cost |
|---|---|---|---|---|---|
| CSV Export | Archival only | Low | Partial (15-month max) | No | Free |
| API-Based | Full migration | High | Yes | Yes | Engineering time |
| Middleware | Ongoing sync | Low | No | No | Per-task fees |
| Custom ETL | Enterprise | High | Yes | Yes | Engineering + infra |
| Managed Service | Any | Low (your side) | Yes | Yes | Service fee |

### Recommendations

- **Small team, <5,000 conversations, in-house dev capacity:** API-based migration with a Python or Node.js script.
- **Enterprise, >50,000 conversations, compliance requirements:** Custom ETL or managed service.
- **No engineering bandwidth, any size:** Managed migration service.
- **Ongoing sync after migration:** Middleware (Zapier/Make) for new conversations only. Alternatively, use Freshchat webhooks (configured under Admin → Webhooks) to POST conversation events directly to a lightweight endpoint that creates HappyFox tickets — this avoids per-task middleware fees and gives you control over payload transformation during the cutover window.
- **Data older than 15 months or mixed with CRM-side objects:** Skip CSV as the primary plan. ([developers.freshchat.com](https://developers.freshchat.com/api/))

## Throughput Calculation: How Long Will the Load Phase Take?

This is the most common planning question, so here is a concrete formula.

**Given:**
- Target POST rate: 200/min (leaving 33% headroom below HappyFox's 300 POST/min limit)
- Each ticket requires: 1 POST for ticket creation + (N-1) POSTs for message updates (where N = total messages in the conversation)
- Attachments require additional POSTs (1 per attachment, multipart/form-data)

**Example: 25,000 conversations, average 8 messages each, 10% have 1 attachment**

| Component | API Calls | Calculation |
|---|---|---|
| Ticket creation | 25,000 | 25,000 conversations ÷ 100 per bulk request = 250 bulk POSTs |
| Message updates | 175,000 | 25,000 × 7 subsequent messages per conversation |
| Attachments | 2,500 | 25,000 × 10% × 1 attachment |
| **Total POST calls** | **~177,750** | |
| **Estimated load time** | **~14.8 hours** | 177,750 ÷ 200 per minute ÷ 60 |

Add a 30–50% buffer for retries, pagination delays, and 429 recovery. Realistic estimate: **20–25 hours of continuous runtime** for the load phase of this example. Extraction and transformation add comparable time depending on Freshchat's response latency, so plan for the full migration (extract + transform + load + validate) to span **3–7 calendar days** for a 25,000-conversation dataset.

> [!NOTE]
> **Freshchat extraction rate:** Freshchat does not publish explicit rate limits. In practice, sustained extraction at 2–3 requests/second with 0.3–0.5s sleeps between paginated calls is generally safe on Growth and Pro plans. Monitor the `X-RateLimit-Remaining` header and back off dynamically if it drops below 10.

## When a Managed Migration Service Makes Sense

DIY migrations between Freshchat and HappyFox carry specific risks:

- **Conversation threading breaks silently.** If messages are loaded out of order or without correct sender attribution, the ticket history becomes unreadable. Count-based validation will not catch this — you need content-level spot checks.
- **Attachment handling is non-trivial.** HappyFox requires multipart/form-data for ticket attachments, and each attachment must be uploaded separately. Freshchat inline images require downloading, re-hosting, and re-linking. Freshchat attachment URLs may be time-limited (signed URLs that expire within minutes to hours).
- **Rate limit lockouts compound.** HappyFox's 10-minute lockout on 429 errors means a single burst of requests can stall your entire migration pipeline. In a 25,000-conversation migration, three lockout events add 30 minutes of dead time each occurrence.
- **Bot conversations pollute ticket data.** Freshchat bot interactions (Freddy AI, custom bots) look like conversations but often contain system-generated messages that should not become tickets.
- **Token expiration.** Freshchat API tokens can expire during long-running extractions, returning 401 errors that require automated refresh logic.

**When NOT to build in-house:**

- Your team lacks experience with rate-limit backoff algorithms.
- You cannot afford any operational downtime during cutover.
- You need exact thread recreation, attachment fidelity, or CRM-side relationship handling.
- The cost of a "two-week project" becoming a multi-month operational distraction outweighs the cost of a specialist.

## Pre-Migration Planning

### Data Audit Checklist

Before extracting anything, inventory what exists in Freshchat:

- [ ] **Conversations:** Total count, date range, status breakdown (new, assigned, resolved, reopened)
- [ ] **Messages per conversation:** Average and maximum depth (affects API call count — see throughput calculation above)
- [ ] **Users/Contacts:** Total count, duplicates, contacts without conversations
- [ ] **Agents:** Active vs. deactivated, group memberships
- [ ] **Groups/Channels:** Mapping to HappyFox categories
- [ ] **Custom conversation properties:** Field names, types, values
- [ ] **Labels:** Full list for tag mapping
- [ ] **Attachments:** Count and total size in GB (affects migration duration and staging storage requirements)
- [ ] **Bot conversations:** Count of bot-only interactions (candidates for exclusion)
- [ ] **CSAT responses:** Whether these need to be preserved
- [ ] **Knowledge base content:** If using Freshdesk KB alongside Freshchat, count sections and articles for HappyFox KB migration
- [ ] **CRM-side data:** Any Freshsales Suite objects (Accounts, Leads, Opportunities) that need separate handling

### Define Migration Scope

Not everything should migrate. Use a scope sheet with four columns: **move**, **rebuild**, **archive**, and **drop**.

- **Exclude:** Bot-only conversations with no human agent involvement, test conversations, spam
- **Archive only:** Conversations older than your retention policy (consider a read-only export to S3 or a data warehouse instead of loading into HappyFox)
- **Migrate:** All human-agent conversations, contacts, custom field values, attachments, and knowledge base content (if applicable)
- **Rebuild:** Automations, SLA policies, canned responses, satisfaction surveys

### Migration Strategy

| Strategy | When to Use | Risk |
|---|---|---|
| **Big bang** | Small datasets (<5,000 conversations), short cutover window acceptable | Higher — all-or-nothing |
| **Phased** | Large datasets, critical operations | Lower — validate each phase |
| **Incremental** | Ongoing operations during migration | Lowest — but requires delta sync logic |

For most Freshchat-to-HappyFox migrations, a **phased approach** works best:

1. Extract and load historical data up to a specific cutoff date.
2. Train the team on HappyFox.
3. Migrate contacts first, then historical conversations in date-range batches.
4. Perform a final "delta" migration over a weekend to catch any new conversations created during the transition. For delta capture, configure a Freshchat webhook (`Admin → Webhooks`) to POST `conversation.created` and `message.created` events to a lightweight receiver endpoint. This gives you real-time capture of new conversations without polling.

## Data Mapping: Freshchat Objects to HappyFox

This is the section that determines whether your migration succeeds or fails.

### Object-Level Mapping

| Freshchat Object | HappyFox Object | Notes |
|---|---|---|
| User | Contact | Map `email`, `first_name`, `last_name`, `phone`, `created_time` |
| Agent | Staff | Match by email; create manually if needed (no bulk staff create API) |
| Conversation | Ticket | First message → ticket description; subsequent → updates |
| Message (user) | Contact reply | Appended as reply on the ticket |
| Message (agent) | Staff update | Appended via `/staff_update/` endpoint |
| Message (bot/system) | Private note or excluded | Decide upfront; most teams exclude bot-only content |
| Group | Category | Create categories in HappyFox first; map by name |
| Conversation property | Ticket custom field | Create fields in HappyFox first; match by type |
| Label | Tag | Direct mapping; create tags in HappyFox |
| Attachment | Ticket attachment | Download from Freshchat, upload via multipart/form-data |
| CSAT response | No direct equivalent | Store as a private note or custom field value |

For company-level data, **HappyFox contact groups** are the closest fit. Contact groups can group contacts, control visibility, and auto-add by domain. ([support.happyfox.com](https://support.happyfox.com/kb/article/699-manage-contact-groups/?section_id=128))

For **Leads** or **Opportunities** from the wider Freshworks stack, the realistic options are: keep them in the CRM, flatten only the fields you need into HappyFox custom fields, or archive them externally. HappyFox's published help desk API covers tickets, contacts, contact groups, assets, reports, and read-only KB access — CRM parity is not a 1:1 outcome. ([support.happyfox.com](https://support.happyfox.com/kb/article/360-api-for-happyfox/?section_id=131))

### Field-Level Mapping

| Freshchat Field | Type | HappyFox Field | Type | Transformation |
|---|---|---|---|---|
| `conversation_id` | UUID | Custom field (`cf_freshchat_id`) | String | Store for traceability |
| `status` | String (new, assigned, resolved, reopened) | `status` | Integer ID | Map to HappyFox status IDs via `GET /api/1.1/json/statuses/` |
| `assigned_agent_id` | UUID | `assignee` | Integer ID | Look up staff ID by email |
| `created_time` | ISO 8601 | `created_at` or custom field | Datetime | See timestamp section below |
| `user.email` | String | `email` | String | Primary contact identifier |
| `user.first_name` + `last_name` | String | `name` | String | Concatenate |
| `group_id` | UUID | `category` | Integer ID | Map via pre-created category lookup |
| `messages [].message_parts` | Array | Update body | HTML/text | Flatten message_parts into HTML body |
| `messages [].actor_type` | String (user/agent/system) | — | — | Determines if contact reply, staff update, or private note |
| `conversation_properties` | Key-value | `custom_fields` | Key-value (by field ID) | Match by field name; transform values |
| `labels []` | Array | `tags` | Array | Name match |

> [!WARNING]
> **Critical: Freshchat message_parts vs HappyFox update body**
>
> Freshchat messages use a `message_parts` array where each part can be text, image, file, or a structured element (buttons, carousels). HappyFox expects plain text or HTML in update bodies. You must write a transformer that converts each message_part type into HTML. Bot-generated structured elements (quick replies, carousels) will lose their interactive formatting — flatten them to text descriptions. Example: a quick-reply part `{"type": "quick_reply", "content": {"text": "Choose one:", "options": ["Billing", "Technical"]}}` should become `<p>Choose one: [Billing] [Technical]</p>`.

### Handling Relationships and Load Order

The load order matters. HappyFox requires valid IDs for referenced entities:

1. **Staff** — Create or verify manually (no bulk create via API)
2. **Categories** — Create via UI or API before loading tickets
3. **Custom fields** — Create ticket and contact custom fields via API (`POST /api/1.1/json/ticket_custom_fields/`)
4. **Contacts** — Load via API; use bulk create/update endpoint
5. **Tickets** — Load with references to staff IDs, category IDs, and contact emails
6. **Ticket updates** — Append messages as staff updates or contact replies
7. **Attachments** — Upload per-ticket via multipart/form-data POST

## Knowledge Base Migration

If you use Freshdesk's knowledge base alongside Freshchat and want to migrate articles into HappyFox's built-in KB, here is the mapping:

| Freshdesk KB Object | HappyFox KB Object | Notes |
|---|---|---|
| Category | Section | Top-level grouping |
| Folder | Sub-section | Nested within a section |
| Article | Article | HTML body, title, status |

**Extraction:** Use the Freshdesk API (not Freshchat) — `GET /api/v2/solutions/categories`, `GET /api/v2/solutions/folders/{id}/articles`.

**Loading:** HappyFox's public API v1.1 provides **read-only** access to KB articles (`GET /api/1.1/json/kb/sections/` and `GET /api/1.1/json/kb/articles/`). There is no documented public endpoint for creating or updating KB articles via API. This means you have three options:

1. **Manual recreation:** Copy-paste articles through the HappyFox UI. Feasible for <50 articles.
2. **HappyFox import tool:** Check with HappyFox support — some plans offer a bulk KB import feature via CSV.
3. **Browser automation:** For 50–500 articles, Selenium or Playwright scripts can automate the UI-based article creation. This is fragile but workable as a one-time operation.

For KB migrations exceeding 500 articles, contact HappyFox support to request API-based bulk import access or a direct database import.

## Migration Architecture

### Data Flow

```
Freshchat API (v2)          Staging Layer            HappyFox API (v1.1)
─────────────────    ──────────────────────    ─────────────────────────
GET /v2/agents    →  agents.json             →  Manual staff setup
GET /v2/users     →  contacts.json           →  POST /users/ (bulk)
GET /v2/conversations → conversations.json   →  POST /tickets/ (bulk, 100/req)
GET /v2/conversations/{id}/messages → msgs/  →  POST /ticket/{n}/staff_update/
                                              →  POST /ticket/{n}/user_reply/
Download attachments → attachments/          →  POST /ticket/{n}/ (multipart)
```

### Freshchat API Details

- **Base URL:** `https://{account}.freshchat.com/v2/`
- **Auth:** `Authorization: Bearer <API_KEY>`
- **Conversations list:** `GET /v2/conversations` — paginated via `page` and `items_per_page` (max 50 per page)
- **Messages:** `GET /v2/conversations/{id}/messages` — max 50 per page
- **Users:** `GET /v2/users` — up to 100 per page, use `pagination.next_link`
- **Rate limits:** Not explicitly published. Monitor `X-RateLimit-Remaining` and `X-RateLimit-Limit` response headers. In practice, sustained rates of 2–3 requests/second with 0.3–0.5s sleeps are generally safe on Growth and Pro plans. HTTP 429 on exceeded limits with a `Retry-After` header. ([developers.freshchat.com](https://developers.freshchat.com/api/))

### HappyFox API Details

- **Base URL:** `https://{subdomain}.happyfox.com/api/1.1/json/`
- **Auth:** HTTP Basic Auth with API key as username and auth code as password
- **Create ticket:** `POST /api/1.1/json/tickets/` — single or bulk (max 100 per request)
- **Staff update:** `POST /api/1.1/json/ticket/{ticket_number}/staff_update/`
- **User reply:** `POST /api/1.1/json/ticket/{ticket_number}/user_reply/`
- **Contacts:** `GET /api/1.1/json/users/?size=50&page=1` — size max 50
- **Rate limits:** 500 GET/min, 300 POST/min. Exceeding triggers HTTP 429 with a **10-minute cooldown**. ([support.happyfox.com](https://support.happyfox.com/kb/article/1148-rate-limiting/))

**HappyFox 429 response body format:**

```json
{
  "error": "Rate limit exceeded. Please retry after 600 seconds.",
  "code": 429
}
```

**HappyFox validation error response body format (HTTP 400):**

```json
{
  "error": "Validation failed",
  "errors": {
    "category": ["Invalid category ID"],
    "email": ["This field is required"]
  }
}
```

> [!CAUTION]
> **HappyFox's 10-minute lockout is the single biggest operational risk.** Unlike most APIs that use sliding windows, HappyFox blocks all API access for a full 10 minutes after a 429 response. A single burst of over-aggressive writes will halt your entire migration pipeline. Build in conservative throttling — target 200 POST/min maximum to leave headroom.

### HappyFox Bulk Ticket Creation Payload Format

The bulk ticket creation endpoint accepts a JSON array at the root level, not a wrapper object. Each element in the array follows the single-ticket schema:

```json
[
  {
    "subject": "Chat conversation abc12345",
    "text": "<p>Hi, I need help with my billing.</p>",
    "category": 3,
    "email": "customer@example.com",
    "name": "Jane Doe",
    "priority": 2,
    "assignee": 12,
    "t-cf-1": "fc-conv-uuid-abc12345",
    "tags": ["migrated", "chat"]
  },
  {
    "subject": "Chat conversation def67890",
    "text": "<p>How do I reset my password?</p>",
    "category": 3,
    "email": "john@example.com",
    "name": "John Smith",
    "priority": 1,
    "t-cf-1": "fc-conv-uuid-def67890",
    "tags": ["migrated", "chat"]
  }
]
```

**Notes on the bulk payload:**
- Maximum 100 ticket objects per request
- Custom field keys use the format `t-cf-{id}` where `{id}` is the custom field's integer ID from `GET /api/1.1/json/ticket_custom_fields/`
- The response is an array of created ticket objects, each containing the assigned `id` (ticket number)
- If any ticket in the batch fails validation, HappyFox may reject the entire batch — validate payloads individually before batching
- Attachments cannot be included in bulk ticket creation; they must be uploaded separately via multipart/form-data POSTs after ticket creation

## Step-by-Step Migration Process

### Step 1: Extract Data from Freshchat

```python
import requests
import json
import time
import os

FRESHCHAT_BASE = "https://yourcompany.freshchat.com/v2"
HEADERS = {
    "Authorization": "Bearer YOUR_API_KEY",
    "Accept": "application/json"
}
CHECKPOINT_FILE = "extraction_checkpoint.json"

def load_checkpoint():
    if os.path.exists(CHECKPOINT_FILE):
        with open(CHECKPOINT_FILE) as f:
            return json.load(f)
    return {"conversations_page": 1, "completed_conversations": []}

def save_checkpoint(state):
    with open(CHECKPOINT_FILE, "w") as f:
        json.dump(state, f)

def dynamic_backoff(response):
    """Back off based on X-RateLimit-Remaining header."""
    remaining = int(response.headers.get("X-RateLimit-Remaining", 100))
    if remaining < 10:
        time.sleep(5)
    elif remaining < 30:
        time.sleep(1)
    else:
        time.sleep(0.3)

def extract_conversations():
    checkpoint = load_checkpoint()
    conversations = []
    page = checkpoint["conversations_page"]
    while True:
        resp = requests.get(
            f"{FRESHCHAT_BASE}/conversations",
            headers=HEADERS,
            params={"page": page, "items_per_page": 50}
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
        if resp.status_code == 401:
            # Token expired — refresh and retry
            HEADERS["Authorization"] = f"Bearer {refresh_token()}"
            continue
        resp.raise_for_status()
        data = resp.json()
        convos = data.get("conversations", [])
        if not convos:
            break
        conversations.extend(convos)
        page += 1
        save_checkpoint({"conversations_page": page,
                         "completed_conversations": checkpoint["completed_conversations"]})
        dynamic_backoff(resp)
    return conversations

def refresh_token():
    """
    Freshchat API keys don't expire, but if using OAuth tokens
    via Freshworks Marketplace, refresh here.
    Returns the new token string.
    """
    # Implementation depends on your auth method
    raise NotImplementedError("Implement OAuth token refresh for your setup")

def extract_messages(conversation_id):
    messages = []
    page = 1
    while True:
        resp = requests.get(
            f"{FRESHCHAT_BASE}/conversations/{conversation_id}/messages",
            headers=HEADERS,
            params={"page": page, "items_per_page": 50}
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        msgs = data.get("messages", [])
        if not msgs:
            break
        messages.extend(msgs)
        page += 1
        dynamic_backoff(resp)
    # Sort chronologically — Freshchat may return newest-first
    messages.sort(key=lambda m: m["created_time"])
    return messages

def download_attachment(url, dest_dir="attachments"):
    """Download attachment immediately — Freshchat signed URLs may expire."""
    os.makedirs(dest_dir, exist_ok=True)
    resp = requests.get(url, stream=True)
    filename = url.split("/")[-1].split("?")[0]  # strip query params
    filepath = os.path.join(dest_dir, filename)
    with open(filepath, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)
    return filepath
```

### Step 2: Transform Data

The transformation layer does the heavy lifting:

1. **Sort messages** by `created_time` ascending within each conversation
2. **Classify each message** by `actor_type`: user messages become contact replies, agent messages become staff updates, system/bot messages become private notes or are excluded
3. **Convert `message_parts`** to HTML: text parts stay as-is, image parts become `<img>` tags with local file references, file parts become attachment references
4. **Map conversation properties** to HappyFox custom field IDs (pre-create fields and build a lookup table)
5. **Map group IDs** to HappyFox category IDs
6. **Map agent UUIDs** to HappyFox staff integer IDs (match by email)

```python
def flatten_message_parts(parts):
    """Convert Freshchat message_parts array to HTML string."""
    html_parts = []
    for part in parts:
        part_type = part.get("type", "text")
        content = part.get("content", {})

        if part_type == "text":
            text = content if isinstance(content, str) else content.get("text", "")
            html_parts.append(f"<p>{text}</p>")
        elif part_type == "image":
            url = content.get("url", "")
            html_parts.append(f'<p><img src="{url}" alt="attachment" /></p>')
        elif part_type == "file":
            url = content.get("url", "")
            name = content.get("name", "file")
            html_parts.append(f'<p><a href="{url}">{name}</a></p>')
        elif part_type == "quick_reply":
            text = content.get("text", "")
            options = content.get("options", [])
            opts_str = " ".join(f"[{o}]" for o in options)
            html_parts.append(f"<p>{text} {opts_str}</p>")
        elif part_type == "carousel":
            # Flatten carousel cards to text descriptions
            cards = content.get("cards", [])
            for card in cards:
                title = card.get("title", "")
                desc = card.get("description", "")
                html_parts.append(f"<p><strong>{title}</strong>: {desc}</p>")
        else:
            # Unknown part type — preserve as text
            html_parts.append(f"<p>[{part_type}: {json.dumps(content)}]</p>")

    return "\n".join(html_parts) if html_parts else "<p>(empty message)</p>"


# Status mapping: Freshchat status string → HappyFox status integer ID
# Populate HF status IDs from GET /api/1.1/json/statuses/
STATUS_MAP = {
    "new": 1,        # Map to your HappyFox "New" status ID
    "assigned": 2,   # Map to your HappyFox "In Progress" status ID
    "resolved": 3,   # Map to your HappyFox "Resolved" status ID
    "reopened": 2,   # Map reopened to "In Progress"
}

def transform_to_ticket(conversation, messages, field_map, category_map, staff_map):
    # First user message becomes ticket description
    first_msg = next((m for m in messages if m["actor_type"] == "user"), None)

    if first_msg:
        description = flatten_message_parts(first_msg["message_parts"])
    else:
        # Proactive/bot-initiated conversation — use first agent message
        first_msg = messages[0] if messages else None
        description = flatten_message_parts(first_msg["message_parts"]) if first_msg else "(no initial message)"

    conv_id = conversation["conversation_id"]
    user_email = conversation.get("user", {}).get("email")
    if not user_email:
        user_email = f"anonymous-{conv_id[:8]}@migration.placeholder"

    ticket_payload = {
        "subject": f"Chat conversation {conv_id[:8]}",
        "text": description,
        "category": category_map.get(conversation.get("group_id"), DEFAULT_CATEGORY_ID),
        "email": user_email,
        "name": f"{conversation.get('user', {}).get('first_name', '')} {conversation.get('user', {}).get('last_name', '')}".strip() or "Unknown",
        "priority": 2,  # default to medium
        "status": STATUS_MAP.get(conversation.get("status", "new"), 1),
        "t-cf-1": conv_id,  # Store Freshchat ID in custom field (use your actual field ID)
        "tags": ["migrated", "freshchat"]
    }

    # Map labels to tags
    labels = conversation.get("labels", [])
    if labels:
        ticket_payload["tags"].extend(labels)

    if conversation.get("assigned_agent_id"):
        staff_id = staff_map.get(conversation["assigned_agent_id"])
        if staff_id:
            ticket_payload["assignee"] = staff_id

    # Map custom conversation properties
    for prop_key, prop_val in conversation.get("conversation_properties", {}).items():
        hf_field_key = field_map.get(prop_key)
        if hf_field_key:
            ticket_payload[hf_field_key] = prop_val

    # Remaining messages become updates
    updates = []
    for msg in messages:
        if msg == first_msg:
            continue
        actor = msg.get("actor_type", "user")
        if actor == "agent":
            update_type = "staff_update"
        elif actor in ("system", "bot"):
            update_type = "private_note"  # or skip: continue
        else:
            update_type = "user_reply"

        updates.append({
            "type": update_type,
            "body": flatten_message_parts(msg["message_parts"]),
            "timestamp": msg["created_time"],
            "staff_id": staff_map.get(msg.get("actor_id")) if actor == "agent" else None,
            "attachments": [
                p for p in msg.get("message_parts", [])
                if p.get("type") in ("image", "file")
            ]
        })

    return ticket_payload, updates
```

### Step 3: Load into HappyFox

```python
import requests
from requests.auth import HTTPBasicAuth

HF_BASE = "https://yourcompany.happyfox.com/api/1.1/json"
HF_AUTH = HTTPBasicAuth("YOUR_API_KEY", "YOUR_AUTH_CODE")

# Track POST calls for rate limiting
post_count = 0
post_window_start = time.time()

def throttle_post():
    """Enforce <200 POST/min to stay safely under 300/min limit."""
    global post_count, post_window_start
    post_count += 1
    elapsed = time.time() - post_window_start
    if elapsed < 60 and post_count >= 200:
        sleep_time = 60 - elapsed + 1
        time.sleep(sleep_time)
        post_count = 0
        post_window_start = time.time()
    elif elapsed >= 60:
        post_count = 1
        post_window_start = time.time()

def create_tickets_bulk(ticket_payloads):
    """Create up to 100 tickets per request. Payload is a JSON array at root level."""
    batch_size = 100
    results = []
    for i in range(0, len(ticket_payloads), batch_size):
        batch = ticket_payloads[i:i + batch_size]
        throttle_post()
        resp = requests.post(
            f"{HF_BASE}/tickets/",
            auth=HF_AUTH,
            json=batch  # JSON array at root level, not wrapped in an object
        )
        if resp.status_code == 429:
            print("Rate limited — waiting 10 minutes")
            time.sleep(610)  # 10-minute lockout + buffer
            throttle_post()
            resp = requests.post(f"{HF_BASE}/tickets/", auth=HF_AUTH, json=batch)
        if resp.status_code == 400:
            # Log validation errors and continue with remaining batches
            print(f"Validation error on batch {i//batch_size}: {resp.json()}")
            # Optionally: retry individual tickets to find the bad one
            for ticket in batch:
                throttle_post()
                single_resp = requests.post(f"{HF_BASE}/tickets/", auth=HF_AUTH, json=ticket)
                if single_resp.status_code == 201:
                    results.append(single_resp.json())
                else:
                    print(f"Failed ticket: {ticket.get('subject')} — {single_resp.json()}")
            continue
        resp.raise_for_status()
        results.extend(resp.json())  # Response is an array of created ticket objects
    return results

def append_update(ticket_number, message_body, staff_id=None, is_staff=True):
    endpoint = "staff_update" if is_staff else "user_reply"
    payload = {"text": message_body}
    if is_staff and staff_id:
        payload["staff"] = staff_id
    throttle_post()
    resp = requests.post(
        f"{HF_BASE}/ticket/{ticket_number}/{endpoint}/",
        auth=HF_AUTH,
        json=payload
    )
    if resp.status_code == 429:
        time.sleep(610)
        throttle_post()
        resp = requests.post(
            f"{HF_BASE}/ticket/{ticket_number}/{endpoint}/",
            auth=HF_AUTH,
            json=payload
        )
    return resp

def upload_attachment(ticket_number, filepath, filename):
    """Upload attachment via multipart/form-data — JSON payloads do not support attachments."""
    throttle_post()
    with open(filepath, "rb") as f:
        resp = requests.post(
            f"{HF_BASE}/ticket/{ticket_number}/",
            auth=HF_AUTH,
            files={"attachments": (filename, f)},
            data={"text": f"Attachment: {filename}", "staff": 1}  # staff ID required
        )
    if resp.status_code == 429:
        time.sleep(610)
        with open(filepath, "rb") as f:
            resp = requests.post(
                f"{HF_BASE}/ticket/{ticket_number}/",
                auth=HF_AUTH,
                files={"attachments": (filename, f)},
                data={"text": f"Attachment: {filename}", "staff": 1}
            )
    return resp
```

### Step 4: Rebuild Relationships

After tickets are created:

- Store the Freshchat `conversation_id` → HappyFox `ticket_id` mapping in a crosswalk database (SQLite or PostgreSQL) for traceability and reruns.
- Verify contact-to-ticket associations by querying each ticket and checking the user field.
- If contacts were created during ticket creation (HappyFox auto-creates contacts by email), merge duplicates.
- Apply tags by updating tickets post-creation if they were not included in the initial payload.

### Step 5: Validate

Run validation queries against both systems:

- Total conversation count in Freshchat vs. total ticket count in HappyFox (minus documented exclusions)
- Message count per conversation vs. update count per ticket
- Contact count comparison (expect fewer in HappyFox due to email-based deduplication)
- Spot-check 5–10% of tickets for content accuracy, threading order, and attachment accessibility

## Edge Cases and Challenges

### Duplicate Contacts

Freshchat users may exist with the same email but different UUIDs (e.g., across channels). Freshchat also creates anonymous users based on browser sessions. HappyFox deduplicates contacts by email — if you create tickets referencing the same email, HappyFox associates them to the same contact automatically. This is usually the desired behavior, but verify that contact custom fields from different Freshchat user records do not overwrite each other. Deduplicate by email address before loading into HappyFox. For anonymous Freshchat users (no email), assign a deterministic placeholder email like `anonymous-{user_uuid_first8}@migration.placeholder` to ensure traceability without polluting your real contact data.

### Bot Conversations

Freshchat bot interactions (Freddy AI, custom bots) generate conversations with `actor_type: system` or `actor_type: bot` messages. Decide upfront: do these become tickets, or are they excluded? Most teams exclude bot-only conversations and only migrate conversations where a human agent was involved. Filter criterion: if every message in a conversation has `actor_type` in `("system", "bot")`, exclude it. If at least one message has `actor_type == "agent"`, include it.

### Conversations with No User Message

Some Freshchat conversations are initiated by proactive agent messages or bot triggers. These have no initial user message. HappyFox requires an `email` and `text` field on ticket creation. Use the agent's outbound message as the ticket description and assign a placeholder contact email.

### Attachments

Freshchat message attachments are referenced as URLs in the `message_parts` array. These URLs are signed and may expire within minutes to hours. You cannot simply pass the URL to HappyFox — download all files to your staging server during extraction, then re-upload to HappyFox via multipart/form-data. HappyFox's ticket creation with attachments only works via HTTP POST with `multipart/form-data` payloads, not JSON. Note that HappyFox attachment URLs returned in ticket GET responses also expire in approximately 5 minutes. ([support.happyfox.com](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))

### CSAT Responses

Freshchat collects CSAT at the conversation level. HappyFox has its own satisfaction survey system, but there is no API to import historical satisfaction ratings. Preserve CSAT data as a private note on the ticket (e.g., "Original CSAT: 4/5, Feedback: 'Quick and helpful'") or as a custom field value for reporting.

### Multi-Channel Conversations

A single Freshchat user may have conversations across web chat, WhatsApp, and Messenger. Each conversation migrates as a separate ticket in HappyFox. Tag them with the source channel (e.g., `channel:whatsapp`, `channel:web`) for reporting continuity.

### Timestamps

HappyFox's standard ticket creation API sets `created_at` to the time of the API call, not a custom timestamp. There is no documented public API parameter to backdate ticket creation timestamps. Store the original Freshchat timestamp in a custom field (`cf_original_date`), and work with HappyFox support to discuss backdating options if historical date accuracy matters for SLA reporting or compliance.

> [!WARNING]
> **Timestamp preservation is the most commonly overlooked issue in messaging-to-ticket migrations.** If historical date accuracy matters for SLA reporting or compliance, confirm HappyFox's backdating capabilities with their support team before committing to the migration. Some teams have reported that HappyFox support can perform server-side timestamp adjustments as a one-time accommodation — confirm this for your account.

## Limitations and Constraints

### HappyFox Constraints

- **No true custom objects.** HappyFox has tickets, contacts, contact groups, and assets. There is no equivalent to complex relational data beyond custom fields.
- **Contact pagination limited to 50 per page.** Extracting large contact lists from HappyFox (for validation) is slower than expected.
- **No bulk update endpoint for ticket updates.** Each staff update or contact reply is a separate POST call. For a conversation with 20 messages, that is 19 individual API calls after the initial ticket creation. This is the primary driver of load-phase duration.
- **10-minute API lockout on rate limit breach.** This is the most punishing rate limit behavior of any major help desk API.
- **Attachment URLs returned in ticket payloads expire in about 5 minutes.** ([support.happyfox.com](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))
- **No public API for KB article creation.** KB migration must be done via UI, CSV import (check with support), or browser automation.

### Freshchat Constraints

- **Rate limits are not explicitly published.** You must monitor response headers (`X-RateLimit-Remaining`) and implement dynamic throttling. Empirically, 2–3 requests/second is sustainable on Growth and Pro plans. ([developers.freshchat.com](https://developers.freshchat.com/api/))
- **Messages API max page size is 50.** For conversations with hundreds of messages, expect many paginated calls per conversation.
- **Chat-Transcript CSV exports are limited to 24-hour windows**, and raw report extraction cannot start more than **15 months** back. ([developers.freshchat.com](https://developers.freshchat.com/api/))
- **Signed attachment URLs may expire.** Download attachments immediately during extraction — do not defer to the load phase.
- **OAuth tokens can expire during long-running extractions.** Build in 401 handling and automatic token refresh. (Note: static API keys generated in the admin console do not expire, but OAuth tokens from Freshworks Marketplace integrations do.)

## Validation and Testing

### Record Count Comparison

| Metric | Freshchat Source | HappyFox Target | Expected Difference | Status |
|---|---|---|---|---|
| Total conversations | Count from API | Ticket count | Subtract excluded bot conversations | Must match |
| Total contacts/users | Count from API | Contact count | Fewer in HappyFox (email dedup) | Document delta |
| Messages per conversation | Sum per conversation | Updates per ticket | Subtract excluded bot messages | Must match |
| Attachments | Count from extraction | Count in HappyFox | Must match | Must match |

### Field-Level Validation

For a random sample of 5–10% of tickets:

- Verify subject/description matches first message content
- Verify agent assignment matches original conversation assignee
- Verify category matches original group
- Verify custom field values are correctly mapped
- Verify chronological order of updates matches message order
- Verify attachment count and file names match
- Verify `cf_freshchat_id` custom field contains the correct conversation UUID

### UAT Process

1. Run a test migration with 100–200 conversations (select a representative sample including multi-message, attachment-heavy, bot-involved, and multi-channel conversations)
2. Have 2–3 agents review migrated tickets for accuracy and readability
3. Verify that searching for a contact shows all associated tickets
4. Test that custom field filters work on migrated data
5. Confirm attachments are downloadable
6. Verify that tags and categories are correctly applied
7. Check that the `cf_original_date` custom field displays correctly in views and reports

### Rollback Planning

Before running the production migration:

- Document the current state of HappyFox (existing ticket count, contact count)
- HappyFox does not have a point-in-time restore. If you need to roll back, you will need to bulk-delete migrated tickets via API (`DELETE /api/1.1/json/ticket/{ticket_number}/`)
- Tag all migrated tickets with a `migration-batch-{date}` tag for easy identification and potential cleanup
- Use the ID crosswalk database to identify and delete all migrated records if validation fails

## Post-Migration Tasks

### Rebuild in HappyFox

These cannot be migrated programmatically:

- **Smart Rules** (HappyFox automations) — recreate trigger conditions and actions
- **SLA policies** — configure response and resolution time targets per category
- **Canned Actions** — rebuild saved response templates
- **Satisfaction surveys** — configure HappyFox's built-in survey settings
- **Agent roles and permissions** — set up staff roles and category access
- **Email parsing rules** — configure incoming email routing to categories

### User Training

- Freshchat agents are accustomed to a messaging interface. HappyFox uses a traditional ticket queue. Plan for 1–2 training sessions covering the workflow differences.
- Demonstrate how to search for migrated conversations (by original conversation ID stored in the `cf_freshchat_id` custom field).
- Train on HappyFox's category-based routing vs. Freshchat's group-based assignment.
- Help agents understand SLA timers, category routing, and contact group visibility.

### Monitor for 30 Days

- Watch for duplicate contacts created by incoming tickets from existing customers
- Verify that custom field values display correctly in reports
- Check that SLA clocks are calculating correctly on migrated tickets (timestamps stored in `cf_original_date` may differ from HappyFox's `created_at`)
- Set up HappyFox webhooks to monitor for data inconsistencies or failed email parsing
- Track agent feedback on migrated ticket readability

## Best Practices

1. **Back up everything first.** Extract all Freshchat data to a local staging store (JSON files or SQLite database) before writing anything to HappyFox.
2. **Run at least two test migrations.** The first will surface mapping errors. The second will validate your fixes.
3. **Throttle conservatively.** Target 60–70% of HappyFox's rate limit ceiling (200 POST/min). The 10-minute lockout is not worth the time saved by pushing to 280/min.
4. **Use idempotency keys.** Store the Freshchat `conversation_id` in a HappyFox custom field. If a migration run fails midway, query HappyFox for existing tickets with that custom field value before creating duplicates.
5. **Filter bot conversations early.** Do not extract, transform, and load data you will just delete later. Apply the bot filter at extraction time.
6. **Validate at every stage.** Check data at extraction (row counts), transformation (payload schema validation), and load (response status codes and record counts) — not just at the end.
7. **Migrate during off-hours.** Both APIs perform better under lighter load. Schedule production runs for nights or weekends in your primary time zone.
8. **Keep Freshchat active until validation is complete.** Do not cancel your Freshchat account until you have confirmed all data is accurately represented in HappyFox. Maintain at least one billing cycle of overlap.
9. **Use configuration files for mappings.** Do not hard-code field mappings if you have more than 10. Define them in a JSON or YAML config file that can be version-controlled and audited.
10. **Treat CRM data as a separate stream.** If your Freshchat data includes Freshsales Suite objects, migrate them separately into HappyFox custom fields or keep them in the CRM.
11. **Download attachments during extraction.** Do not defer attachment downloads to the load phase — Freshchat signed URLs expire and will be inaccessible later.
12. **Persist checkpoints.** Save extraction and load progress (last page, last conversation ID, last created ticket ID) so that restarts resume from where they left off rather than restarting from scratch.

## Sample Data Mapping Table

| # | Freshchat Source | API Path | HappyFox Target | Transform |
|---|---|---|---|---|
| 1 | Conversation ID | `/v2/conversations` → `conversation_id` | Custom field `t-cf-{id}` | String copy |
| 2 | Status | `status` | Status ID | Enum → integer ID lookup via `GET /statuses/` |
| 3 | Assigned agent | `assigned_agent_id` | Assignee | UUID → staff ID (email match) |
| 4 | User email | `users [].email` | Contact email | Direct |
| 5 | User name | `users [].first_name` + `last_name` | Contact name | Concatenate |
| 6 | Group | `group_id` | Category | UUID → category ID lookup |
| 7 | First message | `messages [0].message_parts` | Ticket description | Flatten parts to HTML |
| 8 | Subsequent messages | `messages [1..n].message_parts` | Updates | Per-message POST (staff_update / user_reply) |
| 9 | Label | `labels []` | Tag | Name match |
| 10 | Attachment URL | `message_parts [].url` | Attachment file | Download + multipart upload |
| 11 | CSAT score | CSAT response | Private note or custom field | Formatted text |
| 12 | Created time | `created_time` | Custom field `t-cf-{id}` | ISO 8601 string |
| 13 | Channel source | Conversation metadata | Tag (`channel:web`, `channel:whatsapp`) | String prefix + channel name |

## Automation Script Outline

High-level structure for a Python-based migration script:

```python
# migration.py — Freshchat to HappyFox migration

import json
import os

class FreshchatExtractor:
    """Handles paginated extraction from Freshchat v2 API."""
    def __init__(self, base_url, api_key): ...
    def get_agents(self) -> list: ...           # GET /v2/agents
    def get_users(self) -> list: ...            # GET /v2/users (paginated, 100/page)
    def get_conversations(self) -> list: ...    # GET /v2/conversations (paginated, 50/page)
    def get_messages(self, conv_id) -> list: ... # GET /v2/conversations/{id}/messages (50/page)
    def download_attachment(self, url, dest_dir) -> str: ...  # Returns local filepath

class Transformer:
    """Maps Freshchat data to HappyFox payloads."""
    def __init__(self, field_map, category_map, staff_map, status_map): ...
    def conversation_to_ticket(self, conv, msgs) -> tuple: ...  # (ticket_payload, updates)
    def flatten_message_parts(self, parts) -> str: ...          # message_parts → HTML
    def map_status(self, fc_status) -> int: ...                 # status → HF status ID
    def user_to_contact(self, user) -> dict: ...

class HappyFoxLoader:
    """Handles writes to HappyFox v1.1 API with rate limiting."""
    def __init__(self, base_url, api_key, auth_code): ...
    def create_contacts_bulk(self, contacts) -> list: ...
    def create_tickets_bulk(self, tickets, batch_size=100) -> list: ...
    def append_staff_update(self, ticket_num, staff_id, body) -> dict: ...
    def append_user_reply(self, ticket_num, body) -> dict: ...
    def upload_attachment(self, ticket_num, filepath, filename) -> dict: ...
    def _throttle(self): ...  # Enforce <200 POST/min with counter and timer

class Validator:
    """Post-migration checks."""
    def compare_counts(self, fc_data, hf_data) -> dict: ...
    def sample_check(self, sample_size_pct=10) -> list: ...
    def verify_threading(self, ticket_num, expected_messages) -> bool: ...
    def verify_attachments(self, ticket_num, expected_count) -> bool: ...

class CrosswalkDB:
    """SQLite-backed ID mapping for idempotency and rollback."""
    def __init__(self, db_path="crosswalk.db"): ...
    def record_mapping(self, fc_conv_id, hf_ticket_id): ...
    def get_hf_ticket_id(self, fc_conv_id) -> int | None: ...
    def get_all_hf_ticket_ids(self) -> list: ...

if __name__ == "__main__":
    # Load configuration
    with open("migration_config.json") as f:
        config = json.load(f)

    # 1. Extract
    extractor = FreshchatExtractor(config["freshchat_base"], config["freshchat_key"])
    agents = extractor.get_agents()
    users = extractor.get_users()
    conversations = extractor.get_conversations()

    # 2. Build lookup maps from HappyFox
    # Requires pre-created staff, categories, custom fields in HappyFox
    staff_map = build_staff_map(agents, hf_staff)       # FC agent UUID → HF staff int ID
    category_map = build_category_map(fc_groups, hf_categories)  # FC group UUID → HF cat int ID
    field_map = build_field_map(fc_properties, hf_custom_fields) # FC prop name → HF "t-cf-{id}"
    status_map = build_status_map()  # FC status string → HF status int ID

    # 3. Transform and load contacts
    transformer = Transformer(field_map, category_map, staff_map, status_map)
    loader = HappyFoxLoader(config["hf_base"], config["hf_key"], config["hf_auth"])
    crosswalk = CrosswalkDB()

    contacts = [transformer.user_to_contact(u) for u in users]
    loader.create_contacts_bulk(contacts)

    # 4. Transform and load tickets + updates
    for conv in conversations:
        # Check idempotency — skip if already migrated
        existing = crosswalk.get_hf_ticket_id(conv["conversation_id"])
        if existing:
            continue

        msgs = extractor.get_messages(conv["conversation_id"])
        ticket, updates = transformer.conversation_to_ticket(conv, msgs)
        result = loader.create_tickets_bulk([ticket])
        ticket_num = result[0]["id"]

        crosswalk.record_mapping(conv["conversation_id"], ticket_num)

        for update in updates:
            if update["type"] == "staff_update":
                loader.append_staff_update(ticket_num, update.get("staff_id"), update["body"])
            elif update["type"] == "user_reply":
                loader.append_user_reply(ticket_num, update["body"])
            # Handle attachments
            for att in update.get("attachments", []):
                filepath = extractor.download_attachment(att["content"]["url"])
                loader.upload_attachment(ticket_num, filepath, att["content"].get("name", "file"))

    # 5. Validate
    validator = Validator(extractor, loader)
    report = validator.compare_counts(fc_data=conversations, hf_data=loader)
    sample_results = validator.sample_check(sample_size_pct=10)
    print(json.dumps(report, indent=2))
    print(f"Sample check: {sum(1 for r in sample_results if r['pass'])}/{len(sample_results)} passed")
```

**Configuration file example (`migration_config.json`):**

```json
{
  "freshchat_base": "https://yourcompany.freshchat.com/v2",
  "freshchat_key": "YOUR_FRESHCHAT_API_KEY",
  "hf_base": "https://yourcompany.happyfox.com/api/1.1/json",
  "hf_key": "YOUR_HF_API_KEY",
  "hf_auth": "YOUR_HF_AUTH_CODE",
  "default_category_id": 3,
  "default_priority": 2,
  "exclude_bot_only": true,
  "status_map": {
    "new": 1,
    "assigned": 2,
    "resolved": 3,
    "reopened": 2
  }
}
```

Persist checkpoints for users, conversations, last message page, and HappyFox IDs so restarts are safe and reruns are idempotent.

## What to Do Next

If your team has the engineering bandwidth and a dataset under 10,000 conversations, the API-based approach outlined here is achievable in 2–4 weeks of part-time engineering effort. For larger datasets, complex custom fields, or zero-downtime requirements, the engineering cost of building, testing, and debugging a custom migration script often exceeds the cost of working with a specialist.

Use the throughput calculation above to estimate your specific timeline: count your conversations, multiply by average messages per conversation, add attachment counts, divide by your target POST rate, and add a 50% buffer. That gives you the load-phase runtime. Double it for the full migration including extraction, transformation, and validation.

For further reading on moving data into and out of HappyFox, review our guides on [How to Migrate from Happy

## Frequently asked questions

### Can I migrate Freshchat conversations to HappyFox using CSV export?

Not effectively. Freshchat's CSV export flattens entire chat transcripts into a single text field, losing message-level timestamps, sender attribution, and threading. The Chat-Transcript report is limited to 24-hour windows with a 15-month lookback cap. HappyFox has no native CSV import for tickets. For anything beyond small archival needs, use the API-based approach.

### What are HappyFox's API rate limits for migration?

HappyFox allows 500 GET requests per minute and 300 POST requests per minute. Exceeding these triggers a 429 error with a 10-minute lockout — all API access is blocked for 10 minutes. Target 200 POST/min maximum during migration to avoid hitting this limit.

### How long does a Freshchat to HappyFox migration take?

For mid-size datasets (10,000–50,000 conversations), plan for 5–15 business days. The primary bottleneck is the per-message API calls required to recreate conversation threading in HappyFox, combined with HappyFox's rate limits. Small datasets under 5,000 conversations can complete in 3–5 days.

### Can I preserve original timestamps when migrating to HappyFox?

HappyFox's standard ticket creation API sets the created_at field to the time of the API call. There is no documented public API parameter to backdate ticket creation. Store the original Freshchat timestamp in a custom field, and contact HappyFox support to discuss backdating options.

### What Freshchat data cannot be migrated to HappyFox?

Bot conversation flows (interactive elements like carousels and quick replies), CSAT survey results (no import API), smart assignments, automation rules, and real-time analytics data cannot be migrated programmatically. These must be rebuilt manually or preserved as notes. CRM-side objects like Leads and Opportunities from Freshsales Suite are also not native to either platform's chat/ticket model.
