---
title: "HappyFox to Missive Migration: The Technical Guide"
slug: happyfox-to-missive-migration-the-technical-guide
date: 2026-07-21
author: Nachi
categories: [Migration Guide, HappyFox, Missive]
excerpt: "Technical guide to migrating from HappyFox to Missive. Covers data model mapping, API constraints, history preservation, and step-by-step implementation."
tldr: HappyFox to Missive requires an API pipeline — CSV exports lose history. Rate limits and 5-minute attachment URL expiry are the top technical constraints.
canonical: https://clonepartner.com/blog/happyfox-to-missive-migration-the-technical-guide/
---

# HappyFox to Missive Migration: The Technical Guide


## What Is a HappyFox to Missive Migration?

A **HappyFox to Missive migration** is the process of moving tickets, contacts, contact groups, custom fields, tags, knowledge base articles, and attachments from HappyFox Help Desk into Missive while preserving conversation history, timestamps, and relational integrity.

This migration is architecturally complex because the two platforms have fundamentally different data models. HappyFox is a traditional ticket-based helpdesk where each support interaction lives inside a numbered ticket with statuses, priorities, categories, and assignment queues. Missive is a collaborative email client built around conversations — shared inboxes where email, SMS, WhatsApp, and custom channel messages are managed, assigned, and discussed using internal comments, labels, and team routing.

Teams typically move from HappyFox to Missive for three reasons:

1. **Simpler collaboration model.** Missive's internal-comment-on-thread approach eliminates the overhead of a full ticketing system for teams where email *is* the primary workflow.
2. **Multi-channel unification.** Missive natively handles email, SMS (Twilio), WhatsApp, and custom channels in one inbox. HappyFox requires separate products or add-ons for some of these channels.
3. **Cost at scale.** For teams under 50 users, Missive's Productive plan ($18–24/user/month) typically costs less per seat than equivalent HappyFox tiers while covering the core collaboration use case.

A HappyFox-to-Missive migration works when your team is moving from formal ticket management to a lighter shared-inbox model. If you still need deep helpdesk behavior — asset-linked tickets, heavy SLA workflows, strict historical ticket reporting — Missive is not a 1:1 replacement. If your real operating model is shared inboxes, collaborative drafting, and quick internal comments around live conversations, Missive is a strong target.

## Core Data Model Differences: HappyFox vs. Missive

Before writing any migration code, you need to understand what you are mapping between. These are not equivalent systems — they are different categories of software.

