Skip to content

Missive to Unthread Migration: A Technical Guide

A technical guide to migrating from Missive to Unthread — covering data model mapping, API constraints, transformation challenges, and the edge cases that break migrations.

Raajshekhar Rajan Raajshekhar Rajan · · 20 min read
Missive to Unthread Migration: A 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 Unthread Migration: A Technical Guide

Info

TL;DR — Missive to Unthread Migration

Migrating from Missive to Unthread means translating a channel-agnostic collaborative inbox into a Slack-native ticketing platform. No native migration path exists. Missive provides a built-in data export and a REST API (5 concurrent requests, 300 req/min, 900 req/15-min rolling window). Unthread provides REST APIs for accounts, customers, conversations, tags, and ticket types. The hard parts are HTML-to-Slack-markdown conversion, mapping flat contacts into B2B accounts, preserving internal comments as private notes, handling multi-channel threads, and the fact that Unthread does not support backdating createdAt timestamps on imported conversations. Expect 1–3 days for contacts-only moves and 2–6 weeks for full historical migrations with attachments.

If you are searching for how to migrate from Missive to Unthread, the short answer is: yes, but only with a custom ETL pipeline and a target-state decision up front. Decide first whether Unthread will become your email support system, your Slack support system, or a hybrid. That choice determines how you map Missive conversations, comments, labels, assignees, and contacts into Unthread projects, channels, accounts, and tickets. (docs.unthread.io)

This is not a platform swap — it is a data-model translation. Missive treats every interaction as a shared, channel-agnostic conversation where email, SMS, WhatsApp, and live chat coexist in one thread with layered internal comments. Unthread treats every interaction as a trackable ticket originating from a specific channel type — Slack, email, or internal triage — with AI classification, SLA enforcement, and customer-linked context. (missiveapp.com)

No import wizard exists between the two platforms. No Zapier template connects their data models at the depth required. Every viable approach requires extracting data from Missive, restructuring the object hierarchy, and loading into Unthread via its REST API.

This guide covers the exact object mapping, API constraints on both sides, transformation challenges, migration execution, and the edge cases that break imports if you do not plan for them.

For a broader comparison of these platforms, see Unthread vs Missive (2026): Architecture, TCO & Migration Guide. For source-side audit prep, see Missive Migration Checklist. If you are evaluating other Slack-native alternatives, our Missive to Pylon Migration: The CTO's Technical Guide covers a similar data-model translation.

Why Teams Move from Missive to Unthread

The typical trigger is organizational, not feature-driven. Missive excels as a collaborative inbox for email-first teams that need internal chat layered on top of external conversations. Unthread excels as a Slack-native ticketing system for B2B support teams that already live in Slack.

Common migration drivers:

  • Slack-first workflow shift: The team manages most communication in Slack and wants ticketing to happen where work happens, not in a separate app.
  • SLA enforcement: Unthread has native SLA policies with configurable response and resolution targets per priority level. Missive lacks native SLA tracking.
  • AI ticket automation: Unthread's AI can auto-classify, route, and resolve tickets from connected knowledge bases. Missive offers BYO-key AI for drafting and summarization, but not autonomous ticket resolution.
  • Structured ticketing: Unthread supports ticket types with custom fields, conditional field logic, and approval workflows — structured data that Missive's label-based system cannot replicate.

Data Model Mapping: Missive → Unthread

The core translation challenge is converting Missive's flat, channel-agnostic conversation model into Unthread's typed, customer-linked ticket model.

Missive Object Unthread Equivalent Notes
Conversation Conversation Unthread requires a type field: slack, email, or triage. Missive conversations are channel-agnostic.
Message (email, SMS, chat) Message Unthread messages use Slack markdown or HTML body. Missive messages can be HTML-rich emails, SMS, or WhatsApp.
Internal Comment Private note (isPrivateNote: true) Missive comments live inline. Unthread separates private notes from customer-facing messages.
Label (organization) Tag Create tags via POST /tags, then link to conversations. Provider-synced email labels usually should not be migrated.
Contact Customer or Account Missive contacts are flat records. Unthread separates Customers (Slack channels) from Accounts (company entities with email domains).
Team / Shared Mailbox Project Missive organizes around shared mailboxes. Unthread organizes around Projects with scoped permissions.
Assignment assignedToUserId Direct mapping, but requires a user ID lookup — Unthread syncs users from Slack.
Shared Draft No equivalent. Unsent shared drafts cannot be migrated.
Rules Automations Trigger/action models differ. Manual re-creation required.
Canned Responses Knowledge base / templates Missive exports these as .html files. No direct API import into Unthread. Manual re-creation.
Warning

