Skip to content

HappyFox to Missive Migration: The Technical Guide

Technical guide to migrating from HappyFox to Missive. Covers data model mapping, API constraints, history preservation, and step-by-step implementation.

Nachi Nachi · · 26 min read
HappyFox to Missive Migration: The Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

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)
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)

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)

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

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)

  • A Missive conversation has a 400-message hard limit (missiveapp.com)

  • 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)
  • 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)
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.
Info

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)
  • Rate limits: 500 GET and 300 POST requests per minute (support.happyfox.com)
  • 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)
  • Conversation limit: A single conversation can contain at most 400 messages (missiveapp.com)
  • 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)
  • 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):

{
  "messages": {
    "id": "msg_abc123",
    "conversation": {
      "id": "conv_xyz789",
      "subject": "Re: Order issue",
      "team": { "id": "team_456" }
    },
    "delivered_at": 1719849600
  }
}

POST /v1/posts response:

{
  "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

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

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

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

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)

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) 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 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)
  • 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)

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)

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:

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.

More from our Blog

Missive Migration Checklist
Checklist/Missive

Missive Migration Checklist

Planning a move to Missive? Use this comprehensive Missive migration checklist to protect your data, map workflows accurately, and ensure a seamless go-live without downtime.

Tejas Mondeeri Tejas Mondeeri · · 7 min read
Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration
Help Desk

Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration

Ensure your help desk migration is a success with this comprehensive 20-point post-migration QA checklist. This expert guide details the 20 essential tests needed to validate your data integrity, system functionality, user-friendliness, and performance . Learn exactly how to check everything from ticket data, attachments, and knowledge base articles to critical workflows, automations, and integrations before you go live. This process is your final line of defense against lost tickets, broken workflows, and unhappy customers.

Raaj Raaj · · 8 min read