---
title: "Pylon to Missive Migration: The Technical Guide"
slug: pylon-to-missive-migration-the-technical-guide
date: 2026-07-27
author: Nachi
categories: [Migration Guide, Pylon, Missive]
excerpt: "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."
tldr: Migrating from Pylon to Missive means translating a Slack-first ticketing model into a conversation-centric inbox. There is no native import — plan for a custom API pipeline with strict rate limits on both sides.
canonical: https://clonepartner.com/blog/pylon-to-missive-migration-the-technical-guide/
---

# Pylon to Missive Migration: The Technical Guide


# Pylon to Missive Migration: The Technical Guide

> [!NOTE]
> **TL;DR — Pylon to Missive Migration**
>
> Migrating from Pylon to Missive means translating a Slack-first, account-centric ticketing model into a conversation-centric collaborative inbox. There is no native import path. Contacts can move via CSV, but historical issue threads require a custom API pipeline using Pylon's REST API and Missive's Posts or custom channel endpoints. Plan for strict rate limits on both sides, a 400-message-per-conversation hard limit in Missive, and the loss of custom objects, knowledge base, and SLA management. End-to-end timelines range from 1–2 days for contact-only migrations to 3–8 weeks for full historical migrations with attachments, depending on volume and complexity.

Migrating from Pylon to Missive is a data-model translation problem, not a settings import. You are moving from a platform built for B2B Slack-first support — where the core object is the **Issue** tied to an **Account** — to a collaborative team inbox where everything revolves around **Conversations**, shared labels, and contact books. There is no native migration path between the two platforms. Every approach requires extraction from Pylon's REST API, transformation of the data model, and loading into Missive via its API or CSV import.

This guide covers the exact object mapping, API constraints on both sides (including response schemas and common error codes), migration approaches with trade-offs, and edge cases that will break your migration if you don't plan for them.