Multi-channel threads don't map cleanly. A single Missive conversation can contain an email, an SMS reply, a WhatsApp message, and internal comments — all in one thread. Unthread conversations are typed at creation: slack, email, or triage. You must decide at migration time whether to flatten multi-channel Missive threads into a single Unthread email conversation or split them by channel type. Splitting preserves channel fidelity but breaks the chronological thread. Our recommendation: use the originating channel of the first message to set the conversation type, and store per-message channel metadata in a custom ticket field for audit purposes.

For statuses, Unthread's API accepts open, in_progress, on_hold, and closed, with priority values of 3, 5, 7, and 9. A practical mapping: Missive closedclosed, active assigned work → in_progress, unassigned active → open, and snoozed/waiting → on_hold if your team uses snooze as a pending state. For snooze states, preserve the original Missive snooze timestamp in a custom ticket field — Unthread has no native snooze-until timestamp on conversations, so any scheduled re-open logic must be rebuilt in your automation layer. Build an explicit normalization layer — do not attempt a blind copy. (docs.unthread.io)

Extracting Data from Missive

You have two extraction paths: the built-in data export and the REST API. For most migrations, use both — the export for bulk history and the API for delta syncs after the export was generated.

Built-In Data Export

Missive provides a full data export via Settings → Login & Security. This produces:

  • Conversations: All messages, comments, assignments, and timestamps in .json per organization
  • Comments: Internal comments in .csv per organization (excludes trashed conversations)
  • Contacts: All contacts from every contact book in .csv per book
  • Phone data: From/To fields across SMS messages in a single .csv
  • Rules: In .csv per organization
  • Responses: Canned responses in .html format with attachments

Missive delivers the export as a conversation in your inbox when ready. The JSON structure is Missive-specific and requires transformation, but this is the simplest starting point for a historical backfill. (missiveapp.com)

REST API Extraction

For selective or incremental extraction, use the Missive REST API. Key constraints:

  • Rate limits: 5 concurrent requests, 300 requests per minute, 900 requests per 15-minute rolling window
  • Pagination: Conversations use cursor-based pagination with an until parameter (equals last_activity_at of the oldest conversation from the previous page), max 50 conversations per page. Message and comment pages max out at 10 items each.
  • Scope: Conversation listing is scoped to the token owner and must be filtered by mailbox, shared label, or team.
  • Auth: Bearer token from Settings → API tab. Requires the Productive plan ($24/user/month annual) or higher — the Starter plan does not include API access. (missiveapp.com)
  • Batch endpoints: Missive supports batch GET /v1/messages/:id,:id2,:id3 calls to reduce request volume when fetching message bodies. (missiveapp.com)
import requests
import time
 
MISSIVE_API = "https://public.missiveapp.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_MISSIVE_TOKEN"}
 
def extract_conversations(mailbox_id, until=None):
    """Extract conversations page by page from Missive."""
    all_convos = []
    while True:
        params = {"mailbox": mailbox_id, "limit": 50}
        if until:
            params["until"] = until
        resp = requests.get(
            f"{MISSIVE_API}/conversations",
            headers=HEADERS, params=params
        )
        resp.raise_for_status()
        data = resp.json().get("conversations", [])
        if not data:
            break
        all_convos.extend(data)
        until = data[-1]["last_activity_at"]
        time.sleep(1)  # Respect rate limits
    return all_convos
Tip

Extraction math: At 1 request per second and 50 conversations per page, listing 50,000 conversations takes ~1,000 requests ≈ 17 minutes. But each conversation's messages require separate API calls, adding ~50,000 more requests. Even with batch message endpoints, budget 10–15 hours of sustained API polling for a dataset this size. Use a distributed task queue (like Celery or BullMQ) to manage workers, track cursor states, and resume gracefully after failures.