| Concept | HappyFox | Missive |
|---|---|---|
| Primary unit | Ticket (numbered, stateful) | Conversation (email thread, stateless beyond open/closed) |
| Message history | Updates array on ticket object | Messages within a conversation |
| Categorization | Categories (hierarchical) | Shared Labels + Teams |
| Status tracking | Open, Pending, On Hold, Resolved, Closed | Inbox, Closed, Snoozed, Trashed |
| Priority | Urgent, High, Medium, Low | No native priority field |
| Assignment | Agent / Agent Group | User assignees per conversation |
| Custom fields | Ticket-level and contact-level | Contact-level only |
| Contacts | Contacts + Contact Groups | Contacts in Contact Books + Companies + Groups |
| Automation | Smart Rules, SLAs, Canned Actions | Rules (up to 1,000 on Productive/Business) |
| Knowledge Base | Built-in KB with categories | No built-in knowledge base |
| API auth | HTTP Basic Auth (API key + Auth code) | Bearer token (personal, no org-level token) |
| Conversation size | No documented limit | 400 messages per conversation ([missiveapp.com](https://missiveapp.com/docs/core-features/conversations/merging?utm_source=openai)) |
| Email threading | Ticket ID–based; original `Message-ID` and `In-Reply-To` headers stored but not exposed via API | Thread detection via `Message-ID`, `In-Reply-To`, and `References` headers |

> [!WARNING]
> **The biggest structural gap:** HappyFox tickets carry rich metadata — priority, SLA timers, custom fields, due dates, satisfaction ratings. Missive conversations are intentionally lightweight. Much of this metadata will be lost or must be flattened into labels, contact notes, or posts. Define what you are willing to lose *before* you start.

## Migration Approaches

### 1. Native CSV Export + Manual Import

**How it works:** Export tickets from HappyFox's Reports module as CSV/XLSX. Manually process the files and attempt to recreate data in Missive.

**When to use it:** Contact lists only. Never for tickets if you need conversation history.

**Why it fails for tickets:** HappyFox's native CSV export only includes the initial message and subject line — not staff replies, client replies, or private notes. The tabular export does not include full conversation threads. Export links expire in 3 days. ([support.happyfox.com](https://support.happyfox.com/kb/article/995-how-do-i-export-my-tickets-from-happyfox/))

**What it can handle:** Contact exports (name, email, phone, company) translate well to Missive contact books via CSV import or the Contacts API. Missive's documented self-serve CSV import is for contacts, not conversations. ([missiveapp.com](https://missiveapp.com/docs/core-features/contacts/importing))

- **Complexity:** Low
- **Scalability:** Small datasets only
- **Risk:** High data loss for ticket history

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

**How it works:** Extract full ticket data from the HappyFox REST API (v1.1 — the current production version as of June 2025), transform payloads to match Missive's data structure, and load via the Missive REST API using the Posts or Messages endpoint.

**Step-by-step:**
1. Authenticate against HappyFox using HTTP Basic Auth with your API key and Auth code
2. Paginate through `/api/1.1/json/tickets/?size=50&page=N` to extract all tickets with full `updates` arrays
3. For each ticket, download attachments immediately — URLs expire in 5 minutes
4. Transform tickets into Missive conversations using the Posts endpoint or Custom Channel Messages endpoint
5. Create/sync contacts via Missive's Contact endpoints
6. Apply shared labels to conversations to replicate HappyFox categories
7. Set conversation state (closed, snoozed) using post actions

**When to use it:** Any migration where conversation history matters. This is the only approach that preserves full thread fidelity.

**Pros:**
- Full conversation history preserved
- Programmatic control over mapping logic
- Can handle attachments, custom fields, and relationships
- Idempotent if you track external IDs

**Cons:**
- Requires engineering effort (see throughput calculation below for sizing)
- Missive API rate limits constrain throughput — no bulk import endpoint
- HappyFox attachment URLs expire in 5 minutes — your pipeline must download and re-upload in near-real-time
- Missive's `POST /v1/messages` endpoint is only for custom channels, and custom channels do not support Missive Rules ([missiveapp.com](https://missiveapp.com/docs/developers/custom-channels))
- A Missive conversation has a 400-message hard limit ([missiveapp.com](https://missiveapp.com/docs/core-features/conversations/merging?utm_source=openai))

- **Complexity:** High
- **Scalability:** Enterprise-grade with proper batching
- **Risk:** Medium (rate limiting, attachment expiry, model mismatch)

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

**How it works:** Use Zapier or Make (formerly Integromat) to connect HappyFox triggers to Missive actions. Missive has a native Zapier integration and a verified Make integration.

**When to use it:** Ongoing sync of *new* tickets/contacts only. Not for historical migration.

**Pros:**
- No-code setup
- Good for continuous sync post-migration
- Make's Missive module supports creating contacts, posts, and drafts

**Cons:**
- Cannot backfill historical data at scale
- Zapier/Make task limits make large migrations expensive
- Limited transformation logic — no control over conversation threading or timestamp preservation

- **Complexity:** Low
- **Scalability:** Small volumes only
- **Risk:** High for historical data; low for ongoing sync

### 4. Custom ETL Pipeline

**How it works:** Build a dedicated Extract-Transform-Load pipeline using Python, Node.js, or a data integration framework (Airbyte, dlt). Extract from the HappyFox API, transform in your application layer, and load into the Missive API. Optionally stage data in a warehouse (Postgres, Snowflake) for auditing before loading.

**When to use it:** Large datasets (50,000+ tickets), complex custom field mappings, regulated data, or when you need full auditability.

**Pros:**
- Maximum control over data transformation
- Can implement retry logic, checkpointing, and idempotency
- Supports incremental migration with an audit trail built in

**Cons:**
- Highest engineering investment
- Requires deep familiarity with both APIs
- Must handle Missive's rate limits (900 requests/15 minutes sustained)

- **Complexity:** High
- **Scalability:** Best for enterprise
- **Risk:** Low with proper error handling

### 5. Managed Migration Service

**How it works:** Engage a specialist team to handle extraction, transformation, loading, validation, and cutover planning.

**When to use it:** When you cannot afford data loss, need zero downtime, or lack in-house API engineering bandwidth.

- **Complexity:** Low (for you)
- **Scalability:** Any size
- **Risk:** Lowest

### Approach Comparison

| Approach | Complexity | History Preserved | Scalability | Best For |
|---|---|---|---|---|
| CSV Export | Low | ❌ No | Small | Contact lists only |
| API-Based | High | ✅ Yes | Enterprise | Teams with dev resources |
| Middleware (Zapier/Make) | Low | ❌ No (new only) | Small | Ongoing sync |
| Custom ETL | High | ✅ Yes | Enterprise | Large, complex datasets |
| Managed Service | Low (for you) | ✅ Yes | Any | Zero-downtime requirements |

**Recommendations by scenario:**
- **Small team (<5,000 tickets), simple fields:** API-based script
- **Mid-size (5,000–50,000 tickets):** Custom ETL or managed service
- **Enterprise (50,000+ tickets):** Managed service strongly recommended
- **Ongoing sync post-migration:** Zapier/Make for new conversations

## Throughput Calculation and Timeline Estimation

Rather than quoting rough estimates, here is the math so you can size your own migration:

**Extraction phase (HappyFox API):**
- HappyFox allows 500 GET requests/minute and returns max 50 tickets per page ([support.happyfox.com](https://support.happyfox.com/kb/article/1148-rate-limiting/?section_id=131))
- At 1 request/second (conservative): 50 tickets/second × 1 page/second = 50 tickets/second, but attachment downloads dominate. Budget ~2 seconds per ticket with attachments.
- 30,000 tickets ≈ 16–17 hours of extraction with attachment downloads

**Loading phase (Missive API):**
- Missive allows 300 requests/minute and 900 requests/15 minutes. The binding constraint is the 15-minute window: 900 requests/15 min = 1 request/second sustained.
- Each ticket requires: 1 API call for the initial message + N calls for subsequent messages + 1 call to apply labels + 1 call to close/snooze = (N + 3) calls per ticket.
- For 30,000 tickets averaging 5 messages each: 30,000 × (5 + 3) = 240,000 API calls.
- At 1 request/second sustained: **240,000 seconds ≈ 67 hours of continuous loading.**

**Total runtime for 30,000 tickets:** ~83 hours (3.5 days) of continuous, throttled API calls — excluding transformation, validation, error retries, and attachment re-uploads.

**Scaling reference points:**
| Ticket Count | Avg Messages/Ticket | Estimated API Calls | Load Time (sustained 1 req/s) |
|---|---|---|---|
| 5,000 | 4 | 35,000 | ~10 hours |
| 10,000 | 5 | 80,000 | ~22 hours |
| 30,000 | 5 | 240,000 | ~67 hours |
| 50,000 | 6 | 450,000 | ~125 hours (5.2 days) |

Add 30–50% overhead for retries, validation passes, and contact/label creation. Engineering effort (building, testing, debugging the pipeline) is separate from runtime: expect 80–200 hours of development depending on custom field complexity and edge case density.

## Pre-Migration Planning

### Data Audit Checklist

Before writing any migration code, audit your HappyFox instance:

- [ ] **Tickets:** Total count, date range, status distribution (open vs. closed)
- [ ] **Contacts:** Total count, duplicates, contacts with no associated tickets
- [ ] **Contact Groups:** Group structure and membership
- [ ] **Categories:** Full hierarchy — these become your Missive labels/teams
- [ ] **Custom Fields:** List all ticket-level and contact-level custom fields, data types, and picklist values
- [ ] **Tags:** Full tag list and usage frequency
- [ ] **Knowledge Base:** Article count, category structure, embedded media
- [ ] **Canned Actions:** Templates that need manual recreation in Missive
- [ ] **Smart Rules:** Automation logic that needs rebuilding as Missive Rules
- [ ] **SLAs:** Response/resolution time targets to recreate
- [ ] **Attachments:** Total volume (GB) — directly affects migration duration and staging storage requirements
- [ ] **Agents/Staff:** User list, roles, and team assignments
- [ ] **Data residency:** Confirm HappyFox and Missive hosting regions for GDPR/compliance (see compliance section below)

### Identify What to Leave Behind

Not everything in HappyFox has a Missive equivalent. Consciously decide what to drop:

- **Satisfaction survey results** — Missive has no native CSAT system
- **SLA timer data** — No equivalent in Missive's data model
- **Ticket priority levels** — Missive has no priority field (use labels as a workaround)
- **Asset management data** — Missive has no asset tracking
- **Report configurations** — Must be rebuilt using Missive's analytics
- **Old closed tickets** — Many teams choose to archive tickets older than 24 months to a data warehouse and only migrate recent history to Missive. The 400-message conversation limit and lack of a dedicated ticket-history import flow make selective migration the pragmatic choice.

### Data Compliance and Residency

Moving customer support data between platforms has compliance implications:

- **GDPR:** If your HappyFox instance serves EU data subjects, verify that Missive's hosting region and data processing agreements meet your obligations. Missive is a Canadian company; confirm data residency with their team before migration.
- **PII in transit:** Your ETL pipeline stages customer data (names, emails, conversation content, attachments) locally or in a warehouse. Encrypt data at rest in your staging layer (AES-256 minimum) and in transit (TLS 1.2+).
- **Data retention:** After migration, you have a copy of all support data in your staging environment. Define a retention and deletion policy for this intermediate data before starting.
- **Right to erasure:** If a customer requests deletion under GDPR during the migration window, you must be able to locate and delete their data in HappyFox, your staging layer, and Missive.

### Migration Strategy

| Strategy | When to Use | Risk Level |
|---|---|---|
| **Big bang** | Small dataset, can tolerate brief downtime | Medium |
| **Phased** | Multiple categories/teams, want to validate incrementally | Low |
| **Incremental** | Large dataset, zero-downtime requirement | Lowest |

For most teams, **phased migration by category** works best. Migrate one HappyFox category at a time, validate in Missive, then proceed to the next. If you can connect the real shared mailboxes to Missive for live email during the transition, incremental is even safer — but HappyFox-only private notes and ticket metadata still need a separate migration path.

## Data Model & Object Mapping

This is the most important section of any migration plan. Get the mapping wrong, and you re-run the entire migration.

### Object-Level Mapping

| HappyFox Object | Missive Equivalent | Notes |
|---|---|---|
| Ticket | Conversation | Core unit. Each ticket becomes one conversation. |
| Ticket Updates (replies, notes) | Messages / Posts within a conversation | Use Messages endpoint for custom channels or Posts for injected content. |
| Contact | Contact (in a Contact Book) | Direct mapping. Create shared contact books first. |
| Contact Group | Contact Group or Company | HappyFox contact groups can affect ticket visibility; Missive companies provide organization context, groups act as tags, and routing belongs in team inboxes. ([support.happyfox.com](https://support.happyfox.com/kb/article/699-manage-contact-groups/?section_id=128&utm_source=openai)) |
| Category | Shared Label or Team | Structural decision — see callout below. |
| Status | Conversation state | Open → Inbox, Closed → Closed, Pending → Snoozed |
| Priority | Shared Label | No native priority. Use labels like "Priority: High". |
| Agent | User (assignee) | Map HappyFox agent emails to Missive user IDs. |
| Agent Group | Team | HappyFox agent groups map to Missive teams. |
| Tag | Shared Label | Tags become organization labels. |
| Custom Field (ticket) | Post content or Contact field | Ticket-level custom fields have no native home — flatten into post body or contact metadata. |
| Custom Field (contact) | Contact field | Direct mapping where types align. |
| Knowledge Base Article | *No equivalent* | Missive has no built-in KB. Export to a separate tool. |
| Canned Action | Canned Response | Manual recreation required. |
| Smart Rule | Rule | Manual recreation required. Rules require Productive plan or higher. |
| SLA | *No equivalent* | Use Rules with time-based conditions as a workaround. |

> [!NOTE]
> **Category → Label vs. Team decision:** If your HappyFox categories represent routing queues (Sales, Support, Billing), map them to Missive **Teams**. If they represent topic tags that can overlap, map them to **Shared Labels**. Most teams use a combination.

### Handling Ticket-Level Custom Fields

HappyFox supports extensive custom fields on tickets — dropdowns, text fields, date fields, checkboxes. Missive does not support ticket-level custom fields; only contact-level. Your options:

- **Map to Labels:** Convert custom field values into Missive labels (e.g., a "Plan Type: Enterprise" field becomes a "Plan: Enterprise" label).
- **Flatten into the first Post body** as structured text for historical reference.
- **Store in contact-level fields** if the data belongs to the customer rather than the ticket.
- **Accept the loss** if the field data is no longer operationally relevant.

Complex reporting based on custom fields will require a BI tool connected to Missive's API.

### Field-Level Mapping

| HappyFox Field | Type | Missive Field | Transformation |
|---|---|---|---|
| `id` (ticket) | Integer | External reference in Post body | Store as metadata for traceability |
| `display_id` | String | Label or subject prefix | Preserves original ticket ID for search |
| `subject` | String | Conversation subject | Direct |
| `text` (initial message) | HTML | First message body | Direct |
| `updates [].message` | HTML | Subsequent messages | Ordered by timestamp |
| `updates [].timestamp` | UTC datetime | `delivered_at` (Unix timestamp) | Convert to epoch seconds |
| `updates [].by` | Object | `from_field` | Map to contact or agent |
| `status.name` | String | Conversation state | Map: Open → inbox, Closed → close action |
| `priority.name` | String | Shared label | Create "Priority: X" labels |
| `category.name` | String | Shared label or Team | Structural decision |
| `tags []` | Array of strings | Shared labels | One label per tag |
| `assigned_to.email` | String | `add_assignees` | Map to Missive user ID |
| `contact.name` | String | `first_name` + `last_name` | Split on space |
| `contact.email` | String | Contact email | Direct |
| `contact.phone` | String | Contact phone | Direct |
| `attachments [].url` | URL (expires 5 min) | Re-uploaded attachment | Download immediately, re-attach |
| `custom_fields []` | Various | Contact field / Label / Post body | Flatten only high-value fields |

### Email Threading and Message-ID Preservation

When HappyFox tickets originated from email, the original email headers (`Message-ID`, `In-Reply-To`, `References`) may be relevant for thread integrity. HappyFox's API does not expose these headers on ticket updates. This means:

- You cannot reconstruct the original email thread chain in Missive using native header-based threading.
- If you import via the custom channel Messages endpoint, Missive creates its own conversation threading based on the `conversation` ID you supply — not email headers.
- If you later connect the same shared mailbox to Missive and a customer replies to an old email, Missive will create a **new** conversation because the original `Message-ID` chain is absent from the imported history. Document this behavior for your team.

**Workaround:** If you have access to the original mailbox (e.g., `support@yourcompany.com`) via IMAP, you can connect it directly to Missive and let Missive import the raw email history with headers intact. Then use the API migration only for HappyFox-specific metadata (labels, assignments, private notes). This hybrid approach preserves email threading but adds complexity.

## Migration Architecture

### Data Flow

```
HappyFox API (v1.1)          Transform Layer           Missive API (v1)
─────────────────          ───────────────           ─────────────────
 GET /tickets/?size=50  →   Map fields      →   POST /contacts
 GET /user/<id>         →   Convert timestamps →  POST /messages (custom channel)
 GET /contact_group/    →   Download attachments → POST /posts
                        →   Build conversation →  (apply labels, assign users)
                            payloads
                        →   Stage in Postgres  →  (optional audit layer)
```

### HappyFox API Constraints

- **Authentication:** HTTP Basic Auth with API key and Auth code
- **Pagination:** Max 50 records per page, page-based
- **Attachment URLs:** Pre-signed S3 links that expire in 5 minutes — download immediately on extraction ([support.happyfox.com](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))
- **Rate limits:** 500 GET and 300 POST requests per minute ([support.happyfox.com](https://support.happyfox.com/kb/article/1148-rate-limiting/?section_id=131))
- **Timestamp format:** All API responses use UTC
- **Error responses:** Returns HTTP 429 with no `Retry-After` header when rate-limited; returns HTTP 401 for invalid credentials; returns HTTP 400 with a JSON `error` field for malformed requests

### Missive API Constraints

- **Authentication:** Bearer token (personal). Requires the Productive plan ($18–24/user/month) or higher. All tokens are personal — no organization-level service account exists.
- **Rate limits:** 5 concurrent requests, 300 requests/minute, 900 requests/15 minutes
- **Pagination:** Cursor-based using `until` parameter (Unix timestamp), max 50 conversations per page
- **No bulk import:** Each message/post/contact is a separate API call
- **Custom channels:** The Messages endpoint for custom channels is the best way to inject historical conversations with preserved timestamps, but custom channels do not support Missive Rules ([missiveapp.com](https://missiveapp.com/docs/developers/custom-channels))
- **Conversation limit:** A single conversation can contain at most 400 messages ([missiveapp.com](https://missiveapp.com/docs/core-features/conversations/merging?utm_source=openai))
- **Webhooks:** Missive supports webhooks for conversation and message events, which can be used for post-migration validation (confirming messages were created) or ongoing sync monitoring ([missiveapp.com](https://missiveapp.com/docs/developers/webhooks))
- **Search indexing delay:** After bulk import, conversations may not appear in Missive's search results immediately. Budget 15–30 minutes of indexing delay for large batches before running search-based validation.
- **Error responses:** Returns HTTP 429 with a `Retry-After` header (seconds); returns HTTP 422 with a JSON body describing validation errors (e.g., missing required fields); returns HTTP 401 for invalid or expired tokens.

### Missive API Response Schema (Key Endpoints)

Understanding what Missive returns is critical for extracting conversation IDs and chaining requests:

**`POST /v1/messages` response (custom channel):**
```json
{
  "messages": {
    "id": "msg_abc123",
    "conversation": {
      "id": "conv_xyz789",
      "subject": "Re: Order issue",
      "team": { "id": "team_456" }
    },
    "delivered_at": 1719849600
  }
}
```

**`POST /v1/posts` response:**
```json
{
  "posts": {
    "id": "post_def456",
    "conversation": "conv_xyz789",
    "markdown": "Internal note content"
  }
}
```

Extract `messages.conversation.id` from the first message response and pass it as `conversation` in subsequent calls to thread messages into the same conversation.

> [!WARNING]
> **Rate limit math:** At a sustained 1 request/second (the safe rate for Missive), migrating 30,000 tickets with an average of 5 messages each requires ~240,000 API calls. That is roughly 67 hours of continuous, throttled API calls — just for the message import. See the throughput calculation section above for full sizing methodology.

## Step-by-Step Migration Process

### Step 1: Extract Data from HappyFox

```python
import requests
import time
import json
import os
from datetime import datetime, timezone

HF_BASE = "https://yourcompany.happyfox.com"
HF_API_KEY = os.environ["HF_API_KEY"]
HF_AUTH_CODE = os.environ["HF_AUTH_CODE"]

def parse_timestamp(ts_string):
    """Convert HappyFox UTC timestamp string to Unix epoch seconds.
    
    HappyFox returns timestamps in ISO 8601 format: '2024-03-15T14:30:00Z'
    Missive's delivered_at expects Unix epoch seconds as an integer.
    """
    dt = datetime.fromisoformat(ts_string.replace("Z", "+00:00"))
    return int(dt.timestamp())

def extract_tickets(page=1, size=50):
    """Extract tickets with full update history."""
    url = f"{HF_BASE}/api/1.1/json/tickets/"
    params = {"size": size, "page": page, "status": "_all"}
    resp = requests.get(url, auth=(HF_API_KEY, HF_AUTH_CODE), params=params)
    if resp.status_code == 429:
        time.sleep(60)  # HappyFox does not return Retry-After
        return extract_tickets(page, size)
    resp.raise_for_status()
    return resp.json()

def download_attachment(att_url, save_dir="./attachments"):
    """Download attachment before 5-minute URL expiry."""
    os.makedirs(save_dir, exist_ok=True)
    resp = requests.get(att_url, stream=True)
    resp.raise_for_status()
    filename = att_url.split("/")[-1].split("?")[0]
    filepath = os.path.join(save_dir, filename)
    with open(filepath, "wb") as f:
        for chunk in resp.iter_content(8192):
            f.write(chunk)
    return filepath

def extract_all_tickets():
    page = 1
    all_tickets = []
    while True:
        data = extract_tickets(page=page)
        tickets = data.get("data", [])
        if not tickets:
            break
        for ticket in tickets:
            # Download attachments immediately (URLs expire in 5 min)
            for update in ticket.get("updates", []):
                for att in update.get("attachments", []):
                    att["local_path"] = download_attachment(att["url"])
            all_tickets.append(ticket)
        page += 1
        time.sleep(1)  # Respect rate limits
    return all_tickets
```

### Step 2: Transform Data

```python
def transform_ticket_to_missive(ticket, label_map, user_map):
    """Transform a HappyFox ticket into Missive message payloads.
    
    Args:
        ticket: Full HappyFox ticket object from API
        label_map: Dict mapping HappyFox label names to Missive label IDs
        user_map: Dict mapping HappyFox agent emails to Missive user IDs
    
    Returns:
        Dict with 'messages', 'labels', 'assignee', 'close', and 'hf_ticket_id' keys
    """
    messages = []

    # Initial message
    messages.append({
        "body": ticket.get("text", ""),
        "from_field": {
            "name": ticket["user"]["name"],
            "address": ticket["user"]["email"]
        },
        "delivered_at": parse_timestamp(ticket["created_at"]),
        "subject": f"[#{ticket.get('display_id', '')}] {ticket.get('subject', '')}"
    })

    # Subsequent updates
    for update in sorted(ticket.get("updates", []), key=lambda u: u["timestamp"]):
        if update.get("message"):
            messages.append({
                "body": update["message"],
                "from_field": {
                    "name": update["by"]["name"],
                    "address": update["by"].get("email", "")
                },
                "delivered_at": parse_timestamp(update["timestamp"]),
                "is_note": update.get("message_type") == "private_note"
            })

    # Enforce 400-message conversation limit
    if len(messages) > 400:
        # Log truncation for audit trail
        print(f"WARNING: Ticket {ticket['id']} has {len(messages)} messages, truncating to 400")
        messages = messages[:400]

    # Map labels
    labels = []
    if ticket.get("category"):
        labels.append(label_map.get(ticket["category"]["name"]))
    for tag in ticket.get("tags", []):
        labels.append(label_map.get(tag))
    if ticket.get("priority"):
        labels.append(label_map.get(f"Priority: {ticket['priority']['name']}"))

    return {
        "messages": messages,
        "labels": [l for l in labels if l],
        "assignee": user_map.get(ticket.get("assigned_to", {}).get("email")),
        "close": ticket.get("status", {}).get("name") in ["Closed", "Resolved"],
        "hf_ticket_id": ticket["id"]
    }
```

### Step 3: Load into Missive

```python
import hashlib

MISSIVE_BASE = "https://public.missiveapp.com/v1"
MISSIVE_TOKEN = os.environ["MISSIVE_API_TOKEN"]
CUSTOM_CHANNEL_ID = os.environ["MISSIVE_CHANNEL_ID"]

# Migration log for traceability and replay
migration_log = []

def create_missive_message(payload):
    """Create a message in Missive via custom channel.
    
    Returns the parsed JSON response on success.
    Handles HTTP 429 (rate limit) with Retry-After.
    Raises on HTTP 401 (auth), 422 (validation), or other errors.
    """
    headers = {
        "Authorization": f"Bearer {MISSIVE_TOKEN}",
        "Content-Type": "application/json"
    }
    resp = requests.post(
        f"{MISSIVE_BASE}/messages",
        headers=headers,
        json=payload
    )
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return create_missive_message(payload)  # Retry
    if resp.status_code == 422:
        print(f"Validation error: {resp.json()}")
        raise ValueError(f"Missive rejected payload: {resp.json()}")
    resp.raise_for_status()
    return resp.json()

def load_conversation(transformed, org_id):
    """Load a full conversation into Missive.
    
    Returns the Missive conversation ID, or None on failure.
    Logs every write for deterministic replay.
    """
    conversation_id = None

    for i, msg in enumerate(transformed["messages"]):
        payload = {
            "messages": {
                "account": CUSTOM_CHANNEL_ID,
                "body": msg["body"],
                "from_field": msg["from_field"],
                "delivered_at": msg["delivered_at"],
                "organization": org_id
            }
        }
        if i == 0 and msg.get("subject"):
            payload["messages"]["subject"] = msg["subject"]

        if conversation_id:
            payload["messages"]["conversation"] = conversation_id

        if transformed.get("labels") and i == 0:
            payload["messages"]["add_shared_labels"] = transformed["labels"]

        # Compute payload hash for idempotency checking
        payload_hash = hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()

        result = create_missive_message(payload)
        if not conversation_id and result:
            conversation_id = result.get("messages", {}).get("conversation", {}).get("id")

        # Log for traceability
        migration_log.append({
            "hf_ticket_id": transformed["hf_ticket_id"],
            "missive_conversation_id": conversation_id,
            "message_index": i,
            "payload_hash": payload_hash,
            "status": "success"
        })

        time.sleep(1)  # Respect rate limits

    # Close conversation if ticket was resolved/closed
    if transformed.get("close") and conversation_id:
        close_conversation(conversation_id, org_id)

    return conversation_id
```

### Step 4: Validate Data

```python
def validate_migration(hf_tickets, missive_conversations):
    """Compare source and target record counts and message fidelity.
    
    Args:
        hf_tickets: List of original HappyFox ticket objects
        missive_conversations: Dict mapping HappyFox ticket ID to Missive conversation metadata
    
    Returns:
        List of mismatch dicts with ticket_id and issue description
    """
    print(f"HappyFox tickets: {len(hf_tickets)}")
    print(f"Missive conversations: {len(missive_conversations)}")

    mismatches = []
    for ticket in hf_tickets:
        conv = missive_conversations.get(ticket["id"])
        if not conv:
            mismatches.append({"ticket_id": ticket["id"], "issue": "missing"})
            continue

        expected_msgs = 1 + len([u for u in ticket.get("updates", []) if u.get("message")])
        expected_msgs = min(expected_msgs, 400)  # Account for truncation
        actual_msgs = conv.get("message_count", 0)
        if expected_msgs != actual_msgs:
            mismatches.append({
                "ticket_id": ticket["id"],
                "issue": f"message count: expected {expected_msgs}, got {actual_msgs}"
            })

    print(f"Mismatches found: {len(mismatches)}")
    return mismatches
```

Log source ID, target ID, payload hash, attempt count, and attachment checksum for every write. If a batch fails, you want deterministic replay, not manual repair.

## Edge Cases & Failure Modes

Data migrations rarely follow the happy path. Plan for these HappyFox-to-Missive-specific failure modes.

### Attachment URL Expiry

HappyFox attachment URLs in the API response are pre-signed S3 links that expire in 5 minutes. If your extraction script pauses or crashes, you cannot resume — you must re-extract the ticket to get fresh URLs. **Mitigation:** Download every attachment immediately during extraction, before processing any other tickets. Store locally with a checksum (SHA-256) for integrity verification during re-upload. ([support.happyfox.com](https://support.happyfox.com/kb/article/1039-tickets-endpoint/?section_id=131))

### Duplicate Contacts

HappyFox allows contacts with the same email across different contact groups. Missive deduplicates contacts within a contact book by email. Run deduplication *before* loading into Missive, or you will hit API conflicts (HTTP 422). Watch for shared aliases and plus-addressing (e.g., `user+sales@company.com` vs. `user+support@company.com`) that can collapse unrelated people.

### Private Notes vs. Public Replies

HappyFox distinguishes between public replies, private notes, and client replies. Missive's custom channel messages do not natively support private/internal annotations in the same way. Use the **Posts** endpoint to inject private notes as internal comments visible only to team members. The Posts endpoint returns a simpler response (see response schema above) and does not create customer-visible messages.

### Multi-Level Relationships

HappyFox supports Contact → Contact Group → Category relationships. Missive's model is flatter: Contact → Contact Book, Conversation → Labels/Teams. Flatten these hierarchies and ensure objects are created in the right order: Organizations first, then Contacts, then Conversations.

### Long Ticket Threads

Missive caps a conversation at 400 messages. ([missiveapp.com](https://missiveapp.com/docs/core-features/conversations/merging?utm_source=openai)) If a HappyFox ticket has more than 400 updates with messages, you will need to split it across multiple Missive conversations or truncate older messages. Document this decision for stakeholder sign-off. If splitting, link conversations with a label like "Split: HF-#12345 (Part 2)".

### API Failures and Retries

Both platforms return HTTP 429 under load. Missive includes `Retry-After` headers; HappyFox does not. Build exponential backoff into your pipeline (start at 1 second, max 120 seconds, multiply by 2 on each retry). Log every failed request with the full payload and HTTP response body so you can replay failures without re-extracting from HappyFox.

### Email Thread Fragmentation Post-Migration

If a customer replies to an old email thread after migration, Missive may create a new conversation instead of appending to the imported one. This happens because imported conversations via custom channels lack the original email `Message-ID` headers. There is no automated fix — train your support team to manually merge conversations when this occurs.

## Limitations & Constraints

Be explicit with stakeholders about what Missive cannot replicate from HappyFox:

- **No ticket numbers.** Missive conversations do not have sequential IDs visible to end users. If your customers reference ticket numbers, this workflow breaks. Store the HappyFox `display_id` as a subject prefix (e.g., `[#12345]`) for search-based retrieval.
- **No SLA timers.** Missive has no built-in SLA tracking. You can approximate with Rules that flag conversations based on time, but there is no countdown timer or SLA breach reporting.
- **No CSAT surveys.** HappyFox's built-in satisfaction ratings have no Missive equivalent. Use a third-party tool (e.g., Nicereply, Klaus) integrated via webhooks.
- **No knowledge base.** Migrate your HappyFox KB to a standalone tool (Notion, GitBook, Document360). See our [knowledge base migration checklist](https://clonepartner.com/blog/blog/the-ultimate-knowledge-base-migration-checklist-a-zero-downtime-plan/) for guidance.
- **No custom objects.** Missive does not support custom objects or custom data entities.
- **Contact-level custom fields only.** There is no conversation-level custom field system.
- **API access requires Productive plan.** You need the Productive plan ($18–24/user/month) to generate API tokens. This is a gating requirement for any programmatic migration.
- **Personal tokens only.** Every Missive API token is tied to a specific user — there is no organization-level service account. If the token owner's account is deactivated, the token stops working.
- **Custom channels do not support Rules.** If you import history via custom channels, Missive's Rules engine will not fire on those conversations. ([missiveapp.com](https://missiveapp.com/docs/developers/custom-channels))
- **No archive API parameter.** There is currently no API parameter to archive conversations directly.
- **Analytics are not 1:1.** Missive analytics depend on configured business hours and inactivity settings rather than a helpdesk ticket object, so reporting must be rebuilt from scratch. ([missiveapp.com](https://missiveapp.com/docs/administration/organization-settings))

## Validation & Testing

### Record Count Comparison

After migration, verify counts match:
- Total conversations in Missive = Total tickets in HappyFox (for the migrated scope)
- Total contacts in Missive contact book = Total unique contacts in HappyFox
- Messages per conversation = Updates per ticket (plus initial message), capped at 400

### Field-Level Validation

Sample 5% of records (minimum 50, maximum 500) and manually verify:
- Conversation subject matches ticket subject (including `display_id` prefix)
- Message bodies render correctly (HTML preserved)
- Timestamps are accurate (check UTC-to-local timezone conversion)
- Attachments are accessible, download correctly, and checksums match source
- Labels match original categories and tags
- Assignees are correct
- Private notes appear as internal posts, not customer-visible messages

### Search Indexing Validation

After loading a batch, wait 15–30 minutes for Missive's search index to update, then:
- Search for 10 known ticket subjects and verify they return results
- Search for specific customer email addresses and confirm conversation linkage
- Search for the `display_id` prefix to confirm ticket number traceability

### UAT Process

1. Migrate a single category as a pilot (ideally one with 500–1,000 tickets covering diverse edge cases)
2. Have 2–3 agents review their assigned conversations
3. Verify they can find historical conversations via search
4. Confirm canned responses and rules work as expected
5. Sign off before proceeding with remaining categories

### Rollback Plan

Missive does not have a "delete all imported data" button. Your rollback options:
- Delete the custom channel — this permanently deletes all associated conversations
- If you used the Posts endpoint on existing email conversations, you cannot bulk-remove posts

**Best practice:** Run the migration into a dedicated custom channel (e.g., "HappyFox Archive"). If validation fails, delete the channel and start over. This keeps migrated data separate from new incoming email.

## Post-Migration Tasks

### Rebuild Automations

HappyFox Smart Rules do not export. Rebuild them as Missive Rules:
- **Auto-assignment rules** → Missive's "Assign user(s)" rule action with round-robin or load-balancing
- **Auto-categorization** → "Apply label" rule action based on sender, subject, or body content
- **SLA escalation** → Time-based rules that move conversations to inbox or apply warning labels

Rules require the Productive or Business plan. Missive supports up to 1,000 rules on these plans. ([missiveapp.com](https://missiveapp.com/docs/advanced-features/rules/))

### Recreate Canned Responses

Export your HappyFox Canned Actions (name + body) and recreate them as Missive Canned Responses. This is manual — there is no API for bulk canned response creation in Missive. For teams with more than 50 canned responses, budget 2–4 hours for this task.

### Set Up Webhooks for Monitoring

Configure Missive webhooks to monitor for:
- New conversations created in the archive channel (indicating data leakage or routing errors)
- Conversation state changes that might indicate automation issues

### Update DNS and Mail Routing

Connect the real shared accounts (`support@`, `sales@`, `info@`) to Missive team inboxes. If you are changing support email addresses, ensure your MX and forwarding rules point to Missive. Decide whether multi-team mail should create a copy in each team inbox or a single shared conversation.

### User Training

Missive's mental model is fundamentally different from HappyFox. Key training points:
- **No ticket numbers** — use search and labels to find conversations; the `[#display_id]` subject prefix enables search
- **Internal comments** — use @mentions in conversation comments instead of private notes
- **Assignment** — click the assign button or use rules; no "claim" button
- **Closing** — closing a conversation archives it; use snooze for "pending customer reply"
- **Thread fragmentation** — if a customer replies to a pre-migration email and it creates a new conversation, manually merge it with the imported conversation

### Monitor for Drift

For 2 weeks post-cutover, monitor for:
- Conversations that should have migrated but are missing
- Contacts that appear duplicated
- Labels that were applied incorrectly
- Search results that do not return expected historical conversations (may indicate indexing gaps)
- Duplicate conversations from the parallel-run period
- Thread fragmentation from customers replying to old email threads

## Best Practices for a Zero-Downtime Migration

1. **Back up everything first.** Export HappyFox data via API to a local JSON archive before starting. Store it permanently — this is your source of truth if anything goes wrong. Encrypt at rest.
2. **Freeze the schema.** Do not allow admins to add new custom fields or categories in HappyFox during the migration window.
3. **Run a test migration.** Pick 500–1,000 representative tickets (mix of statuses, categories, attachment sizes, thread lengths including at least one 400+ message ticket) and migrate them to a Missive test environment first.
4. **Use a custom channel for history.** Create a dedicated Missive custom channel called "HappyFox Archive" for imported conversations. This keeps migrated data cleanly separated from new incoming email and enables clean rollback by deleting the channel.
5. **Use delta syncs for zero downtime.** Extract historical data (everything older than 30 days) during the week. On cutover night, run a delta sync to catch only the tickets created or updated in the last 30 days. This eliminates downtime.
6. **Validate incrementally.** Do not wait until the full migration is done. Validate each batch of 1,000 conversations before proceeding. Wait for search indexing (15–30 minutes) before running search-based validation.
7. **Log everything.** Your API script must log every HappyFox Ticket ID alongside its new Missive Conversation ID, payload hash, and attachment checksums. If a failure occurs, this mapping file is your only path to a targeted rollback.
8. **Plan for the knowledge base separately.** Missive has no KB — decide early where your articles will live post-migration.
9. **Document your mapping decisions.** When you decide to drop a field or flatten a hierarchy, write it down. You will need this for stakeholder sign-off, compliance audits, and future reference.
10. **Version-pin your dependencies.** Record the HappyFox API version (v1.1), Missive API version (v1), and the date you verified API behavior. APIs change — if you re-run the pipeline later, reverify.

*Last verified against HappyFox API v1.1 and Missive API v1, June 2025.*

## Making the Right Migration Decision

A HappyFox to Missive migration is not a lift-and-shift. It is a platform redesign that carries historical data forward. The two systems solve different problems, and the migration forces you to confront every structural assumption your support team has built around HappyFox's ticket model.

If your team primarily uses email as the support channel and wants simpler collaboration without the overhead of a full ticketing system, Missive is a strong target. If you depend heavily on ticket numbers, SLA timers, satisfaction surveys, or a built-in knowledge base, you will need to fill those gaps with external tools.

The best HappyFox-to-Missive migration is usually selective: move the operating surface into Missive, preserve the recent history people actually use (typically 12–24 months), and keep the rest in an archive or adjacent data store. The technical work is straightforward for a team familiar with both APIs — it is the data model mapping decisions that determine success or failure.

For related guides, see:
- [How to Migrate HappyFox to Zendesk](https://clonepartner.com/blog/blog/how-to-migrate-happyfox-to-zendesk-the-technical-guide/)
- [How to Migrate HappyFox to Freshdesk](https://clonepartner.com/blog/blog/how-to-migrate-from-happyfox-to-freshdesk-the-complete-guide/)
- [Missive Migration Checklist](https://clonepartner.com/blog/blog/missive-migration-checklist/)
- [Post-Migration QA: 20 Tests to Run](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/)

## Frequently asked questions

### Can I migrate HappyFox tickets to Missive using CSV export?

Not if you need conversation history. HappyFox's native CSV export only includes the initial message and subject — it strips staff replies, client replies, and private notes. Missive's self-serve CSV import only supports contacts, not conversations. You must use the HappyFox REST API (v1.1) to extract full ticket threads for any meaningful migration.

### Does Missive have a ticket import API?

No. Missive has no dedicated ticket import endpoint. You need to use the Messages endpoint (for custom channels) or the Posts endpoint to inject historical conversation data. Each message requires a separate API call, and rate limits cap you at 300 requests per minute and 900 requests per 15 minutes.

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

For a mid-size dataset of 10,000–50,000 tickets, expect 2–4 weeks including planning, scripting, test migration, validation, and cutover. The actual data transfer can take multiple days due to Missive's API rate limits.

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

Missive has no equivalent for HappyFox's SLA timers, satisfaction survey results, ticket priority fields, knowledge base, asset management, or ticket-level custom fields. Priority can be approximated with labels, but SLA and CSAT data must be archived separately.

### Do I need a paid Missive plan to migrate data via API?

Yes. Missive API access requires the Productive plan or higher. All API tokens are personal — there is no organization-level service account — so you need a licensed user to generate the token used for migration.