For teams evaluating a move to a traditional helpdesk instead, see [How to Migrate from Pylon to Zendesk: The 2026 Technical Guide](https://clonepartner.com/blog/blog/how-to-migrate-from-pylon-to-zendesk-the-2026-technical-guide/). If you're validating your Missive setup before go-live, the [Missive Migration Checklist](https://clonepartner.com/blog/blog/missive-migration-checklist/) covers the full pre- and post-migration audit.

## Why Teams Move from Pylon to Missive

**Pylon** is an AI-native support platform designed for B2B companies that interact with customers primarily through shared Slack Connect and Microsoft Teams channels. Its core objects are **Issues**, **Accounts**, and **Contacts** — every issue is associated with a contact, and every contact belongs to an account. Pylon also supports subaccounts, portal roles, tasks/projects, CRM sync, and account-level activity. ([support.usepylon.com](https://support.usepylon.com/articles/4220063775-platform-orientation-pylon-101?utm_source=openai))

**Missive** is a collaborative team inbox that unifies email, SMS, WhatsApp, and social media into a single view. Its core object is the **Conversation** — a message thread that can span multiple channels, with shared labels, assignments, and collaborative drafting. Missive's REST API is currently unversioned — there is no `/v2/` path — so monitor their changelog for breaking changes before and during migration. ([missiveapp.com](https://missiveapp.com/docs/?utm_source=openai))

Teams migrate for these reasons:

- **Channel consolidation.** Pylon is optimized for Slack-first B2B support. When a team needs a unified inbox across email, SMS, WhatsApp, Instagram, and live chat as equal channels, Missive's multi-channel architecture is a better fit.
- **Collaborative email workflows.** Missive's standout feature is real-time collaborative drafting — multiple team members can co-write a single email before sending. Pylon doesn't offer this.
- **Simpler operational model.** Teams moving from complex B2B account-based support to a leaner shared inbox model — common for agencies, professional services firms, and small-to-mid-size operations — find Missive's label-and-assignment workflow faster to adopt.
- **Pricing.** Missive's per-user pricing (starting at $14/user/month for Starter, $18 for Productive, $26 for Business) is lower than Pylon's enterprise-oriented model for teams that don't need Slack-native support infrastructure.

The primary technical challenge is flattening Pylon's B2B Account model into Missive's Organization and Contact architecture, while converting Slack-formatted Issues into Missive Conversations.

> [!NOTE]
> **Key constraint:** Missive does not have a dedicated ticket import API or bulk data import endpoint for historical conversations. You can import contacts via CSV or API, but conversation history must be injected via the **Posts** endpoint (which creates a notification-style card in a conversation, containing HTML body text, a title, and optional shared label assignments) or loaded via **custom channels**. This is the single biggest architectural constraint of this migration.

## Core Data Model Differences: Pylon vs Missive

Before writing any migration code, understand where the two data models diverge. Pylon is a structured ticketing system with a rich relational model. Missive is a communication-first platform — it stores conversations, not tickets.

| Pylon Object | Missive Equivalent | Notes |
|---|---|---|
| **Issue** | **Conversation** (via Posts / custom channels) | Pylon Issues have lifecycle states (open, snoozed, closed). Missive Conversations have inbox states (inbox, archived, closed, trashed). No 1:1 parity. |
| **Account** | **Organization** (Contact Book) | Pylon Accounts are first-class objects with domains, owners, custom fields, and channels. Missive Organizations are lightweight groupings within contact books. |
| **Contact** | **Contact** | Closest 1:1 mapping. Missive contacts live in Contact Books and support name, email, phone, company, groups, notes, and custom fields. |
| **Messages** (replies + notes) | **Posts** / **Comments** | Customer replies → Posts in a conversation. Internal notes → Comments (Missive's internal annotation layer). |
| **Tags** | **Shared Labels** | Pylon Tags are flat key-value pairs on Issues. Missive Shared Labels can be applied to conversations and trigger rules. |
| **Custom Fields** | **Contact Notes / Custom Fields** | Pylon supports custom fields on Accounts, Issues, and Contacts. Missive custom fields exist on contacts but are not available on conversations natively. |
| **Custom Objects** | **No equivalent** | Missive has no custom object support. Data must be archived externally or flattened into contact notes. |
| **Knowledge Base** | **No native KB** | Missive is an inbox, not a helpdesk. KB articles must be migrated to Notion, GitBook, Confluence, or similar. |
| **Tasks & Projects** | **Tasks** (comment-based) | Missive tasks are lightweight — comments with a checkbox. Pylon's tasks have assignees, due dates, and account associations. |
| **Macros** | **Canned Responses** | Must be recreated manually. No import mechanism. |
| **Ticket Forms** | **No equivalent** | Missive does not have structured intake forms. |
| **Opportunities** | **No equivalent** | Missive has no native CRM pipeline. Keep these in your CRM and surface via integrations. ([missiveapp.com](https://missiveapp.com/docs/advanced-features/integrations?utm_source=openai)) |

> [!WARNING]
> **Data structure compromise:** Missive is not a helpdesk — it's a collaborative inbox. Migrating from Pylon means accepting a simpler operational model. If you need structured ticketing, SLAs, custom objects, or account portal support, evaluate whether Missive is the right target before investing in migration.

## Migration Approaches

There are five viable methods. None offer a push-button experience — this is a custom integration project regardless of approach.

### 1. CSV-Based Import (Contacts Only)

**How it works:** Export contacts from Pylon via the API or CSV export, reformat the CSV into Gmail or Outlook format, and import into a Missive Contact Book. ([missiveapp.com](https://missiveapp.com/docs/core-features/contacts/importing?utm_source=openai))

**When to use it:** You only need to migrate your contact database — not conversation history.

**Pros:** Fast, no code required. No limit on the number of contacts in Missive.

**Cons:** Only works for contacts. No conversation history, no issue data, no attachments. Missive's CSV import only accepts Gmail and Outlook CSV formats — any other format requires manual column mapping.

**Typical duration:** 1–2 days including validation.

**Complexity:** Low

### 2. API-Based Migration (Pylon API → Missive API)

**How it works:** Build a custom ETL pipeline that extracts data from Pylon's REST API and loads it into Missive via the Missive REST API. Historical issue threads can be loaded using the **Posts** endpoint or via **custom channels** using `POST /v1/messages`, which supports `delivered_at` timestamps and `external_id` references.

**When to use it:** You need to migrate contacts, conversation history, labels, and assignments.

**Pros:** Maximum data fidelity. Preserves conversation history as readable posts or custom channel messages. Handles contacts, organizations, and labels.

**Cons:** Requires engineering effort. Both APIs have rate limits that constrain throughput. Missive's Posts endpoint creates read-only injected content — not native email threads. Custom channel messages support `delivered_at` for timestamps, but Missive's rules engine **does not support custom channels**. ([missiveapp.com](https://missiveapp.com/docs/developers/custom-channels?utm_source=openai))

**Typical duration:** 3–6 weeks of engineering effort for 1,000–10,000 issues.

**Complexity:** High

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

**How it works:** Use a no-code/low-code automation platform to connect Pylon triggers to Missive actions. ([missiveapp.com](https://missiveapp.com/docs/advanced-features/integrations?utm_source=openai))

**When to use it:** Ongoing sync of new data (new contacts, new conversations) — not historical migration.

**Pros:** No custom code. Good for forward-looking sync.

**Cons:** Not designed for bulk historical migration. Zapier and Make have execution limits, task quotas, and timeout constraints that make migrating thousands of records impractical.

**Typical duration:** 1–2 weeks for setup and testing.

**Complexity:** Low–Medium

### 4. Custom ETL Pipeline

**How it works:** Land Pylon data in a staging database or warehouse, normalize it, build ID crosswalks, generate target payloads, then load Missive in deterministic batches. Pylon's warehouse sync can write customer issue data to Snowflake, BigQuery, Redshift, or S3, which is often better than paginating through API endpoints for large migrations. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/data-warehouse?utm_source=openai))

**When to use it:** Enterprise datasets requiring replayability, auditability, delta runs, or parallel cutover testing.

**Pros:** Strongest observability, rollback control, and fault tolerance with built-in retry logic.

**Cons:** Highest infrastructure and build cost. Cloud infrastructure costs (staging DB, compute for ETL workers) typically add $50–200/month during active migration.

**Typical duration:** 4–8 weeks of engineering effort.

**Complexity:** High

### 5. Managed Migration Service

**How it works:** A specialist migration team handles the full pipeline — extraction, transformation, loading, validation, and cutover. Missive itself points teams to its partners directory for custom integration help, which signals that many migrations need bespoke work rather than a turnkey importer. ([missiveapp.com](https://missiveapp.com/docs/support?utm_source=openai))

**When to use it:** You have >5,000 issues, complex custom field mappings, attachments, multi-level relationships, or zero engineering bandwidth.

**Pros:** Fastest time-to-completion. Handles edge cases, deduplication, and validation. No engineering distraction.

**Cons:** External cost. Requires sharing API credentials with a third party.

**Typical duration:** 2–4 weeks depending on volume and complexity.

**Complexity:** Low (for your team)

### Approach Comparison

| Criteria | CSV Import | API Pipeline | Middleware | Custom ETL | Managed Service |
|---|---|---|---|---|---|
| Contacts | ✅ | ✅ | ✅ | ✅ | ✅ |
| Conversation History | ❌ | ✅ (as Posts / custom channel) | ⚠️ Forward-only | ✅ | ✅ |
| Attachments | ❌ | ⚠️ Manual handling | ❌ | ✅ | ✅ |
| Custom Fields | ❌ | ⚠️ Partial (contacts only) | ❌ | ✅ | ✅ |
| Labels / Tags | ❌ | ✅ | ⚠️ Limited | ✅ | ✅ |
| Engineering Effort | None | 3–6 weeks | 1–2 weeks | 4–8 weeks | None |
| Best For | Contact-only | Full migration with dev team | Forward sync | Audit-heavy enterprise | Complex or large migrations |

### Scenario Recommendations

- **Small team (<1,000 issues, simple fields):** CSV import for contacts. Accept that conversation history stays in Pylon or an export archive. Start fresh in Missive.
- **Mid-size team (1,000–10,000 issues, some custom fields):** API-based pipeline if you have a developer available for 3–4 weeks. Otherwise, managed service.
- **Enterprise (>10,000 issues, custom objects, attachments, compliance requirements):** Managed migration service. The rate-limit math alone makes DIY impractical.
- **Ongoing sync (Pylon stays active alongside Missive):** Middleware (Make or n8n) for contact and conversation forwarding during a transition period.

## Pre-Migration Planning

### Data Audit Checklist

Before extracting anything, inventory what exists in Pylon and what needs to move:

- [ ] **Accounts:** Count, custom fields, domains, owner assignments, subaccounts
- [ ] **Contacts:** Count, email addresses, phone numbers, account associations, duplicates
- [ ] **Issues:** Count by status (open, snoozed, closed), date range, message count per issue (flag any with >400 messages — these will hit Missive's conversation limit)
- [ ] **Custom Fields:** List all fields on accounts, contacts, and issues — note data types (text, dropdown, date, number)
- [ ] **Custom Objects:** List all custom objects. These have **no equivalent in Missive** — plan an archival strategy.
- [ ] **Tags:** Full tag list with usage counts
- [ ] **Knowledge Base:** Article count, collections, languages
- [ ] **Macros:** List all macros to recreate as Missive Canned Responses
- [ ] **Attachments:** Estimate total file size. Missive's API payload limit is **10 MB per request**. ([missiveapp.com](https://missiveapp.com/docs/developers/rest-api/endpoints?utm_source=openai))
- [ ] **Active automations:** Pylon Runbooks, triggers, and SLA rules to rebuild as Missive Rules
- [ ] **CRM-derived data:** If agents rely on CRM fields surfaced in Pylon, audit the CRM source, not just Pylon. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/integrations/crm?utm_source=openai))
- [ ] **Slack Connect channels:** Inventory all active Slack Connect channels managed by Pylon — these must be decommissioned or redirected post-migration.

### Define Migration Scope

Not everything should move. Pylon is a richer system than Missive for structured support operations.

**Migrate:** Contacts, active and recent issues (as conversation history), tags → labels, account → organization mappings.

**Archive externally:** Custom objects, knowledge base articles (→ Notion, GitBook, or similar), detailed SLA/escalation data, audit logs, opportunities.

**Drop:** Unused custom fields, test data, spam issues, orphaned contacts with no associated issues.

### Migration Strategy

- **Big bang:** Extract everything from Pylon, load into Missive over a weekend, cut over on Monday. Works for teams with <5,000 issues. Typical end-to-end window: 48–72 hours including validation.
- **Phased:** Migrate contacts and active issues first, then historical data in batches. Reduces risk. Standard for helpdesk transitions — migrate historical data over a weekend, switch MX records and Slack integrations to Missive on Sunday night, then run a final delta sync. Typical timeline: 1–3 weeks.
- **Incremental:** Keep Pylon running during migration, sync new data forward to Missive via webhooks, do a final delta sync at cutover. Longest timeline (2–4 weeks) but lowest risk.

For cutover preparation, pair this guide with the [Missive Migration Checklist](https://clonepartner.com/blog/blog/missive-migration-checklist/) and the [Post-Migration QA Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

## Migration Architecture

### Data Flow

```
Pylon REST API → Extract (JSON) → Transform (map fields, flatten objects) → Load → Missive REST API
     ↓                                        ↓                                    ↓
  Accounts                            Normalize to Missive                   Contact Books
  Contacts                            Contact/Org schema                     Contacts
  Issues + Messages                   Convert to Post/message payloads       Conversations
  Tags                                Map to Label IDs                       Shared Labels
  Custom Fields                       Flatten to notes/fields               Contact notes
```

### Rate Limit Math

This is where migrations stall. Both platforms enforce strict rate limits.

**Pylon extraction limits:**
- Accounts: 60 requests/minute
- Contacts: 60 requests/minute
- Issues (`GET /issues`): **10 requests/minute** — this is the bottleneck
- Issues (`POST /issues/search`): 20 requests/minute ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/api-reference/accounts?utm_source=openai))
- Attachments: 10 requests/minute

**Missive loading limits:**
- 5 concurrent requests maximum
- 300 requests/minute
- 900 requests per 15 minutes
- Conversations paginate at max 50 per page

([missiveapp.com](https://missiveapp.com/docs/developers/rest-api/rate-limits?utm_source=openai))

**Practical throughput calculation:** If you need to extract 10,000 issues, the Pylon Issues endpoint at 10 req/min means approximately **17 hours** of extraction time (assuming one page per request with pagination). On the Missive side, the 900 requests per 15-minute ceiling gives a long-run average of about **3,600 requests per hour**. A single Pylon Issue with 50 replies requires 51+ API calls on the Missive side — one to create the conversation and one per message. That means 10,000 issues averaging 10 messages each would require ~110,000 Missive API calls, consuming roughly **30 hours** of loading time at sustained throughput. Plan your cutover window accordingly.

Neither platform charges per-API-call fees, but Missive requires the **Productive plan ($18/user/month) or higher** to generate API tokens.

> [!WARNING]
> **Pylon Issues API constraint:** The `GET /issues` endpoint requires a time range filter with a **maximum window of 30 days**. To extract all historical issues, you must iterate through 30-day windows. Factor this into your extraction script.

> [!WARNING]
> **Missive conversation size limit:** Missive enforces a **400-message hard limit per conversation**. Very long Pylon issues must be split, truncated, or archived outside Missive. ([missiveapp.com](https://missiveapp.com/docs/core-features/conversations/merging?utm_source=openai))

### Authentication

**Pylon:** Bearer token authentication. Only Admin users can create API tokens. Tokens are organization-scoped — a single token covers all accounts and issues in the Pylon org. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/authentication?utm_source=openai))

**Missive:** Bearer token via the Authorization header. API tokens are **personal** — there is no organization-level token. This means: (1) the token inherits the permissions of the user who created it, (2) records created via the API will show as authored by that user, and (3) if that user is deactivated, the token stops working. Use a dedicated service account user for migration. You must be on the **Productive plan or higher** to generate API tokens. ([missiveapp.com](https://missiveapp.com/docs/developers/rest-api/?utm_source=openai))

## Field-Level Data Mapping

### Contacts

| Pylon Field | Missive Field | Transform |
|---|---|---|
| `name` | `first_name` + `last_name` | Split on first space |
| `email` | `email_addresses [0].address` | Direct map |
| `phone` | `phone_numbers [0].number` | Direct map |
| `account.name` | `organization` | Map to Contact Book Organization |
| `custom_fields []` | `notes` or custom fields | Flatten to text if no matching field type |
| `tags []` | `groups []` | Map to Contact Groups |
| `created_at` | N/A | Missive does not preserve original creation timestamps on contacts |

### Issues → Conversations

| Pylon Field | Missive Equivalent | Method |
|---|---|---|
| `issue.title` | Post `notification.title` or message `subject` | Set via Posts or custom channel endpoint |
| `issue.state` | Conversation state | Map: open → inbox, closed → closed, snoozed → snoozed |
| `issue.assignee` | `add_assignees` | Map Pylon user ID → Missive user ID |
| `issue.tags []` | `add_shared_labels` | Pre-create labels, map by name → ID |
| `issue.account_id` | `organization` on conversation | Link via organization field on post |
| `issue.custom_fields []` | Post body or comment | No native custom fields on conversations |
| `messages [].body_html` | Post `text` (HTML) | Direct map; convert Slack markdown first |
| `messages [].attachments` | Post attachments | Reupload; 10 MB payload limit |
| `messages [].created_at` | `notification.date` or `delivered_at` | Unix timestamp |
| `messages [].is_internal` | Comment type | Internal notes → Missive Comments |

### Tags → Labels

| Pylon Tag | Missive Label Type | Setup |
|---|---|---|
| Issue-level tag | Organization Shared Label | Pre-create in Missive Settings → Labels |
| Contact-level tag | Contact Group | Create via API or manually |

## Step-by-Step Migration Process

### Step 1: Extract Data from Pylon

Use cursor-based pagination and iterate through 30-day time windows for issues.

```python
import requests
import time
import json
from datetime import datetime, timedelta

PYLON_TOKEN = "your_pylon_api_token"
BASE_URL = "https://api.usepylon.com"
HEADERS = {"Authorization": f"Bearer {PYLON_TOKEN}"}

def extract_accounts():
    accounts = []
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(f"{BASE_URL}/accounts", headers=HEADERS, params=params)
        resp.raise_for_status()
        data = resp.json()
        accounts.extend(data["data"])
        if not data["pagination"].get("has_next_page"):
            break
        cursor = data["pagination"]["cursor"]
        time.sleep(1)  # Respect 60 req/min
    return accounts

def extract_issues_by_window(start_date, end_date):
    """Extract issues in 30-day windows (Pylon API constraint)."""
    issues = []
    current = start_date
    while current < end_date:
        window_end = min(current + timedelta(days=30), end_date)
        cursor = None
        while True:
            params = {
                "from": current.isoformat(),
                "to": window_end.isoformat(),
                "limit": 50
            }
            if cursor:
                params["cursor"] = cursor
            resp = requests.get(f"{BASE_URL}/issues", headers=HEADERS, params=params)
            resp.raise_for_status()
            data = resp.json()
            issues.extend(data["data"])
            if not data["pagination"].get("has_next_page"):
                break
            cursor = data["pagination"]["cursor"]
            time.sleep(6)  # 10 req/min = 1 request per 6 seconds
        current = window_end
    return issues
```

**Pylon webhook events for delta sync:** If you're running an incremental migration, Pylon supports webhooks for `issue.created`, `issue.updated`, `issue.closed`, `contact.created`, and `contact.updated`. Configure these in Pylon Settings → Webhooks. The payload includes the full object body, so you can skip the extraction API call for new records during the transition period. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/webhooks?utm_source=openai))

### Step 2: Transform Data

```python
def transform_contact(pylon_contact):
    """Map Pylon contact to Missive contact schema."""
    name_parts = (pylon_contact.get("name") or "").split(" ", 1)
    return {
        "first_name": name_parts[0] if name_parts else "",
        "last_name": name_parts[1] if len(name_parts) > 1 else "",
        "email_addresses": [{"address": pylon_contact.get("email"), "label": "work"}] if pylon_contact.get("email") else [],
        "phone_numbers": [{"number": pylon_contact.get("phone"), "label": "work"}] if pylon_contact.get("phone") else [],
        "notes": json.dumps(pylon_contact.get("custom_fields", {})),
        "organization": pylon_contact.get("account", {}).get("name", "")
    }

def transform_issue_to_post(pylon_issue, messages, label_map):
    """Convert a Pylon Issue + messages into Missive Post payload."""
    body_parts = []
    for msg in sorted(messages, key=lambda m: m.get("created_at", "")):
        sender = msg.get("user", {}).get("name") or msg.get("contact", {}).get("name") or "Unknown"
        timestamp = msg.get("created_at", "")
        body_parts.append(
            f"<p><strong>{sender}</strong> ({timestamp}):</p>{msg.get('body_html', '')}<hr/>"
        )
    return {
        "posts": {
            "notification": {
                "title": pylon_issue.get("title", "Migrated Issue"),
                "body": f"Pylon Issue #{pylon_issue.get('issue_number', 'N/A')}"
            },
            "text": "".join(body_parts),
            "add_shared_labels": [
                label_map[tag] for tag in pylon_issue.get("tags", []) if tag in label_map
            ]
        }
    }
```

#### Slack Markdown Conversion

Pylon messages sourced from Slack use a proprietary markup format. Standard markdown parsers will fail on user mentions (`<@U123456>`), channel links (`<#C123456|channel-name>`), and custom emoji syntax (`:custom_emoji:`). Write a custom regex parser:

```python
import re

def convert_slack_to_html(text):
    """Convert Slack-specific markup to standard HTML."""
    # User mentions: <@U123456> → @username (or strip if user map unavailable)
    text = re.sub(r'<@(U[A-Z0-9]+)>', r'@\1', text)
    # Channel links: <#C123456|channel-name> → #channel-name
    text = re.sub(r'<#C[A-Z0-9]+\|([^>]+)>', r'#\1', text)
    # URLs: <https://example.com|display text> → <a href="...">display text</a>
    text = re.sub(r'<(https?://[^|>]+)\|([^>]+)>', r'<a href="\1">\2</a>', text)
    # Bold: *text* → <strong>text</strong>
    text = re.sub(r'\*([^*]+)\*', r'<strong>\1</strong>', text)
    # Italic: _text_ → <em>text</em>
    text = re.sub(r'_([^_]+)_', r'<em>\1</em>', text)
    # Strikethrough: ~text~ → <del>text</del>
    text = re.sub(r'~([^~]+)~', r'<del>\1</del>', text)
    # Code blocks: ```text``` → <pre><code>text</code></pre>
    text = re.sub(r'```([^`]+)```', r'<pre><code>\1</code></pre>', text)
    # Inline code: `text` → <code>text</code>
    text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
    # Custom emoji: :emoji_name: → (strip or replace with text)
    text = re.sub(r':([a-z0-9_+-]+):', r'[\1]', text)
    return text
```

Do not assume any off-the-shelf markdown-to-HTML library will handle Slack markup correctly.

### Step 3: Load into Missive

```python
MISSIVE_TOKEN = "your_missive_api_token"
MISSIVE_URL = "https://public.missiveapp.com/v1"
MISSIVE_HEADERS = {
    "Authorization": f"Bearer {MISSIVE_TOKEN}",
    "Content-Type": "application/json"
}

def create_missive_contact(contact_data, contact_book_id):
    payload = {
        "contacts": [{
            "contact_book": contact_book_id,
            **contact_data
        }]
    }
    resp = requests.post(f"{MISSIVE_URL}/contacts", headers=MISSIVE_HEADERS, json=payload)
    handle_missive_response(resp)
    time.sleep(1)  # 1 req/sec recommended
    return resp.json()

def create_missive_post(post_payload):
    resp = requests.post(f"{MISSIVE_URL}/posts", headers=MISSIVE_HEADERS, json=post_payload)
    handle_missive_response(resp)
    time.sleep(1)
    return resp.json()

def handle_missive_response(resp):
    """Handle common Missive API error codes."""
    if resp.status_code == 429:
        # Rate limited — respect Retry-After header
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return  # Caller should retry
    elif resp.status_code == 422:
        # Validation error — log and skip
        # Common causes: invalid email format, missing required field,
        # invalid contact_book ID, label ID not found
        error_body = resp.json()
        print(f"Validation error: {error_body}")
        raise ValueError(f"Missive 422: {error_body}")
    elif resp.status_code == 401:
        raise PermissionError("Missive 401: Token invalid or user deactivated")
    elif resp.status_code == 404:
        raise LookupError("Missive 404: Resource not found (check contact_book or label IDs)")
    resp.raise_for_status()
```

**Example Missive API response (successful post creation):**
```json
{
  "posts": {
    "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "conversation": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
    "notification": {
      "title": "Migrated Issue",
      "body": "Pylon Issue #1234"
    },
    "created_at": 1719849600
  }
}
```

**Example Missive API response (422 validation error):**
```json
{
  "errors": [
    {
      "message": "email_addresses[0].address is invalid",
      "field": "email_addresses[0].address"
    }
  ]
}
```

Store the returned `id` and `conversation` values in your crosswalk database — you'll need them for subsequent message loads and for validation.

### Step 4: Validate

- Compare record counts: Pylon contacts vs Missive contacts created.
- Spot-check 5–10% of conversations for message completeness.
- Verify label assignments match tag mappings.
- Confirm organization associations are intact.
- Check the dead-letter queue for failed records and resolve them.

### Project Structure

A production migration loader typically breaks down into these modules:

```text
extract/
  pylon_accounts.py
  pylon_contacts.py
  pylon_issues.py
transform/
  map_fields.py
  normalize_html.py        # Includes Slack markdown conversion
  dedupe.py
load/
  missive_contacts.py
  missive_messages.py
  missive_labels.py
validate/
  compare_counts.py
  sample_records.py
state/
  checkpoints.db           # SQLite: last successful cursor per entity
  id_crosswalks.db         # SQLite: pylon_id → missive_id mapping
  dead_letter_queue.db     # Failed records with error details
```

Store **external IDs** on every loaded record, log every request and response, and keep Pylon `request_id` values for API troubleshooting. Use a dead-letter queue for attachment failures, 422 validation errors, and unsupported field values. ([missiveapp.com](https://missiveapp.com/docs/developers/rest-api/rate-limits?utm_source=openai))

## Edge Cases and Challenges

### Duplicate Contacts

Pylon allows multiple contacts with the same email across different accounts. Missive contact books can also hold duplicates — creating a contact with an email that already exists does **not** return an error; it creates a second contact card. Before import, deduplicate by email address and merge contact records. If you skip this, agents will see duplicate contact cards in the Missive sidebar. Deduplication strategy: group by lowercase email, keep the contact with the most recent `updated_at` timestamp, merge custom fields from the others.

### Multi-Level Relationships

Pylon's relational model — Account → Contact → Issue → Messages — doesn't map cleanly to Missive. Missive conversations are not linked to organizations by default; you must pass the `organization` field when creating posts. If you skip this, conversations become orphaned from their company context.

### Account Hierarchies

Pylon subaccounts roll data up to parent accounts. Missive has no parent-child organization concept. Keep the hierarchy in your CRM or flatten it into a custom field (e.g., `"Parent Account: Acme Corp"` in the organization notes). ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/account-intelligence/accounts/subaccounts?utm_source=openai))

### Attachments

Pylon attachments are stored as URLs in messages. During migration:

1. **Download each attachment during extraction.** Do not rely on Pylon CDN URLs persisting after your contract ends. Pylon does not guarantee URL accessibility post-subscription termination.
2. **Re-upload to Missive** via the post or draft attachment mechanism.
3. **Respect Missive's 10 MB total payload limit** per request. If a single attachment exceeds 10 MB, it must be hosted externally and linked.

For issues with large or numerous attachments, store files in cloud storage (S3, Google Drive) and link from the conversation post instead. Common dead-letter queue entries: files >10 MB, files with unsupported MIME types, and expired Pylon CDN URLs.

### Timestamps and Sort Order

Missive's Posts endpoint accepts a `notification.date` field for display timestamps, and the custom channel endpoint accepts `delivered_at`. But these do not necessarily set the conversation's sort order in the inbox the same way native email timestamps do. Historical conversations may not sort chronologically in a user's inbox view. **Test this behavior in your proof of concept before committing to a loading approach.** In testing, Posts with `notification.date` displayed the correct timestamp on the post card but the conversation appeared in the inbox based on the API call time, not the historical date.

### Conversation Size Limits

Missive enforces a **400-message hard limit per conversation**. Pylon Issues with more than 400 messages must be split across multiple Missive conversations (linked by a shared label like `"Long Thread - Issue #1234 - Part 2"`), truncated to the most recent 400 messages, or archived outside Missive as static HTML. ([missiveapp.com](https://missiveapp.com/docs/core-features/conversations/merging?utm_source=openai))

### Custom Channels and the Rules Engine

If you load historical data via Missive's custom channel mechanism, be aware that Missive's rules engine **does not support custom channels**. Any automations you build in Missive Rules will not fire on custom channel conversations. This matters for post-migration routing and labeling — if you plan to auto-label or auto-assign migrated conversations, use the Posts endpoint instead. ([missiveapp.com](https://missiveapp.com/docs/developers/custom-channels?utm_source=openai))

### API Failures and Retries

Both APIs return `429 Too Many Requests` when rate limits are exceeded. Your migration script must:
- Read the `Retry-After` header (Missive returns this; Pylon may not — default to 60 seconds).
- Implement exponential backoff with jitter (e.g., base delay × 2^attempt + random(0–1s)).
- Handle `422` validation errors by logging the full error body and moving the record to a dead-letter queue — do not retry these, as they will fail again with the same payload.
- Handle `401` by halting the migration — the token has been invalidated or the user deactivated.
- Checkpoint every successful write to a local mapping store (SQLite is sufficient) so you can resume from the last successful record — not restart from zero.

## Limitations and Constraints

Be explicit with your stakeholders about what Missive cannot replicate from Pylon:

| Pylon Capability | Missive Status |
|---|---|
| Custom Objects | ❌ Not supported |
| Knowledge Base | ❌ Not native — use external tool |
| Ticket Forms | ❌ Not supported |
| SLA Management | ❌ No native SLA tracking |
| AI Agent / Runbooks | ⚠️ Missive has AI features (OpenAI-powered drafting, summarization) but no equivalent to Pylon's Runbook automation engine |
| Account Health Scores | ❌ Not supported |
| Issue Lifecycle States | ⚠️ Simplified to inbox / closed / snoozed / trash |
| Custom Fields on Conversations | ❌ Only on contacts |
| Bulk Import API | ❌ No dedicated import endpoint |
| Feature Requests Tracking | ❌ Not supported |
| Customer Portal | ❌ Not supported |
| Subaccount Rollups | ❌ Not supported |
| Slack Reactions | ⚠️ Lost unless explicitly appended as text (e.g., " [👍 x3]") |

If your support team relies on Pylon's customer portal roles, subaccount rollups, or CRM-linked account intelligence, expect functional redesign work beyond the data migration. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/customer-portal/access-control?utm_source=openai))

## Validation and Testing

### Record Count Comparison

| Object | Pylon Count | Missive Count | Delta |
|---|---|---|---|
| Contacts | Extract from API | Query Missive API | Should be 0 (after dedup) |
| Conversations (Issues) | Extract count | Count Posts created | Document any intentionally skipped |
| Labels (Tags) | Tag list | Shared Label list | Must be 1:1 |
| Organizations (Accounts) | Account count | Org count in Contact Book | May differ if accounts were merged |

Missive's own export options — contacts as CSV, comments as CSV, and conversations as JSON — make post-load verification easier than manual clicking. Use these exports as your target-side evidence pack for UAT and sign-off. ([missiveapp.com](https://missiveapp.com/docs/administration/security/faq?utm_source=openai))

### Field-Level Validation

For a random sample of 50–100 records:
- Verify contact name, email, and phone match the source.
- Verify conversation body HTML renders correctly in Missive (check for broken Slack markup artifacts).
- Verify label assignments match original tags.
- Verify organization links are intact.
- Verify internal notes appear as Comments, not visible to customers.
- Verify attachment links are accessible and not returning 404s.

### UAT Process

1. Migrate a small batch (100 contacts, 50 issues) to a **test Missive organization** (not your production org).
2. Have 2–3 agents use the test environment for one day.
3. Collect feedback on conversation readability, contact accuracy, and missing data.
4. Fix mapping issues before the full migration.
5. Run a second test batch with edge cases: issues with >400 messages, issues with large attachments, contacts with special characters in names.

### Rollback Plan

Missive does not have a bulk delete API. If the migration goes wrong:
- Contacts can be deleted by deleting the contact book (this permanently deletes all contacts in it — use a **dedicated migration contact book**, separate from any pre-existing contact data).
- Conversations injected via Posts cannot be easily bulk-removed. They can be trashed individually via the API, but there is no batch endpoint.
- The safest rollback strategy is to migrate into a **separate Missive organization** first, validate thoroughly, then merge — rather than importing directly into your production organization.

Plan rollback before go-live. If you cannot reverse the last delta load cleanly, you are not ready for cutover.

## Post-Migration Tasks

### Rebuild Automations

Pylon Runbooks, triggers, and SLA rules do not transfer. In Missive, recreate critical workflows using **Rules**:
- Auto-assign conversations based on sender domain or label.
- Auto-apply labels based on keywords or contact group membership.
- Set up webhook triggers for external system notifications.

Note: if you loaded history via custom channels, Missive Rules will not fire on those conversations.

### Recreate Canned Responses

Pylon Macros must be manually recreated as Missive Canned Responses. Export macro text from Pylon, then create each response in Missive under Settings → Responses.

### Connect Inboxes and Channels

Route your support email addresses (e.g., support@company.com) to Missive. Set up any SMS, WhatsApp, or social media channels. If you need CRM context in the sidebar, configure the Salesforce, HubSpot, or Pipedrive integrations.

### Decommission Pylon

Post-migration, you need a clean shutdown sequence:

1. **Keep Pylon active for 30 days minimum** after cutover to handle any in-flight issues and verify no data was missed.
2. **Disconnect Slack Connect channels** from Pylon — in Pylon Settings → Integrations → Slack, remove each workspace connection. The Slack Connect channels themselves remain; only Pylon's monitoring stops.
3. **Redirect any Pylon-managed email forwarding** to Missive inboxes.
4. **Export a final data backup** from Pylon (full JSON dump via API) before contract termination. Once your Pylon subscription ends, API access and CDN-hosted attachments may become inaccessible.
5. **Cancel the Pylon subscription** only after confirming: all attachment files have been downloaded, the final delta sync is complete, and agents have validated data in Missive for at least 2 weeks.

### User Training

Pylon and Missive have fundamentally different UX paradigms. Train your team on:
- Shared label workflows vs ticket queues.
- Collaborative drafting (Missive's core differentiator).
- Contact book management and organization groupings.
- Rule-based automation instead of SLA-driven escalation.
- The difference between replying to a customer and leaving an internal comment.
- Unassigned work in team inboxes vs assigned work in personal Inbox and Tasks.

### Monitor for Data Inconsistencies

For two weeks post-migration:
- Spot-check 10 random conversations daily for missing messages or broken formatting.
- Monitor for duplicate contacts created by email syncing alongside the imported contact book.
- Verify that Missive Rules are firing correctly on new conversations.
- Check that attachment links in migrated conversations still resolve (especially if stored externally).

## Best Practices

1. **Backup everything before migration.** Export Pylon data to a local store (JSON files, SQLite database) before making any changes. This is your rollback source of truth.
2. **Run a test migration first.** Migrate 100 records into a test Missive organization. Validate the data model before scaling.
3. **Use idempotent external IDs.** When creating records via the Missive API, use deterministic references (e.g., `pylon_issue_12345`) so re-runs don't create duplicates. Missive's custom channel endpoint supports `external_id` for this purpose.
4. **Checkpoint every write.** Store the Missive ID for every created record mapped to its Pylon source ID. If the script fails at record 5,000 of 10,000, resume from 5,001.
5. **Download attachments during extraction.** Do not depend on Pylon CDN URLs remaining accessible after your subscription ends.
6. **Pre-create labels before loading conversations.** The Posts endpoint requires label IDs — create all Shared Labels in Missive first and build a name → ID mapping.
7. **Migrate contacts before conversations.** Missive matches contacts by email address in the sidebar. If contacts exist before conversations are imported, agents get immediate context.
8. **Plan for the delta.** Issues created in Pylon between extraction and cutover need a final sync pass. Use Pylon webhooks (`issue.created`, `issue.updated`) or a polled re-extraction of the last 24 hours.
9. **Do not flatten CRM objects into Missive** unless the business has explicitly agreed to the behavioral loss. Keep opportunities, pipelines, and custom objects in the CRM and surface them via Missive integrations.
10. **Use a dedicated service account for the Missive API token.** Since tokens are personal and inherit user permissions, a shared migration user prevents token invalidation if an individual leaves the team mid-migration.

## When to Use a Managed Migration Service

DIY makes sense when you have a small dataset, simple field mappings, and an engineer who can dedicate 3–4 weeks. It stops making sense when:

- **Volume exceeds 5,000 issues.** The rate-limit math means multi-day extraction windows and complex retry logic.
- **You have attachments on >20% of issues.** Attachment migration requires downloading, re-uploading, and handling size limits per request — a pipeline that's tedious to build and debug.
- **Custom objects or multi-level relationships exist.** Flattening Pylon's relational model into Missive's simpler schema requires judgment calls that benefit from migration experience.
- **Your team can't absorb the engineering distraction.** A senior engineer spending 4 weeks on migration scripts is 4 weeks not spent on product work.
- **Compliance or audit requirements demand a chain-of-custody record.** Managed services provide migration logs, validation reports, and sign-off documentation.

The hidden cost is not the loader. It is the **crosswalk logic**, exception handling, and cutover support.

At ClonePartner, we've handled 1,500+ data migrations including complex helpdesk transitions with custom objects, multi-level relationships, and attachment-heavy datasets. We handle the Slack markdown-to-HTML conversion, attachment hosting and re-uploading, strict API concurrency limits, and zero-downtime cutovers — without pulling your engineers off product roadmap work.

> Migrating from Pylon to Missive involves real data-model translation — not a settings export. If you want a guaranteed timeline, zero data loss, and no engineering distraction, book a 30-minute scoping call with our team.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Is there a native Pylon to Missive importer?

No. Missive documents CSV import for contacts and a support-assisted migration path from Front, but there is no documented native Pylon import flow. Contacts can move via CSV, but historical conversations require extraction from Pylon's REST API and injection into Missive via the Posts endpoint or custom channels.

### What are the API rate limits for Pylon and Missive during migration?

Pylon's Issues endpoint is limited to 10 requests per minute (the main bottleneck), while Accounts and Contacts allow 60 req/min. Missive allows 300 requests per minute with a max of 5 concurrent requests and a 900-request cap per 15 minutes. Plan for multi-day extraction windows for large datasets.

### Does Missive support custom objects like Pylon?

No. Missive has no custom object support. Data stored in Pylon Custom Objects must be either archived externally (to Airtable, Google Sheets, or your CRM), flattened into contact notes, or accepted as data that will not transfer.

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

Yes, partially. Missive's custom channel message endpoint accepts a delivered_at field, and the Posts endpoint accepts notification.date. However, these may not control conversation sort order in the inbox the same way native email timestamps do. Test this behavior in a proof of concept.

### What is the biggest hard limit to plan around in Missive?

Conversation size. Missive enforces a 400-message hard limit per conversation. Long Pylon issues must be split across multiple Missive conversations, truncated, or archived outside Missive.