Two edge cases matter during extraction:

  1. Merged conversation IDs: Missive conversation IDs can change after merges. Requesting an old ID may return the new surviving ID. If you deduplicate by conversation ID alone, you will create duplicates or miss threads. (missiveapp.com)
  2. 400-message limit: Missive has a hard limit of 400 messages per conversation. Very long or merged threads split into new threads once that ceiling is hit. Account for this in your deduplication logic. (missiveapp.com)

Before exporting, audit complexity with Missive search. Operators like label:, from:, to:, and subject: take no space after the colon. If your connected provider is Gmail or Google Workspace, operators like has:attachment, after:, before:, and older_than: also work:

label:vip has:attachment after:2026/01/01
from:renewals@customer.com subject:invoice
has:attachment older_than:6m

Use searches like these to separate active queues from archive noise before committing to a full-history import. (missiveapp.com)

The Transform Layer

This is where most engineering time goes — expect 60–70% of total effort here. The APIs are straightforward; the data-model translation is not.

HTML to Slack Markdown

Missive stores email bodies as rich HTML. Unthread's POST /conversations endpoint takes a markdown field for the initial message, while POST /conversations/:id/messages accepts either html or markdown via the body field. (docs.unthread.io)

For email-type conversations in Unthread, you can pass body.type: "html" on subsequent messages. But the initial conversation creation always requires markdown. Your parser must handle at minimum:

  • <br> and <p> tags → \n
  • <strong> and <b>*text*
  • <em> and <i>_text_
  • <a href="url">text</a><url|text> (Slack link format)
  • <blockquote>> text

Use a library like turndown (JavaScript) or html2text (Python) as a starting point, then handle Slack-specific syntax on top.

Email signatures and deeply nested quoted replies (the "On Thursday, John wrote:" chains) should be stripped or truncated. They create massive text walls in Slack that bury the actual content.

Missive inline images also need special handling — fetched HTML bodies reference attachments via data-missive-attachment-id, not normal src URLs. You must resolve these references against the conversation's attachment metadata array and re-upload the images separately. Standard HTML parsers will not resolve these references automatically. (missiveapp.com)

B2B Account Mapping

Unthread groups users into Accounts (companies). Missive contacts are flat records, often without strict company groupings.

The transformation logic:

  1. Extract the from email address of the external author in each Missive conversation.
  2. Parse the domain (e.g., user@acmecorp.comacmecorp.com).
  3. Check the domain against a blocklist of freemail providers (gmail.com, yahoo.com, hotmail.com).
  4. If it is a corporate domain, create or map it to an Unthread Account using the emailsAndDomains field.
  5. If it is a freemail domain, you need a fallback — either a CRM lookup (Salesforce, HubSpot) to find the true company, or a catch-all account for B2C contacts.
Warning

Contact deduplication: Missive allows the same email address to exist in multiple contact books. Unthread's Account model uses emailsAndDomains for matching. Deduplicate contacts before loading — otherwise you will create duplicate Accounts or orphan conversations.

Internal Comments to Private Notes

Missive blends external messages and internal team chat into a single chronological timeline. When mapping to Unthread, check the message type:

  • If the Missive message is type email, sms, or chat, map it as a standard conversation message in Unthread.
  • If the Missive message is type comment, map it as a message with isPrivateNote: true in Unthread.

This ensures internal team deliberations remain private and do not accidentally surface in customer-facing channels. For Slack-led flows, test triage-side visibility behavior in a sandbox before running a bulk import to confirm private notes do not appear in customer-facing Slack Connect channels.

Attachment Handling

Missive attachment URLs are authenticated and may expire. Do not pass them directly to Unthread — the links will break.

The pipeline:

  1. Detect attachment arrays in Missive message payloads.
  2. Download the binary file from Missive.
  3. Re-upload to Unthread via multipart/form-data on the conversation or message creation endpoint.

Unthread enforces a 20 MB max file size and a 10-attachment limit per Slack message (a Slack platform constraint). If a Missive conversation has more than 10 attachments, split them across multiple messages.

Danger

Large attachments: If a Missive email has a file exceeding 20 MB, the Unthread upload will fail. Implement a file-size check in your pipeline: for oversized files, upload to external storage (S3, GCS) and insert a text link into the message body instead.

