Skip to content

Missive to Pylon Migration: The CTO's Technical Guide

A technical guide to migrating from Missive to Pylon. Covers API rate limits, Account derivation from flat contacts, object mapping, and step-by-step migration scripts.

Rishabh Rishabh · · 29 min read
Missive to Pylon Migration: The CTO's 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

Missive to Pylon Migration: The CTO's Technical Guide

Info

TL;DR — Missive to Pylon Migration

Migrating from Missive to Pylon means translating a conversation-centric collaborative inbox into an account-centric, Slack-first B2B support platform. There is no native migration path. Contacts can move via CSV export, but full conversation history requires a custom API pipeline: extract via Missive's REST API (300 req/min ceiling, max 50 conversations per page), transform the data model, and load into Pylon's /import/issues endpoint (10 req/min hard limit). The hardest problem is deriving Pylon Accounts from Missive's flat contact books — 30–40% of B2B contacts use free email providers, making domain-based grouping insufficient without CRM cross-referencing. Expect 17–20 hours of continuous API writes per 10,000 issues on the Pylon side (including retry overhead and network latency). End-to-end timelines range from 1–2 days for contacts-only moves to 2–6 weeks for full historical migrations with attachments.

Missive to Pylon is a data-model translation project. You are moving from a collaborative team inbox — where the core object is the Conversation containing emails, SMS, chat messages, comments, and shared labels — to a B2B support platform where the core object is the Issue tied to an Account with Slack-native threading, custom fields, and CRM-synced context.

No import wizard exists between the two. Every approach requires extracting data from Missive's REST API or CSV exports, restructuring the object hierarchy, and loading into Pylon via its REST API.

This guide covers the exact object mapping, API constraints on both sides, viable migration approaches with trade-offs, and the edge cases that will break your migration if you don't plan for them.

For the reverse direction, see Pylon to Missive Migration: The Technical Guide. For source-side audit prep, the Missive Migration Checklist covers the full review. For architectural comparison from a different source system, the Zendesk to Pylon Migration: The CTO's Technical Guide shows the same target architecture.

Why Teams Move from Missive to Pylon

Missive is a collaborative email and messaging app that unifies shared inboxes (email, SMS, WhatsApp, live chat) with internal team chat in a single interface. Its data model revolves around Conversations — threads that can contain multiple message types, shared across teams with labels, assignments, and internal comments (called Posts). Missive excels at team coordination around external communications but lacks formal account hierarchy and B2B-specific intelligence. (missiveapp.com)

Pylon is purpose-built for B2B customer support. Its core object is the Issue — a support thread tied to an Account, with native integrations into Slack Connect, Microsoft Teams, and Discord. Pylon provides account intelligence, health scores, CRM syncs (HubSpot, Salesforce, Attio), AI agents, and knowledge base management — features that Missive doesn't natively offer.

Teams migrate for three concrete reasons:

  • B2B account-centric support. Pylon ties every issue to a company Account with health scores, renewal data, and CRM context. Missive has contact books but no formal account hierarchy with intelligence layers — meaning there's no native way to view all open issues for a given company, track account health, or segment support load by customer tier.
  • Slack-first channel model. B2B customers who live in Slack Connect channels get native support within their workspace. Missive supports Slack notifications but doesn't treat Slack as a primary ticketing channel — messages from Slack cannot create or update Missive conversations automatically.
  • SLA enforcement and AI capabilities. Pylon ships SLA policies with per-account or per-tier configuration, native AI agents with runbooks and backtesting, and account-level reporting. Missive offers rule-based automation but not contract-bound SLAs or AI-powered resolution.

Data Model Differences: What Changes

Understanding the structural differences is the foundation of a clean migration.

Concept Missive Pylon
Core object Conversation Issue
Customer grouping Contact Books (flat) Accounts (hierarchical, CRM-synced)
People Contacts Contacts (tied to Accounts)
Messages Email/SMS/Chat messages within Conversations Messages within Issues
Internal notes Comments/Posts (visible to team) Internal Notes / Threads
Categorization Labels (nested, unlimited depth) Tags (flat) + Custom Fields (typed: text, number, dropdown, date)
Teams Teams (shared mailboxes) Teams (routing, assignment)
Canned replies Responses (HTML, per-team or org, with optional attachments) Macros (text + field update actions)
Automation Rules (trigger → action) Triggers (condition → action)
Tasks Tasks (per-conversation) Tasks & Projects
Knowledge base None (native) Knowledge Base (articles, collections)
SLA management None SLA policies (per-account, per-tier)
Custom objects None Custom Objects (API-managed)
Channel support Email, SMS, WhatsApp, Live Chat, social Email, Slack Connect, MS Teams, Discord, in-app chat
Warning

Key structural gap: Missive has no formal Account object. Contacts live in flat contact books with no company hierarchy. To populate Pylon's Account → Contact → Issue hierarchy, you must derive company associations from email domains, contact book groupings, or an external CRM source. This derivation step is the single biggest source of migration complexity — it typically consumes 1–2 days of engineering time even for teams that expect it to take 2 hours.

Migration Approaches

1. Native CSV Export → Manual Import

How it works: Export data from Missive via Settings → Login & Security → Export. Missive generates CSV files for Contacts (one per contact book), Comments, Conversations, Phone data, and Rules. Transform CSV files and load into Pylon via its API. (missiveapp.com)

When to use it: Contact-only migrations where conversation history isn't needed, or teams comfortable keeping Missive read-only as an archive.

Pros: No API coding required for extraction. Full contact data included per contact book. Quick for small datasets.

Cons: Conversation exports are flat CSV — no threaded message structure, no attachments as files. No way to selectively filter conversations by date range in the export. No native Pylon CSV import — you still need the API on the load side.