Loading Data into Unthread

Target Setup

Before loading a single conversation, configure your Unthread workspace:

  1. Create Projects that map to your Missive shared mailboxes or teams (with Shared vs. Restricted visibility and Internal support vs. External support modes)
  2. Create Ticket Types with custom fields to capture metadata you need to preserve
  3. Create Tags that mirror your Missive organization label taxonomy
  4. Set up email inboxes if migrating email conversations — Unthread's email flow is forwarding-based and replies default to @mail.unthread.io unless you verify your own domain (docs.unthread.io)
  5. Verify user sync — Unthread syncs users from Slack, so confirm target agents exist in the right projects before mapping assignees. This is a common failure mode: the Missive assignee has no matching Unthread user because the agent was never synced from Slack or added to the correct project.
  6. Create Accounts and Customers for company-level and individual contact records
  7. Disable or audit active webhooks and integrations — Unthread fires webhooks on conversation creation events. A bulk import of 50,000 conversations will trigger 50,000 webhook payloads to any connected systems (PagerDuty, Jira, internal tooling). Suspend non-essential webhooks before the import window and re-enable them after validation completes.

Generate API keys from Setup → Service Accounts. Keys can be project-scoped or tenant-wide. (docs.unthread.io)

Tip

Unthread's UI exposes ticket-type field types (like date and file upload) that the public API reference may not fully document. If you plan to generate ticket types programmatically, test field type creation in a sandbox before assuming full UI and API parity. (docs.unthread.io)

Key Write Endpoints

Operation Endpoint Method
Create Account POST /accounts POST
Create Customer POST /customers POST
Create Tag POST /tags POST
Create Conversation POST /conversations POST
Add Message POST /conversations/:id/messages POST
Link Tag POST /tags/:tagId/conversations/create-links POST
Create Ticket Type POST /ticket-types POST

Common API Errors in the Load Phase

Knowing which errors to expect prevents silent data loss. The following 4xx responses occur regularly in Unthread bulk imports:

HTTP Status Likely Cause Fix
400 Bad Request Missing required field (type, emailInboxId for email conversations, channelId for Slack conversations) Validate payload schema before sending; log the raw response body which contains the specific missing field name
400 Bad Request markdown field on conversation creation contains unsupported Slack syntax or exceeds length limits Strip raw HTML tags before conversion; truncate to 40,000 characters and continue message body in first reply
422 Unprocessable Entity assignedToUserId references a user not synced from Slack or not a member of the target project Pre-validate all assignee IDs against GET /users before the load run; fall back to unassigned on 422
422 Unprocessable Entity customerId references a customer record that was not yet created Enforce load order: customers before conversations
404 Not Found emailInboxId does not exist in the target workspace Verify inbox UUIDs in a pre-flight check; inboxes are not auto-created
413 Payload Too Large Attachment exceeds 20 MB limit Detect file size before upload; redirect to external storage link
429 Too Many Requests Rate limit exceeded Implement exponential backoff starting at 2 seconds; increase backoff multiplier by 2× on each retry up to a 60-second ceiling

Log the full response body on every non-2xx response. Unthread's error payloads include a human-readable message field that identifies the specific validation failure — this is more useful than the status code alone.

Creating Conversations

Every Unthread conversation requires a type field — slack, email, or triage:

{
  "type": "email",
  "markdown": "Customer reported login failure on mobile app.",
  "status": "closed",
  "title": "Login failure - mobile app",
  "assignedToUserId": "63335fa9-ffcc-4103-8905-e4440cc7c7d4",
  "customerId": "9YQjOSTFXeOPF8xNURFd",
  "emailInboxId": "inbox-uuid-here",
  "onBehalfOf": {
    "email": "customer@example.com",
    "name": "Jane Doe"
  },
  "priority": 5
}

For type: "email", the emailInboxId is required and the onBehalfOf field identifies the external contact. For type: "slack", provide a channelId or the conversation goes to a DM. For type: "triage", provide a triageChannelId for internal-only tickets.

Note that Unthread's email connector only creates tickets from new emails after setup. Its native Slack channel backfill only covers threads from the previous six months. If you need older Missive history inside Unthread, you must use the API import path described here. (docs.unthread.io)

Adding Messages

After creating the conversation, inject historical messages in chronological order:

{
  "body": {
    "type": "html",
    "value": "<p>Original email body from Missive</p>"
  },
  "isPrivateNote": false,
  "onBehalfOf": {
    "email": "customer@example.com",
    "name": "Jane Doe"
  }
}

Set isPrivateNote: true for Missive internal comments.

Note the format difference: POST /conversations takes a markdown string for the initial message, while POST /conversations/:id/messages accepts a body object with type: "html" or type: "markdown". This means the first Missive message in each thread needs HTML-to-markdown conversion regardless of your approach for subsequent messages. (docs.unthread.io)

Timestamp Limitation

Danger

Unthread does not support backdating createdAt. The POST /conversations endpoint does not expose a createdAt parameter. Passing createdAt in the request body is silently ignored — the API returns 200 but the conversation carries the import timestamp, not the original Missive value. This was confirmed by testing against the Unthread API: a conversation created with "createdAt": "2023-01-15T10:00:00Z" in the payload receives the server-side creation time in the response. This affects SLA calculations, reporting, and audit trails. Preserve original Missive timestamps in a dedicated custom ticket field (e.g., original_created_at) and in your external migration log. Do not rely on Unthread's native createdAt for any historical reporting after migration.

Unthread API Rate Limits and Throughput

Unthread's API documentation does not publish explicit rate limits. Empirically, sustained loads of 5 requests per second on conversation creation run without 429 responses on standard workspaces. At 10 requests per second, 429 responses begin appearing after several hundred consecutive requests. The practical safe ceiling for bulk imports is 5 req/sec with exponential backoff on any 429, starting at a 2-second delay and doubling up to 60 seconds.

At 5 req/sec, a 30,000-conversation import with an average of 4 messages per conversation requires approximately 150,000 API calls — roughly 8.5 hours of sustained load time, excluding attachment uploads. Attachment uploads are slower due to multipart overhead and file transfer time; budget an additional 2–4 hours per 10 GB of attachment data on a standard connection.

For large migrations (25,000+ conversations), contact Unthread's support team before starting. They have accommodated migration use cases with temporary rate limit increases.

Notification Behavior

When bulk-loading historical tickets, test whether conversation creation triggers Slack notifications to your internal triage channels or customer-facing Slack Connect channels. Migrating 50,000 tickets will generate 50,000 Slack pings if notifications are active. Verify notification behavior in a sandbox with a single test account before running the production load. Coordinate with Unthread support if you need notifications suppressed during the import window.

Step-by-Step Migration Execution

Step 1: Audit Missive Data

Inventory what you have before extracting anything:

  • Total conversation count per shared mailbox
  • Volume of internal comments vs. external messages
  • Channels used (email, SMS, WhatsApp, live chat)
  • Label taxonomy — organization labels vs. provider-synced email labels
  • Attachment volume and file sizes (flag any files over 20 MB)
  • Contact book structure and duplicate contacts
  • Snooze states requiring custom field preservation

Use the built-in export first to get a full snapshot. Parse the JSON to build your inventory.

Step 2: Configure Unthread Target Structure

Create Projects, Ticket Types, Tags, email inboxes, Accounts, and Customers. Ensure all agents are synced from Slack and assigned to the correct projects. Build a lookup table mapping Missive user IDs to Unthread user IDs. Add custom fields for: original_created_at, original_channel_type, and missive_conversation_id — these three fields are the minimum audit trail for post-migration validation and rollback reference.

Step 3: Suspend Webhooks and Non-Essential Integrations

Before any data loads, suspend all active Unthread webhooks and connected integrations (Jira, PagerDuty, internal tooling). Re-enable them only after the import passes validation. Failing to do this is the most common cause of production incidents during migrations: bulk imports silently propagate to downstream systems at full volume.

If your Unthread workspace has email forwarding configured, do not update the forwarding rules until the historical import completes. New emails arriving during an in-progress import may create duplicate conversations if the forwarding rules activate before the historical load finishes.

Step 4: Run the Historical Sync