Scalability: Small datasets only (< 1,000 contacts).

Complexity: Low.

2. API-Based Custom Pipeline

How it works: Build a pipeline that reads from Missive's REST API (https://public.missiveapp.com/v1/) and writes to Pylon's REST API (https://api.usepylon.com/). Extract conversations, messages, and contacts; transform into Pylon's Account → Contact → Issue → Message hierarchy; load via Pylon's endpoints. Missive API tokens require a Productive plan ($18/user/month minimum); Pylon API tokens require Admin role. (missiveapp.com)

When to use it: Any production migration requiring conversation history, threaded messages, attachments, and relationship integrity.

Pros: Full data fidelity — messages, timestamps, internal notes, attachments. Historical timestamps preserved via Pylon's import endpoint. Programmatic deduplication and validation. Resumable with checkpoint logic.

Cons: Requires engineering effort (Python or Node.js, typically 3–5 days for a production-grade script). Pylon's 10 req/min Issues ceiling creates a hard throughput bottleneck. Missive's rate limits (300/min, 900/15 min rolling) require pacing on extraction. Attachment migration requires a separate download → upload workflow.

Scalability: The only self-serve approach that scales to production workloads. Budget 17–20 hours per 10,000 issues for Pylon API writes (including retries, network latency, and 429 backoff).

Complexity: High.

3. Third-Party Migration Tools

Help Desk Migration (help-desk-migration.com) supports both Missive and Pylon as source/target platforms. Their Migration Wizard provides a no-code interface for mapping objects, with a free demo migration of up to 20 conversations and a delta migration option.

When to use it: Mid-size migrations (1K–50K conversations) where engineering bandwidth is limited.

Pros: No-code UI for object mapping. Delta migration catches records created during the migration window.

Cons: Per-record pricing (typically $0.05–$0.15 per record depending on volume tier — verify current pricing on their site). Limited control over complex transformations (e.g., deriving Accounts from email domains). Edge cases around Account derivation and custom field mapping may require manual intervention.

Scalability: Good for mid-size datasets. May hit limitations on complex data models.

Complexity: Low–Medium.

4. Middleware Platforms (Zapier, Make, n8n)

How it works: Use integration platforms to create workflows that move data between Missive and Pylon. Missive has a native Zapier integration that triggers on new contacts, messages, and comments. (missiveapp.com)

When to use it: Ongoing sync of new data during a transition period. Not suitable for historical backfill.

Pros: No-code setup. Good for delta sync during cutover.

Cons: Not designed for bulk historical migration. Per-task pricing adds up at scale (Zapier charges per task; 10K tasks/month on the Starter plan). No batch operations — one record per execution. Missive webhooks expect endpoints to respond within 15 seconds, and extra API calls are usually required for full message payloads.

Scalability: Small ongoing workflows only. Not viable for historical data.

Complexity: Low.

5. Custom ETL Pipeline (Airbyte / dlt / Fivetran)

How it works: Use a data integration framework to extract from Missive into a staging database (e.g., Postgres, BigQuery), transform with SQL or Python, then load into Pylon via API. This is the right design when Missive is only one source and you also need CRM or documentation data pulled into the same cutover.

Pros: Airbyte has a Missive source connector. Full control over transformation logic. Intermediate staging allows SQL-based validation and audit queries. Replayable from staging if Pylon load fails — no need to re-extract from Missive.

Cons: Infrastructure overhead (staging DB, orchestration tool like Airflow or Dagster). Still bound by Pylon's 10 req/min Issues ceiling on the load side. Requires data engineering skills.

Scalability: Excellent for large datasets. Staging layer decouples extraction from loading, allowing independent debugging.

Complexity: High.

6. Managed Migration Service

How it works: A specialized engineering team handles the entire extraction, transformation, and load process. Pylon's own migration documentation describes historical ticket and knowledge base migration as separate workstreams, including external comments, internal notes, custom fields, and time metrics — destination fields must be set up by the customer before import begins. (support.usepylon.com)

When to use it: When you need threaded history, attachments, field fidelity, or a tight go-live window — and lack the internal engineering bandwidth.

Pros: Higher fidelity than DIY. Better handling of edge cases (Account derivation, rate limits, attachment re-hosting). Zero engineering time from your team.

Cons: Requires budget allocation. Still needs internal ownership for schema setup and UAT.

Scalability: Enterprise.

Complexity for your team: Zero.

Migration Approach Comparison

Approach Data Fidelity Scalability Complexity Best For
CSV Export Contacts only Small Low Contact-only moves, archive exits
API Pipeline Full Enterprise High Full historical migration
Help Desk Migration Good Mid-size Low–Med Teams without dev resources
Middleware (Zapier) Partial Small Low Delta sync during cutover
ETL (Airbyte/dlt) Full Enterprise High Large datasets with staging needs
Managed Service Full Enterprise Zero (your team) Mid-market and Enterprise teams

Recommendations by scenario:

  • Small team, < 1K conversations, no dev resources: Help Desk Migration or CSV + manual cleanup
  • Mid-size (1K–10K), limited engineering: Help Desk Migration with manual Account mapping review
  • Enterprise (10K+), dedicated dev team: Custom API pipeline or ETL with staging
  • Ongoing sync during transition: Zapier/Make for new conversations; API pipeline for historical

Common DIY Failure Modes

Building a Missive-to-Pylon pipeline in-house is feasible, but these failure modes consistently derail migrations:

  • Orphaned contacts: Contacts loaded without Account associations become unlinked records in Pylon, breaking the B2B support model. Every Pylon Contact should have an account_id.
  • Timestamp loss: Using Pylon's standard POST /issues instead of the /import/issues endpoint overwrites historical dates with current server time. All historical data must go through the import endpoint.
  • Rate limit cascading failures: Hitting Pylon's 10 req/min ceiling without exponential backoff logic causes silent data loss on retries. The naive approach (recursive retry on 429) risks stack overflows — use iterative backoff with a max-retry cap instead.
  • Broken attachments: Expired Missive attachment URLs fail to render in Pylon. Downloading and re-uploading doubles network I/O and storage costs during migration. Download all attachments to local/S3 storage during extraction, not during load.
  • Lost internal context: Dropping Missive Posts (internal comments) because they don't map cleanly to Pylon's default message schema. Internal comments must be imported with is_private: true in the message payload.
  • Partial thread imports: Pylon's /import/issues may reject an entire issue if any message in the thread has a malformed payload (e.g., null body_html, invalid contact_id). Validate every message object before submission. Log and quarantine issues that fail, then fix and retry from staging.

Pre-Migration Planning

Data Audit Checklist

Before writing any migration code, audit your Missive data:

  • Conversations: Total count by status (active, closed, snoozed, trashed)
  • Messages: Average messages per conversation (affects API call volume — a 10K conversation dataset with avg 15 messages/conversation = ~150K message records)
  • Contacts: Total across all contact books, duplicates across books (count unique emails)
  • Labels: Full label hierarchy (Missive supports nested labels to arbitrary depth)
  • Teams: Number of teams and shared mailbox configurations
  • Responses: Canned reply count and format (HTML with attachments)
  • Rules: Active automation rules and their trigger conditions
  • Attachments: Total volume (GB) — this drives migration timeline. Attachments at 10 req/min on the Pylon side add ~1.7 hours per 1,000 files.
  • Integrations: Connected apps that depend on Missive conversation IDs
  • Multi-channel conversations: Count of conversations containing SMS or WhatsApp messages (these channels have no Pylon equivalent)

Scope Definition

Decide what moves and what stays:

  • Always migrate: Active contacts, open conversations, labels/tags, canned replies
  • Conditionally migrate: Closed conversations (by age cutoff — e.g., last 12 months), attachments (by size threshold)
  • Leave behind: Trashed conversations, spam, personal/private conversations that aren't team-relevant, draft messages (unsent drafts cannot be migrated via API)

For a framework on what to include vs. exclude, see Help Desk Data Migration Playbook: What Data to Move and What to Leave Behind.

Pylon Target Schema

Before loading any records, configure your Pylon environment. Pylon custom fields are typed (text, number, dropdown, date, boolean), slugged fields across Accounts, Contacts, and Issues — and required fields can block issue updates or closure. Create these before import: (docs.usepylon.com)

  1. Account fields — map any Missive contact book metadata
  2. Contact fields — map any custom contact attributes
  3. Issue fields — map Missive conversation metadata not covered by default fields
  4. Tags — create all tags that correspond to Missive labels (post-flattening)
  5. Teams — match Missive team structure
  6. SLA policies — configure before import so issues get SLA assignments on creation
  7. Views — build filtered views for post-migration validation
  8. Ticket forms — if using Pylon's form-based intake

Migration Strategy

Strategy Risk Downtime When to Use
Big bang Higher Minimal (single cutover) Small datasets (< 2K conversations), tight timelines
Phased Lower None (parallel operation) Large datasets, risk-averse teams
Incremental Lowest None Ongoing sync with gradual cutover

For most Missive-to-Pylon moves, a phased approach works best: migrate historical data first while both systems run in parallel, then cut over active conversations on a planned date. Running both systems simultaneously creates a risk of Missive intercepting emails meant for Pylon, so plan your DNS MX record changes and email forwarding cutover carefully — update forwarding rules in a single, coordinated step.

Field-Level Data Mapping

Contacts

Missive Field Pylon Field Notes
first_name + last_name name Pylon uses a single name field — concatenate with space separator
email email Primary email; Pylon enforces uniqueness per Account
phone_number phone If available
contact_book account_id (derived) Map contact books to Accounts where logical; see Account derivation section
avatar_url avatar_url Direct URL mapping; verify URLs haven't expired
Custom fields Custom Contact Fields Must be pre-created in Pylon settings with matching type

Conversations → Issues

Missive Field Pylon Field Notes
id external_id Store Missive conversation ID for cross-reference and dedup
subject title Direct mapping; use first 100 chars of body if subject is blank
assignees assignee_id Pylon supports single assignee; map primary (most recent or team lead)
labels tag_ids Create tags in Pylon first, then map by name; flatten nested labels
team team_id Map Missive teams to Pylon teams by name
created_at created_at Preserved via /import/issues only — not via standard POST /issues
closed_at closed_at Status mapping required
Status: active open Direct
Status: closed closed Direct
Status: snoozed open Snooze schedule is lost — no Pylon equivalent
Status: trashed (skip) Don't import trashed conversations

Messages

Missive Field Pylon Field Notes
body (HTML) body_html Sanitize for Pylon's rendering; replace CID inline images with hosted URLs
from_field user_id or contact_id Agent messages → user_id; customer messages → contact_id
created_at Timestamp in import payload Preserved in /import/issues message array
attachments [].url Upload → new URL Download binary from Missive, upload to Pylon /attachments, reference new URL
type (email/sms/chat) (varies) SMS and WhatsApp messages import as plain text since Pylon lacks these channels

Comments → Internal Notes

Missive Field Pylon Field Notes
Comment body body_html with is_private: true Must set private flag explicitly
Comment author user_id Map to Pylon agent via user mapping dictionary
Comment created_at Timestamp Preserved in import payload
Tip

Label → Tag mapping: Missive labels are hierarchical (nested to arbitrary depth). Pylon tags are flat strings. Flatten your label hierarchy before migration using one of these strategies:

  • Concatenate: Product > Feature > Requestproduct-feature-request
  • Leaf only: Product > Feature > Requestrequest
  • Multiple tags: Create separate tags for each level: product, feature, request

Choose based on your Pylon reporting needs. Concatenation preserves the most context but creates long tag names. Multiple tags enable flexible filtering but lose the hierarchy relationship.

Migration Architecture

The data flow follows a standard ETL pattern:

Missive REST API  →  Extract (Python/Node.js)  →  Staging (JSON/DB)
                                                        ↓
                                                  Transform (clean, map, derive Accounts)
                                                        ↓
Pylon REST API  ←  Load (rate-limited writer)  ←  Validated payload

Your loading script must operate in strict dependency order: Accounts → Contacts → Issues (with Messages) → Attachments. You cannot create a message without an Issue ID, and you cannot create an Issue without an Account ID.

Extraction: Missive API Constraints

  • Base URL: https://public.missiveapp.com/v1/
  • Auth: Bearer token (Productive plan required)
  • Rate limits: 5 concurrent requests, 300/min, 900/15 min rolling window (missiveapp.com)
  • Conversations pagination: until cursor based on last_activity_at, max 50 per page
  • Messages pagination: Up to 10 messages per page per conversation — a conversation with 150 messages requires 15 API calls
  • Key endpoints: GET /conversations, GET /conversations/:id/messages, GET /contacts

A typical Missive conversation object looks like this (simplified):

{
  "id": "conv_abc123",
  "subject": "Billing question",
  "created_at": 1700000000,
  "last_activity_at": 1700100000,
  "assignees": [{"id": "user_1", "name": "Alice"}],
  "labels": [{"id": "lbl_1", "name": "Billing"}],
  "team": {"id": "team_1", "name": "Support"},
  "shared_labels": [],
  "messages_count": 8,
  "organization": {"id": "org_1"}
}

Messages are fetched separately per conversation and include body, from_field, to_field, type (email/sms/chat), attachments, and created_at.

Loading: Pylon API Constraints

  • Base URL: https://api.usepylon.com/
  • Auth: Bearer token (Admin role required)
  • Rate limits by endpoint: (docs.usepylon.com)
    • Issues (GET/POST): 10 req/min
    • Issue updates (PATCH): 20 req/min
    • Messages: 20 req/min
    • Accounts, Contacts, Tags: 60 req/min
    • Attachments: 10 req/min
    • Issue deletion (DELETE /issues/:id): shares the 10 req/min ceiling — rollback of 10K issues also takes ~17 hours
  • Historical import: POST /import/issues — accepts full message thread with timestamps in a single request
  • Pagination: Cursor-based; GET /issues enforces a start_time/end_time window no longer than 30 days, which matters for validation queries
Danger

The bottleneck is Pylon's Issues API. At 10 requests per minute, loading 10,000 issues takes approximately 17–20 hours of continuous writes (the theoretical minimum is 16.7 hours, but retries, 429 backoffs, and network latency add 15–20% overhead). Loading 50,000 issues takes 4–5 days. The rate limit is per-organization, not per-token — you cannot parallelize around it with multiple API keys. Factor this into your migration timeline and schedule off-peak runs when live agents aren't competing for API capacity.

Step-by-Step Migration Process

Step 1: Extract Data from Missive

Take a full Missive export (Settings → Login & Security) as a backup, then use API pulls for controlled batching and incremental reruns. (missiveapp.com)

import requests
import time
import json
import logging
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("missive_extract")
 
MISSIVE_TOKEN = "missive_pat-..."
BASE_URL = "https://public.missiveapp.com/v1"
HEADERS = {
    "Authorization": f"Bearer {MISSIVE_TOKEN}",
    "Content-Type": "application/json"
}
 
def extract_conversations():
    """Extract all conversations with cursor-based pagination.
    Missive paginates using 'until' based on last_activity_at.
    Max 50 conversations per page, 300 req/min shared pool."""
    conversations = []
    until = None
    while True:
        params = {"limit": 50, "all": "true"}
        if until:
            params["until"] = until
        resp = requests.get(f"{BASE_URL}/conversations",
                           headers=HEADERS, params=params)
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            logger.warning(f"Rate limited. Waiting {retry_after}s")
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        batch = resp.json().get("conversations", [])
        if not batch:
            break
        conversations.extend(batch)
        logger.info(f"Extracted {len(conversations)} conversations")
        until = batch[-1]["last_activity_at"]
        time.sleep(0.5)  # Stay well under 300/min
    return conversations
 
def extract_messages(conversation_id):
    """Extract all messages for a conversation.
    Missive returns up to 10 messages per page.
    Must paginate for conversations with >10 messages."""
    all_messages = []
    after = None
    while True:
        params = {}
        if after:
            params["after"] = after
        resp = requests.get(
            f"{BASE_URL}/conversations/{conversation_id}/messages",
            headers=HEADERS, params=params
        )
        if resp.status_code == 429:
            time.sleep(60)
            continue
        resp.raise_for_status()
        messages = resp.json().get("messages", [])
        if not messages:
            break
        all_messages.extend(messages)
        if len(messages) < 10:
            break  # Last page
        after = messages[-1]["id"]
        time.sleep(0.5)
    return all_messages

Step 2: Derive Accounts from Contact Data

Missive doesn't have an Account object. You need to derive company groupings from email domains:

from collections import defaultdict
 
# Comprehensive free email provider list — extend as needed
FREE_DOMAINS = {
    "gmail.com", "yahoo.com", "outlook.com", "hotmail.com",
    "icloud.com", "aol.com", "protonmail.com", "zoho.com",
    "yandex.com", "mail.com", "gmx.com", "fastmail.com",
    "tutanota.com", "hey.com", "live.com", "msn.com"
}
 
def derive_accounts(contacts):
    """Group contacts into company accounts by email domain.
    
    Returns:
        domain_groups: dict of domain -> list of contacts (for Account creation)
        unmatched: list of contacts with free email domains (need manual review)
    """
    domain_groups = defaultdict(list)
    unmatched = []
    for contact in contacts:
        email = contact.get("email", "").lower().strip()
        if not email or "@" not in email:
            unmatched.append(contact)
            continue
        domain = email.split("@")[-1]
        if domain in FREE_DOMAINS:
            unmatched.append(contact)
        else:
            domain_groups[domain].append(contact)
    
    logger.info(f"Derived {len(domain_groups)} accounts from domains")
    logger.info(f"{len(unmatched)} contacts unmatched (free email or no email)")
    return domain_groups, unmatched
Warning

The free email domain problem: In typical B2B migrations, 30–40% of contacts use free email providers (Gmail, Yahoo, Outlook). These contacts cannot be auto-grouped into company Accounts. You have three options:

  1. Cross-reference against a CRM (HubSpot, Salesforce) to find company associations — highest fidelity, requires CRM API integration
  2. Assign to a catch-all "Individual Contacts" Account — quick, but creates a large, unsegmented bucket in Pylon
  3. Flag for manual review post-migration — most accurate, most labor-intensive

Do not group all Gmail addresses under a single Account — that creates a massive, useless record in Pylon that breaks account-level reporting and SLA tracking.

Step 3: Map Users

Export all Missive users and create them as agents in Pylon. Build a mapping dictionary (missive_user_idpylon_user_id) — you need this to correctly attribute historical messages and internal notes to the right agents.

def build_user_mapping(missive_users, pylon_users):
    """Map Missive users to Pylon users by email address.
    Users must exist in Pylon before import — create manually or via SCIM."""
    mapping = {}
    pylon_by_email = {u["email"].lower(): u["id"] for u in pylon_users}
    unmapped = []
    for mu in missive_users:
        email = mu.get("email", "").lower()
        if email in pylon_by_email:
            mapping[mu["id"]] = pylon_by_email[email]
        else:
            unmapped.append(mu)
            logger.warning(f"No Pylon user for Missive user: {email}")
    return mapping, unmapped

Step 4: Load into Pylon

Create Accounts first, then Contacts, then Issues with full message threads. Use iterative backoff (not recursive) to avoid stack overflows on sustained 429 responses.

import time
 
PYLON_TOKEN = "pylon_..."
PYLON_BASE = "https://api.usepylon.com"
PYLON_HEADERS = {
    "Authorization": f"Bearer {PYLON_TOKEN}",
    "Content-Type": "application/json"
}
 
MAX_RETRIES = 5
 
def api_call_with_backoff(method, url, payload=None, max_retries=MAX_RETRIES):
    """Make API call with iterative exponential backoff on 429."""
    for attempt in range(max_retries):
        if method == "POST":
            resp = requests.post(url, headers=PYLON_HEADERS, json=payload)
        elif method == "GET":
            resp = requests.get(url, headers=PYLON_HEADERS)
        else:
            raise ValueError(f"Unsupported method: {method}")
        
        if resp.status_code == 429:
            wait = min(60 * (2 ** attempt), 300)  # Max 5 min wait
            logger.warning(f"429 on {url}. Attempt {attempt+1}/{max_retries}. Waiting {wait}s")
            time.sleep(wait)
            continue
        resp.raise_for_status()
        return resp.json()
    
    raise Exception(f"Max retries exceeded for {url}")
 
def create_account(name, domain):
    payload = {"name": name, "domains": [domain]}
    result = api_call_with_backoff("POST", f"{PYLON_BASE}/accounts", payload)
    return result["data"]["id"]
 
def create_contact(name, email, account_id):
    payload = {"name": name, "email": email, "account_id": account_id}
    result = api_call_with_backoff("POST", f"{PYLON_BASE}/contacts", payload)
    return result["data"]["id"]
 
def import_issue(title, messages, contact_id, account_id, 
                 tags, created_at, external_id):
    """Import a complete issue with full message thread.
    Uses /import/issues to preserve historical timestamps.
    Each message must have either contact_id or user_id, not both."""
    payload = {
        "title": title,
        "contact_id": contact_id,
        "account_id": account_id,
        "tag_ids": tags,
        "created_at": created_at,
        "external_id": external_id,  # Missive conversation ID for dedup
        "messages": [
            {
                "body_html": msg["body_html"],
                "is_private": msg.get("is_internal", False),
                "contact_id": msg.get("contact_id"),
                "user_id": msg.get("agent_id"),
                "created_at": msg.get("created_at"),
            }
            for msg in messages
        ]
    }
    # Validate payload before submission
    for i, m in enumerate(payload["messages"]):
        if not m.get("body_html"):
            logger.error(f"Message {i} has empty body_html in issue {external_id}")
            m["body_html"] = "<p>[Empty message]</p>"
    
    result = api_call_with_backoff("POST", f"{PYLON_BASE}/import/issues", payload)
    logger.info(f"Imported issue {external_id} -> {result['data']['id']}")
    return result
 
    # Rate pacing: 10 req/min = 1 request every 6 seconds
    time.sleep(6.5)  # Slight buffer

Step 5: Migrate Attachments

For every Missive message containing an attachment, download the binary from the Missive URL and upload it to Pylon's attachment endpoint, then reference the new Pylon URL in the message body or metadata. Pylon's attachment endpoint is also rate-limited to 10 req/min, so attachment-heavy migrations stretch timelines significantly — 5,000 attachments adds approximately 8.5 hours.

Best practice: Download all attachments to local storage or S3 during the extraction phase. Do not rely on Missive URLs being available during the load phase — they may expire.

Failed attachment downloads (expired URLs, deleted files) should be logged with the source conversation ID, not retried — they indicate already-lost data. Create a post-migration report of missing attachments.

Inline images require special attention. Missive emails often contain inline images referenced by Content-ID (CID) in HTML like <img src="cid:image001@example.com">. If you do not parse the HTML, download the image, upload it to Pylon, and replace the CID reference with the new hosted URL, the images will appear as broken links in Pylon.

import re
 
def replace_cid_images(html_body, cid_to_url_map):
    """Replace CID references in HTML with hosted URLs.
    cid_to_url_map: dict of content_id -> uploaded_pylon_url"""
    for cid, url in cid_to_url_map.items():
        html_body = re.sub(
            rf'src=["\']cid:{re.escape(cid)}["\']',
            f'src="{url}"',
            html_body
        )
    return html_body

Step 6: Handle Multi-Channel Conversations

Missive conversations can contain a mix of email, SMS, WhatsApp, and chat messages within a single thread. Pylon does not support SMS or WhatsApp as channels.

Strategy: Import all messages regardless of original channel type. SMS and WhatsApp messages should be imported as plain-text messages within the Pylon issue, with a prefix indicating the original channel:

[SMS] Original message text here

This preserves the conversation context while making it clear which messages came from channels that don't exist in Pylon. Document this transformation for agents during training.

Step 7: Cut Over Active Email Threads

Pylon's standard historical migration path handles closed tickets. For active email conversations, you need a separate approach: reply on the existing thread and CC your Pylon support email so future replies land in Pylon. This creates a clean handoff — the customer sees a continuous thread, and new replies are captured by Pylon's email ingestion. This is a separate workstream from the historical data load. (support.usepylon.com)

For Slack-based customers, configure Slack Connect channels in Pylon before cutover. New messages in connected Slack channels will automatically create Pylon issues.

Step 8: Validate Migrated Data

After loading, run validation checks:

def validate_migration(source_counts, id_mapping):
    """Validate migrated data against source counts.
    id_mapping: dict of missive_conv_id -> pylon_issue_id"""
    
    # Count-level validation
    print(f"Source conversations: {source_counts['conversations']}")
    print(f"Pylon issues created: {len(id_mapping)}")
    print(f"Delta: {source_counts['conversations'] - len(id_mapping)}")
    
    # Sample 5% of records (min 50, max 200) for field-level accuracy
    import random
    sample_size = max(50, min(200, len(id_mapping) // 20))
    sample_ids = random.sample(list(id_mapping.items()), sample_size)
    
    errors = []
    for missive_id, pylon_id in sample_ids:
        # Fetch Pylon issue — note 30-day window constraint on GET /issues
        pylon_issue = api_call_with_backoff(
            "GET", f"{PYLON_BASE}/issues/{pylon_id}")
        
        # Load corresponding source record from staging
        source = load_from_staging(missive_id)
        
        # Compare fields
        if pylon_issue["data"]["title"] != source["subject"]:
            errors.append(f"Title mismatch: {missive_id}")
        if pylon_issue["data"].get("external_id") != missive_id:
            errors.append(f"External ID missing: {missive_id}")
        # Compare message counts, timestamps, tags...
    
    print(f"Sampled {sample_size} records. Errors: {len(errors)}")
    for e in errors:
        print(f"  - {e}")

Edge Cases and Failure Modes

Duplicate Contacts Across Contact Books

Missive allows the same contact to exist in multiple contact books. Pylon expects unique contacts (by email) per Account. Deduplicate by normalized email before loading. Pylon's import endpoint does allow the same email address across different Accounts, which helps for contacts who legitimately belong to multiple companies — but within a single Account, email must be unique.

Multi-Assignee Conversations

Missive supports multiple assignees per conversation. Pylon Issues have a single assignee_id. Pick the primary assignee (most recent or team lead) and map additional assignees as followers using PATCH /issues/:id/followers. Document the assignee selection logic so agents understand why assignments may differ.

Multi-Participant Threads

Missive allows conversations with multiple external CCs. Capture all participants so future replies route correctly in Pylon. Store CC addresses as contact associations on the issue.

Nested Labels → Flat Tags

Missive labels can be nested (e.g., Product > Feature > Request). Pylon tags are flat strings. Decide on a flattening strategy before migration:

  • Concatenate: product-feature-request — preserves hierarchy, long names
  • Leaf only: request — short, but loses context
  • Multiple tags: Create separate tags for each level — flexible filtering, no hierarchy

Conversation Merges

Missive supports conversation merging where multiple threads combine into one. Merged conversations have a "replaced" source conversation ID. When extracting, check for the replaced_by field — skip replaced conversations and only migrate the surviving target conversation. Migrating both creates duplicates.

Generic Email Domains

Emails from @yahoo.com or @outlook.com will group together if you solely rely on domain-to-account mapping. Maintain the FREE_DOMAINS exclusion list (see code above) and handle those contacts separately — either via a catch-all Account, CRM cross-reference, or manual review.

Heavy Threads and Pagination

Missive's API returns messages paginated at 10 per page. Conversations with hundreds of messages require multiple paginated calls. Do not assume the first API page contains the complete thread. The extraction script above handles this with the after cursor, but verify by comparing messages_count from the conversation object against your extracted count.

Agency and Partner Relationships

If you support agency customers, use Pylon's partner accounts and subaccounts instead of folding agency staff into customer accounts. Missive's "one client = one team space" pattern does not automatically become a clean account hierarchy in Pylon. Decide this model up front — changing account structure post-migration requires re-linking all contacts and issues. (docs.usepylon.com)

Responses → Macros

Missive Responses are HTML-formatted canned replies with optional attachments and variable interpolation. Pylon Macros support text actions and field updates but have a different templating syntax. Manual recreation is typically required — there is no Pylon API endpoint for bulk macro creation that accepts HTML templates. Export Missive Responses early and rebuild them in Pylon before cutover. Budget 1–2 hours for teams with fewer than 50 responses; larger libraries may take a full day.

Webhook-Based Delta Sync

For the delta migration window (between initial load and cutover), Missive webhooks can supplement polling. Configure a webhook endpoint that receives new conversation and message events, stages them locally, and queues them for Pylon import. Note: Missive webhooks expect your endpoint to respond within 15 seconds — if your processing takes longer, acknowledge the webhook immediately and process asynchronously.

Limitations and Constraints

Data That Won't Transfer

  • Analytics and reporting history — must be rebuilt in Pylon from migrated issue data
  • Rule configurations — Missive Rules cannot be programmatically exported in a machine-readable format; manual recreation required in Pylon's Triggers system. The CSV export provides rule names and conditions but not action configurations.
  • Integration states — connected apps (GitHub, Stripe, etc.) need reconfiguration in Pylon
  • Private conversations — personal inbox data stays in Missive
  • Unsent drafts — cannot be migrated via API; agents must send or discard drafts before cutover
  • Snoozed timestamps — a snoozed conversation migrates as a standard open issue, losing its snooze schedule
  • Collaborative drafts — Missive collaborative drafts and personal inbox semantics have no direct Pylon equivalent
  • Live Chat widget settings — Pylon's chat widget requires separate configuration
  • SMS/WhatsApp message routing — messages import as text but cannot route through these channels in Pylon

For guidance on handling automations during migration, see Your Help Desk Data Migration's Secret Saboteur: Automations, Macros, and Workflows.

Pylon vs. Missive Feature Gaps

  • No email-as-channel parity: Missive treats email as a first-class channel with full threading, CC/BCC handling, and mail merge. Pylon is architected around Slack and chat — email threading may feel different to agents. Expect a 1–2 week adjustment period.
  • Single assignee: Pylon Issues support one assignee (plus followers). Missive's multi-assignee model has no direct equivalent.
  • No nested tags: Pylon tags are flat; Missive's nested label hierarchy requires flattening.
  • No native SMS/WhatsApp: Missive supports SMS and WhatsApp natively. Pylon doesn't — if these channels are in active use, plan for a coverage gap or third-party integration (e.g., Twilio for SMS).

API Rate Limits: The Full Math

Resource Missive (Extract) Pylon (Load) Notes
Conversations/Issues 300/min (shared pool) 10/min Pylon is the bottleneck
Contacts 300/min (shared pool) 60/min
Accounts N/A 60/min
Messages 300/min (shared pool) 20/min Included in /import/issues payload
Attachments 300/min (shared pool) 10/min Separate upload workflow
Issue deletion N/A 10/min Same ceiling as creation — rollback is slow

Pylon's Issues endpoint is the binding constraint. Real-world timelines including retries and backoff:

  • 1K issues → ~2 hours
  • 5K issues → ~9 hours
  • 10K issues → ~17–20 hours
  • 25K issues → ~45–50 hours (~2 days)
  • 50K issues → ~90–100 hours (~4 days)

These estimates assume issues are pre-validated and the failure rate is under 5%. Higher failure rates extend timelines due to retry queues.

Validation and Testing

Record Count Comparison

After migration, compare:

  • Total Accounts created vs. expected (from domain derivation + catch-all)
  • Total Contacts vs. Missive contact export count (minus deduped)
  • Total Issues vs. Missive conversation count (minus excluded categories: trashed, spam, merged-replaced)
  • Total Messages per Issue vs. source message count
  • Total Attachments successfully uploaded vs. total in source

Field-Level Validation

Sample at least 5% of records (minimum 50, maximum 200) and verify:

  • Issue title matches conversation subject
  • Message body HTML renders correctly in Pylon's UI
  • Timestamps are preserved (not overwritten with import date)
  • Contact ↔ Account associations are correct
  • Tags match the expected label mappings (post-flattening)
  • Internal notes are present and marked private
  • Attachments are accessible and display correctly (click-test a sample)
  • external_id is set to the Missive conversation ID

Edge-Case Validation Buckets

In addition to random sampling, explicitly validate at least one record from each of these categories:

  • Merged conversations (only the surviving thread should exist)
  • Multi-contact Accounts (contacts correctly grouped)
  • Attachment-heavy threads (>10 attachments per conversation)
  • Internal-comment-only conversations (no external messages)
  • Free-email-domain contacts (correctly routed to catch-all or CRM-matched Account)
  • Long threads (>100 messages, verify pagination completeness)
  • Multi-channel threads (SMS/WhatsApp messages preserved as text)

UAT Process

  1. Have 2–3 agents review their migrated conversations in Pylon
  2. Search for known customers to verify Account integrity
  3. Open attachment-heavy issues and verify files are accessible
  4. Run a basic issue count by team/tag to verify distribution
  5. Send a real reply and verify routing works
  6. Test that Pylon reopens closed issues when new activity arrives (docs.usepylon.com)
  7. Verify that the Missive conversation ID is searchable via external_id in Pylon

A practical sampling rule: 5% of migrated records, minimum 50, maximum 200, plus every known edge-case bucket (at least one from each category above).

Rollback Planning

Pylon allows issue deletion via API (DELETE /issues/:id). If validation fails:

  1. Delete all imported issues (maintain an ID log during import)
  2. Fix the transformation logic
  3. Re-run from staging data (no need to re-extract from Missive)

Important: Issue deletion shares the 10 req/min rate limit. Deleting 10K issues takes ~17 hours — the same as creating them. Factor this into your rollback timeline.

Keep Missive read-only until UAT signoff. Do not dismantle forwarding or revoke Missive access until your team has completed validation. Maintain Missive access for at least 30 days post-cutover as an archive reference.

Post-Migration Tasks

  • Rebuild automations: Recreate Missive Rules as Pylon Triggers. Map trigger conditions (label applied, team assigned) to Pylon equivalents. Test each trigger with a sample issue before enabling. Pylon's migration docs treat process configuration as a first-class workstream — not an afterthought. (support.usepylon.com)
  • Recreate canned replies: Rebuild Missive Responses as Pylon Macros manually. Adjust variable syntax to match Pylon's templating format.
  • Update DNS/forwarding: Redirect your support email addresses from Missive to Pylon. Update MX records if applicable. Revoke Missive's access to shared inboxes to prevent duplicate email fetching. Do this in a single coordinated step to avoid split-brain email routing.
  • Configure AI agents: If you're adopting Pylon's AI features, import knowledge base articles and configure training data. Historical migrated issues can serve as AI training data — this is one reason full conversation history migration pays dividends beyond archival.
  • Reconnect integrations: CRM syncs (HubSpot, Salesforce, Attio), issue trackers (Linear, Jira, GitHub), and communication channels (Slack Connect) need fresh configuration in Pylon.
  • Train agents: Pylon's UI, keyboard shortcuts, and workflow differ significantly from Missive's collaborative inbox model. Key differences: single assignee vs. multi-assignee, flat tags vs. nested labels, Slack-centric vs. email-centric. Schedule training sessions before fully cutting over. The Help Desk Data Migration Training Plan has a ready-made framework.
  • Monitor for 2 weeks: Watch for orphaned contacts, missing attachments, and broken Account associations. Run validation queries daily for the first week. Track agent-reported issues in a dedicated Pylon tag or Slack channel.

Lessons from Production Migrations

Three patterns consistently cause delays in Missive-to-Pylon migrations:

  1. Underestimating Account derivation. Teams budget 2 hours for this and it takes 2 days. Missive's flat contact model means every contact needs a company association — and the free-email-domain problem (30–40% of B2B contacts use Gmail) has no fully automated solution. Plan for CRM cross-referencing and a manual review queue.

  2. Ignoring the rate limit math. Engineering teams assume they can parallelize the Pylon load. The 10 req/min ceiling is per-organization, not per-token — multiple API keys don't help. Schedule accordingly and communicate realistic timelines to stakeholders.

  3. Skipping canned reply migration. Teams focus on conversations and contacts, then realize on Day 1 that agents have no macros. Export Missive Responses early and rebuild in Pylon before cutover.

Operational best practices:

  • Run a delta migration. Migrate 95% of your historical data a week before go-live. On cutover weekend, run a delta script that only fetches Missive conversations updated in the last 7 days. This minimizes downtime to hours instead of days.
  • Use external_id for deduplication. Set the Missive conversation ID as the external_id on Pylon issues. This prevents duplicates on re-runs, creates a reliable cross-reference for QA, and enables searching Pylon by Missive conversation ID.
  • Log everything. Every API call, response code, and failed record should be logged with the source Missive ID alongside the new Pylon ID. If a script fails halfway, you need this mapping to resume without creating duplicates. Silent failures are the number-one cause of data loss in DIY migrations.
  • Do not mutate source data. Treat Missive as read-only during the extraction phase. Never delete or modify Missive conversations during migration.
  • Schedule off-peak. Run the Pylon load step during low-traffic hours (nights, weekends) to avoid competing with live agent API calls for rate limit capacity.
  • Validate incrementally. Don't wait until all 50K issues are loaded to check data quality. Validate after every 1,000 records. Catch transformation bugs early.
  • Back up everything. Export Missive's full data dump (Settings → Login & Security) as your safety net before starting any extraction.
  • Pre-validate payloads. Validate every /import/issues payload against Pylon's expected schema before submission. Null body_html, missing contact_id, and invalid user_id values will cause rejections. It's cheaper to catch these in your transformation layer than to debug them during the rate-limited load phase.

Frequently Asked Questions

Can I migrate from Missive to Pylon using CSV export?
Only partially. Missive's CSV export covers contacts and conversation metadata, but not threaded message history or attachments. There's no CSV import in Pylon either — you still need the REST API to load data. For anything beyond contacts, use the API-based pipeline approach.
How long does a Missive to Pylon migration take?
It depends on volume. Pylon's Issues API is rate-limited to 10 requests per minute. Loading 10,000 issues takes approximately 17 hours of continuous writes. A full migration including planning, testing, and validation typically takes 2–6 weeks for datasets above 5,000 conversations.
How do I map Missive contacts to Pylon Accounts?
Missive has no Account object — contacts live in flat contact books. You must derive Account associations by parsing email domains, filtering out free email providers (Gmail, Yahoo, Outlook), and optionally cross-referencing against a CRM. Contacts with free email domains require manual grouping or a catch-all Account.
Does Pylon have a bulk import endpoint for historical tickets?
Yes. Pylon provides a dedicated /import/issues endpoint that accepts full message threads with timestamps, internal notes, and contact references in a single payload. This is the recommended path for backfilling historical closed tickets while preserving original dates.
What Missive data cannot be migrated to Pylon?
Rule configurations (automation logic), analytics history, integration states, live chat widget settings, unsent drafts, collaborative drafts, and private conversations don't transfer. Missive's multi-assignee model and nested labels also require transformation since Pylon uses single assignees and flat tags.

More from our Blog

Pylon to Missive Migration: The Technical Guide
Migration Guide/Pylon/Missive

Pylon to Missive Migration: The Technical Guide

A technical guide to migrating from Pylon to Missive covering data model mapping, API rate limits, migration approaches, edge cases, and step-by-step implementation.

Nachi Nachi · · 25 min read
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 · · 11 min read