Two weeks before your go-live date, begin extracting all Missive conversations. Run your ETL pipeline and load these into Unthread. This moves the bulk of your data volume without impacting daily operations.

Execute the load in strict dependency order:

  1. Tags — create all tags first
  2. Accounts — create company-level entities
  3. Customers — create customer records, linked to accounts
  4. Conversations — create with initial messages
  5. Messages — inject remaining messages into each conversation thread
  6. Tag links — associate tags with conversations
  7. Attachments — upload files as separate multipart requests

Step 5: Delta Sync and Cutover

On cutover day, freeze Missive. If Unthread is handling email, update your forwarding rules so new emails route to Unthread's email inbox. If migrating to Slack-native support, direct customers to your Slack Connect channels or new intake workflow.

Run the extraction script again with a time-based filter, fetching only conversations created or updated since the historical sync began. This delta is small and completes quickly.

Step 6: Validate

Build validation checks before, during, and after:

  • Count match: Total conversations extracted vs. created in Unthread
  • Message count per conversation: Spot-check a random 5–10% sample against Missive
  • Tag coverage: Every organization label from Missive has a corresponding tag
  • Attachment integrity: File sizes match between source and target
  • Internal comment placement: Verify private notes appear in triage threads, not customer-facing channels
  • Assignee mapping: Confirm assignments resolved to valid Unthread users
  • Custom field audit: Verify original_created_at values are populated on a random sample

Keep Missive in read-only mode for 30–90 days as a fallback before decommissioning.

Rollback Procedure

If post-import validation fails at scale, the rollback path is:

  1. Stop the Unthread load pipeline immediately.
  2. Re-enable Missive as the active inbox (it has been in read-only mode, not decommissioned).
  3. Identify which conversation IDs were successfully imported using your migration log (your pipeline should write a log of every successfully created Unthread conversation ID against the source Missive ID).
  4. Delete partially imported Unthread conversations via DELETE /conversations/:id to avoid duplicates when re-running.
  5. Fix the transformation or load error, re-run the affected batch, and re-validate before proceeding.

The critical constraint: because Unthread does not support backdated timestamps, a rollback and re-run will not change the createdAt values on re-imported conversations — they will carry the new re-run timestamp. If timestamp accuracy matters for SLA or audit purposes, escalate to Unthread support before re-running.

Edge Cases That Break Migrations

Merged conversations and the 400-message limit. Missive conversation IDs can change after merges, and threads cap at 400 messages before splitting into new threads. If you assume one customer thread equals one conversation ID forever, you will create duplicates or miss data. (missiveapp.com)

Multi-channel conversations. A Missive thread that starts as email and continues via SMS is one conversation in Missive but must become a single typed conversation in Unthread. You lose channel-per-message granularity. Store the original channel type in a custom conversation field for audit purposes.

Shared drafts. Missive's collaborative draft feature has no Unthread equivalent. Resolve or discard unsent drafts before cutover.

Label types. Missive has provider-synced email labels (Gmail folders, etc.) and Missive-only organization labels. Only organization labels should become Unthread tags. Migrating provider labels creates noise and may duplicate folder structures that already exist in your email provider. (missiveapp.com)

Webhook flood. Any active Unthread webhook subscription will receive a payload for every conversation created during the import. At 50,000 conversations, this is 50,000 outbound webhook calls. Suspend webhooks before import; see Step 3 above.

Email forwarding race condition. If you activate Unthread email forwarding before completing the historical import, new inbound emails may create conversations that overlap with or duplicate in-flight import records. Update forwarding rules only after the import and validation are complete.

Slack Connect channel ownership. If your Unthread target includes Slack Connect channels, Unthread may not be able to connect a shared channel created by another workspace until that workspace allows Post and Invite permissions. (docs.unthread.io)

Inline images. Missive HTML bodies reference inline attachments via data-missive-attachment-id. Standard HTML parsing will not resolve these. You need explicit attachment metadata lookups from the conversation's attachment array and image re-uploads.

Canned responses. Missive exports them as .html files. Unthread does not have a direct API endpoint for importing response templates. These must be manually re-created in Unthread's knowledge base or as automation templates.

Microsoft Teams. Unthread's public docs still describe the Teams integration as beta as of mid-2026. If your target workflow uses Teams instead of Slack, validate the integration stability before committing to a full migration.

Timeline and Effort Estimates

Scenario Scope Estimated Timeline Engineering Cost Range
Contacts only CSV export → scripted Account/Customer creation 1–3 days $500–$2,000
Contacts + labels + structure Tags, Accounts, Customers, ticket types 3–5 days $1,500–$5,000
Full history (< 5,000 conversations) All objects, messages, attachments 1–2 weeks $5,000–$15,000
Full history (5,000–25,000 conversations) All objects with batching and validation 2–4 weeks $15,000–$40,000
Full history (25,000+ conversations) Staged migration with parallel operation 4–6 weeks $35,000–$80,000+

Cost ranges assume a single mid-level engineer at $150–$200/hour. Attachment-heavy datasets, complex multi-channel thread mapping, or significant CRM enrichment requirements push costs toward the upper bound. API costs for both platforms are negligible — Unthread does not charge per API call, and Missive API access is included in the Productive plan subscription.

The dominant cost is the transform layer, not the API calls. Expect 60–70% of engineering time on data transformation, edge case handling, and validation. The remaining 30–40% goes to API integration and error handling.

When Not to Migrate Full History

Full historical migration is not always worth the cost. Consider skipping it if:

  • Your Missive conversations are predominantly email-based and still searchable in your email provider's archive (Gmail, Outlook).
  • Your conversation volume exceeds 50,000 and the transform complexity outweighs the value of old tickets.
  • Your team is willing to run both platforms in parallel for 30–90 days while new conversations flow into Unthread.
  • You have no compliance requirement mandating that historical conversations live in the active support tool.

In these cases, migrate contacts and structural data (tags, accounts), set a cutover date, and archive the full Missive export in cold storage for reference.

Making the Call

A Missive to Unthread migration is a data-model translation project. The APIs on both sides are functional and documented. The hard part is not connecting the pipes — it is deciding how to transform channel-agnostic collaborative inbox data into typed, Slack-native tickets without losing the context your team relies on.

The four decisions that determine migration architecture:

  1. How to type-map conversations — email vs. slack vs. triage. This is set at conversation creation and cannot be changed after the fact.
  2. Whether to migrate full history or start fresh with a cutover date. For most teams with more than 25,000 conversations, a clean cutover plus cold-storage archive is cheaper than full migration.
  3. How to handle the timestamp limitation — migrated records carry import timestamps, not originals. Decide before migration whether SLA and reporting systems can tolerate this or whether custom fields are required.
  4. Where to put internal comments — as private notes (isPrivateNote: true) or triage thread messages. This determines whether historical team deliberations are visible to agents reviewing old tickets.

Get those four decisions right and the rest is engineering execution.

Frequently Asked Questions

Is there a native Missive to Unthread migration tool?
No. Neither Missive nor Unthread provides a native migration path, import wizard, or Zapier template that connects their data models at the required depth. You need a custom ETL pipeline using Missive's built-in export or REST API for extraction and Unthread's REST API for loading.
What are Missive's API rate limits for data extraction?
Missive enforces a maximum of 5 concurrent requests, 300 requests per minute, and 900 requests per 15-minute rolling window. Conversations are paginated at max 50 per page, while messages and comments page at max 10. API access requires the Productive plan ($24/user/month annual) or higher.
Can I preserve original timestamps when migrating to Unthread?
No. Unthread's POST /conversations endpoint does not support setting a custom createdAt timestamp. All migrated conversations carry the import timestamp. Store original Missive timestamps in a custom ticket type field or the metadata field for audit and reporting purposes.
How long does a Missive to Unthread migration take?
A contacts-only migration takes 1–3 days. A full historical migration with conversations, messages, and attachments takes 1–6 weeks depending on volume. Most engineering time (60–70%) goes into the data transformation layer, not the API integration.
Do I need to convert Missive HTML emails for Unthread?
Yes, at least partially. Unthread's POST /conversations endpoint requires the initial message in markdown format. Subsequent messages can use HTML or markdown. You must convert HTML to Slack-compatible markdown for the first message in each thread, and handle inline image references that use Missive-specific attachment IDs.

More from our Blog