---
title: "Desk365 to Intercom Migration: A Technical Guide"
slug: desk365-to-intercom-migration-a-technical-guide
date: 2026-08-03
author: Wahab
categories: [Intercom, Migration Guide, Help Desk]
excerpt: "Technical guide to migrating from Desk365 to Intercom. Covers data model mapping, API extraction, ticket import, attachments, knowledge base, and validation."
tldr: "Desk365 to Intercom migration requires translating a ticket-centric, M365-integrated model into a contact-centric platform. Extract via API, import contacts first, then tickets with full thread history."
canonical: https://clonepartner.com/blog/desk365-to-intercom-migration-a-technical-guide/
---

# Desk365 to Intercom Migration: A Technical Guide


> [!NOTE]
> **TL;DR — Desk365 to Intercom Migration**
>
> A Desk365 to Intercom migration translates a ticket-centric, Microsoft 365-integrated helpdesk into a contact-centric, messenger-first platform. Realistic timeline: 1–2 weeks for under 20K tickets, 2–4 weeks for larger volumes. The hard parts: extracting threaded conversations from Desk365's API (which returns tickets and replies at separate endpoints), mapping multi-level custom fields to Intercom's flat attributes, handling attachments through intermediary storage, suppressing Intercom automations during import, and working within Intercom's API rate limits. Desk365 objects with no Intercom equivalent include Time Entries, Asset Management records, Approval workflows, Change Management items, and SLA policies. Build in-house if you have a developer, low ticket volume, and simple custom fields. For anything larger, a managed migration pays for itself in rework avoided.

## What Is a Desk365 to Intercom Migration?

A **Desk365 to Intercom migration** is the process of extracting tickets, contacts, companies, knowledge base articles, tags, and custom field data from Desk365 and importing them into Intercom — mapping Desk365 **Tickets** to Intercom **Tickets** or **Conversations**, Desk365 **Contacts** to Intercom **Contacts** (Users or Leads), and Desk365 **Companies** to Intercom **Companies**.

This is not a copy-paste operation. Desk365 is built around a traditional ticketing model with deep Microsoft 365 integration — Desk365 works with Microsoft Teams, enabling support teams to manage tickets directly within the Teams interface. Intercom is a messenger-first platform where every interaction revolves around Contacts, Conversations, and Conversation Parts. The data models are fundamentally different.

Teams typically make this move when:

- **Shifting from internal IT support to external customer support.** Desk365 excels at ITSM; Intercom is built for customer-facing communication.
- **Consolidating around a single customer platform.** Teams outgrow Desk365's Microsoft-centric ecosystem and want support, onboarding, and engagement in one place.
- **Adopting AI-first support.** Intercom's Fin AI Agent is designed for autonomous L1 resolution via natural language understanding; Desk365's AI features are narrower in scope, focused primarily on article suggestions and ticket categorization.
- **Moving to messenger-based UX.** Intercom replaces the traditional ticket portal with real-time, chat-style interactions embedded directly in your product.

### Why this migration is non-trivial

Five architectural differences make this harder than it looks:

1. **Identity management.** Desk365 relies on Microsoft 365 and Azure Active Directory (Azure AD) for user and agent identity. Intercom uses standard email addresses or an `external_id`. You need to map Azure AD User Principal Names (UPNs) to Intercom contact profiles — detaching from M365's identity layer is the first hurdle.

2. **Ticket-centric vs. contact-centric.** Desk365's default ticket statuses are Open, Pending, Resolved, and Closed, with tickets representing the core unit of work. Intercom organizes everything around Contacts — a fundamental data model mismatch we also cover in our [Zendesk to Intercom migration guide](https://clonepartner.com/blog/blog/zendesk-to-intercom-migration-the-2026-technical-guide/) — meaning every ticket and conversation must be linked to an existing Contact record. Contacts must exist before you can create tickets.

3. **Custom fields mismatch.** Desk365 supports single-level field types (Dropdown, Text Input, Checkbox, Date, Number) and multi-level fields that dynamically populate child options based on parent selection. Intercom supports flat custom attributes only (string, integer, float, boolean, datetime, list) — there is no native multi-level field support.

4. **Conversation thread structure.** Desk365 stores ticket replies and internal notes as part of the ticket record. Intercom uses a Conversation Parts / Ticket Parts model with a hard limit of 500 parts per ticket. If a Desk365 ticket has hundreds of replies plus internal notes, you need to plan for that ceiling.

5. **Thread formatting.** Desk365 captures Microsoft Teams chat logs and rich HTML emails. Intercom supports a specific subset of HTML in its conversation parts. Complex Teams formatting — nested tables, adaptive cards, Microsoft Word-style inline CSS (e.g., `<span style="mso-bidi-font-weight: normal;">`) — will break or render poorly if not sanitized before import.

## Desk365 vs. Intercom: Data Model Mapping

Before writing any migration code, map every Desk365 object to its Intercom equivalent:

| Desk365 Object | Intercom Equivalent | Migration Path | Notes |
|---|---|---|---|
| Ticket | Ticket or Conversation | API create | Decide before scripting — this shapes everything |
| Contact | Contact (User or Lead) | API create | Email is the minimum required field |
| Company | Company | API create | Matched by `company_id` or name |
| Ticket Reply (agent/customer) | Ticket Part (comment) | API reply | Preserves conversation thread |
| Internal Note | Ticket Part (note) | API reply | Admin-only visibility |
| Knowledge Base Article | Article | Articles API | HTML body preserved |
| KB Category/Folder | Collection | Collections API | Up to 3 levels deep |
| Custom Ticket Field | Ticket Type Attribute or Data Attribute | API create | Flat types only — no multi-level |
| Tag | Tag | API apply | Tags applied post-creation |
| SLA Policy | SLA in Intercom | Manual rebuild | No API import |
| Automation Rule | Workflow | Manual rebuild | Platform-specific logic |
| Time Entry | No equivalent | Archive only | Export as CSV/JSON for reference |
| Asset Record | No equivalent | Archive only | Intercom has no asset management |
| Approval / Change Management | No equivalent | Archive only | ITSM-specific features |
| Department | No direct equivalent | Tag or custom attribute | Map to tags or contact segments |

> [!WARNING]
> **Critical decision: Ticket or Conversation?** Every Desk365 ticket must land as either an Intercom Ticket or an Intercom Conversation. This is one of the most important decisions to make before you start scripting. Intercom Tickets have structured states (submitted, in_progress, waiting_on_customer, resolved) and support Ticket Types with custom attributes. Conversations are lighter, thread-based, and better for historical chat-style data. For most migrations from ticket-centric platforms like Desk365, Intercom Tickets are the right choice because they preserve the structured workflow your team is used to — and critically, the Tickets API supports backdating reply timestamps while the Conversations API does not.

## Step 1: Export Data from Desk365

Desk365 provides two extraction paths: the UI-based CSV export and the REST API.

### CSV export

When you export tickets from Desk365, you create a copy of your ticket data in CSV format. You can choose the time period and select the required ticket fields, custom fields, and contact fields to include.

The CSV export works for small datasets (under 5,000 tickets) and includes ticket metadata. It does **not** include:

- Full conversation threads (individual replies and notes)
- Attachments (only references, not files)
- Knowledge base articles
- Internal notes

For anything beyond a basic metadata migration, you need the API.

### Desk365 API v3

Desk365's API v3 follows the OpenAPI Specification with the following key parameters:

- **Base URL:** `https://<yoursubdomain>.desk365.io/v3/`
- **Authentication:** Each subdomain has its own unique API Key passed as a Bearer token.
- **Pagination:** Retrieve 30, 50, or 100 tickets per call using the `ticket_count` parameter. Desk365 uses offset-based pagination — increment `page` until the response returns fewer records than `ticket_count`, which signals the final page. If a response times out mid-pagination, re-request that page using the same `page` and `ticket_count` values; Desk365 does not use cursors, so there is no state to resume.
- **Description formats:** The API returns ticket descriptions in both HTML format (preserving bold, italic, lists, links) and plain text format (stripped of HTML tags).

```bash
# List tickets from Desk365 API v3
curl -X GET "https://yoursubdomain.desk365.io/v3/tickets?ticket_count=100&page=1" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Desk365 tickets are not flat records. The initial ticket payload contains metadata (subject, status, priority, requester ID), but the actual communication history — replies and internal notes — lives at separate endpoints. Your extraction script must:

1. Query the `/tickets` endpoint using pagination, incrementing `page` until the response record count is less than `ticket_count`.
2. For each Ticket ID, query the replies and notes endpoints separately.
3. Stitch the initial description, replies, and notes into a single chronological JSON array per ticket.
4. Record the timestamp of your final extraction query — you will need this for the delta sync at cutover.

> [!TIP]
> Desk365's API documentation is instance-specific — access it at `https://<yoursubdomain>.desk365.io/apis/api-docs.html`. Available fields vary by plan (Standard, Plus, Premium). Confirm which custom field endpoints are available on your plan before writing extraction scripts.

For a deeper dive on getting data out of Desk365, see our guide: [How to Export Data from Desk365: Methods, API Limits & Portability](https://clonepartner.com/blog/blog/how-to-export-data-from-desk365-methods-api-limits-portability/).

## Step 2: Import Contacts and Companies into Intercom

Contacts must exist in Intercom before you create tickets. Every ticket or conversation needs to be associated with an existing user or lead. These contacts need at minimum a `user_id` (from your product) or email address.

### Before you import: understand Intercom's contact-based pricing

Intercom bills by the number of active contacts in your workspace. Migrating 50,000 historical contacts — many of whom may never interact with your support team again — can trigger an immediate tier upgrade. Before migrating all contacts, audit your Desk365 contact list and decide whether to import all historical contacts or only those active within a defined window (e.g., the last 24 months). Importing a "Legacy" tag on older contacts lets you filter them from active reporting while keeping them available for ticket association.

### Agent mapping (Admins)

In Intercom, agents are called **Admins**. Create all your agents in Intercom before the migration begins. Once created, Intercom assigns an `admin_id` to each.

Extract your Desk365 agent list and build a strict mapping:

```json
{
  "desk365_agent_email@company.com": {
    "desk365_id": "d365_88392",
    "intercom_admin_id": "7382910"
  }
}
```

**Handling departed agents:** If a Desk365 agent has left the company, you have two choices — consume an Intercom seat license to recreate them, or map all their historical actions to a single "Legacy Agent" account in Intercom. The Legacy Agent approach saves licensing costs. Append the original agent's name in the message text so the historical record stays clear.

### Contact creation

Use `POST /contacts` to create contacts — the only required parameter is `email`. Use `POST /data_attributes` to create custom attributes for your contacts.

Key mapping considerations:

- Map Desk365 `email` to Intercom `email`.
- Map Desk365 `contact_id` to Intercom `external_id`. This prevents duplicate user creation if the contact logs into your app later.
- Push Desk365 custom contact fields into Intercom `custom_attributes`.

Desk365 contacts can have multiple email addresses, and Desk365 supports merging multiple contacts into a single record. Before migrating, deduplicate your Desk365 contacts and decide which email becomes the primary identifier.

**Watch for duplicates on the Intercom side:** If you attempt to create a contact that already exists in your Intercom app, you will get a `409 Conflict` response. Build your script to catch 409s, retrieve the existing contact's Intercom ID, and continue.

**Idempotency is critical for re-runnable migrations.** Your script will fail partway through — network timeouts, rate limit pauses, and API errors are inevitable at scale. Before creating any contact, query Intercom's search endpoint using `external_id` (your Desk365 contact ID) to check whether the record already exists. Use `POST /contacts/search` with a filter on `external_id`. If a match is found, retrieve the `id` and skip creation. This pattern prevents duplicate records on re-runs and is far safer than relying solely on 409 handling.

```javascript
// Idempotent contact creation: check before creating
async function getOrCreateContact(desk365Contact) {
  // Search for existing contact by external_id
  const searchResult = await fetch('https://api.intercom.io/contacts/search', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${INTERCOM_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      query: {
        field: 'external_id',
        operator: '=',
        value: desk365Contact.id.toString()
      }
    })
  });
  const { data } = await searchResult.json();
  if (data.length > 0) return data[0].id; // Already migrated

  // Create new contact
  const contact = await fetch('https://api.intercom.io/contacts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${INTERCOM_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      role: 'user',
      email: desk365Contact.email,
      name: desk365Contact.name,
      external_id: desk365Contact.id.toString(),
      custom_attributes: {
        desk365_department: desk365Contact.department || null,
        source_system: 'desk365'
      }
    })
  });
  const newContact = await contact.json();
  return newContact.id;
}
```

### Company creation

Desk365 has both Companies and Departments. A Desk365 contact can belong to a department within a company, be linked directly to a company without a department, or exist on its own. In Intercom, Companies are a flat object — there's no department hierarchy. Map Desk365 departments to Intercom tags or custom attributes on the Company or Contact.

## Step 3: Create Ticket Types and Custom Attributes in Intercom

Before importing tickets, you need matching Ticket Types in Intercom.

Use `POST /tickettypes` to create ticket types with any names of your choosing. A `ticket_type_id` is required to create a ticket using the API.

### Mapping Desk365 ticket types

The 'Type' field in Desk365 helps agents categorize tickets, with four default choices: Question, Incident, Problem, and Request. Teams using custom types will have more. Map each Desk365 type to an Intercom Ticket Type:

| Desk365 Type | Intercom Ticket Type | Custom Attributes to Add |
|---|---|---|
| Question | General Inquiry | priority, category |
| Incident | Incident Report | priority, severity |
| Problem | Problem | priority, root_cause |
| Request | Service Request | priority, request_type |
| (Custom types) | Mapped 1:1 | As needed |

### The multi-level field problem

Desk365 custom ticket fields support multi-level dropdowns where selecting a parent option dynamically reveals child options. Intercom ticket type attributes support only flat types: `string`, `integer`, `float`, `boolean`, `datetime`, and `list`.

You have three options for handling multi-level fields, each with real tradeoffs:

| Option | Approach | When to use | Cost |
|---|---|---|---|
| **1. Flatten to single list** | Concatenate parent + child (e.g., "Hardware > Laptop > Screen") | When you need searchability of the full path and don't need to filter by level separately | ~30 min setup; loses filter granularity |
| **2. Split into separate attributes** | Create `category_l1`, `category_l2`, `category_l3` as separate list attributes | When agents need to filter by individual levels in Intercom reporting or Workflows | ~2 hrs setup per multi-level field; preserves full granularity |
| **3. Drop child levels** | Keep only the top-level value | When child-level values are not used in routing, SLAs, or reporting | Fastest; loses historical detail permanently |

**Decision rule:** Use Option 2 if any Intercom Workflow or report currently filters on a child-level value. Use Option 1 if the full path needs to be searchable but not individually filterable. Use Option 3 only if you have confirmed with your team that the child-level data has no operational use.

## Step 4: Disable Automations and Notifications Before Importing

This step is absent from most migration guides and is operationally critical.

When you create tickets and conversations via the Intercom API, Intercom's automation engine treats them as real events. By default, this means:

- **Workflow triggers fire.** Any Workflow configured to trigger on "ticket created" or "conversation started" will execute against every migrated record. At scale, this can send thousands of autoresponder emails to historical contacts.
- **Fin AI responds.** If Fin is enabled on your workspace, it may attempt to respond to migrated historical tickets as if they are new inbound conversations.
- **Notification emails go out.** Agents assigned to migrated tickets may receive hundreds or thousands of assignment notifications.

Before running your migration:

1. **Disable all Workflows** that trigger on ticket or conversation creation, assignment, or status change. Re-enable them after cutover.
2. **Disable Fin** on your workspace, or set it to operate only on specific inboxes that exclude the migration inbox.
3. **Suppress agent notifications** — in Intercom's notification settings, temporarily disable email and in-app notifications for the admin accounts that will be receiving assignments from migrated tickets.
4. **Use a dedicated inbox** for migrated tickets if your Intercom plan supports it, to isolate migration noise from live support queues.

Re-enable all automations and notifications only after validating that the migration data is correct.

## Step 5: Migrate Tickets with Full Conversation History

This is the core of the migration. Each Desk365 ticket becomes an Intercom Ticket (or Conversation) with its full reply thread.

### Choosing between Tickets and Conversations

| Dimension | Intercom Tickets | Intercom Conversations |
|---|---|---|
| Structured states | Yes (submitted, in_progress, waiting_on_customer, resolved) | No (open/closed only) |
| Custom attributes (Ticket Types) | Yes | Limited |
| Backdate reply `created_at` | Yes — Unix timestamp supported | No — timestamped at import time |
| Best for | Async IT requests, bug reports, long-running issues | Quick Q&A, chat history |

For Desk365 migrations, Intercom Tickets are almost always the correct choice. The inability to backdate timestamps on Conversations is disqualifying for most teams — a 2022 ticket imported today will appear as a new item in your inbox, corrupt historical reporting, and trigger automations.

### Ticket creation flow

1. **Create the ticket** via `POST /tickets` with `ticket_type_id`, contact reference, and ticket attributes.
2. **Add each reply** via `POST /tickets/{id}/reply` in chronological order.
3. **Add internal notes** as admin replies with `message_type: "note"`.
4. **Set final state** — update the ticket to its correct state (submitted, in_progress, waiting_on_customer, resolved).

Apply the same idempotency pattern here as with contacts: before creating a ticket, search for an existing ticket with a custom attribute storing the original Desk365 ticket ID. If found, skip creation. Use `POST /tickets/search` filtering on `ticket_attributes.legacy_ticket_id`. This prevents duplicate tickets on re-runs.

```javascript
// Add a customer reply to an Intercom ticket
await fetch(`https://api.intercom.io/tickets/${intercomTicketId}/reply`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${INTERCOM_TOKEN}`,
    'Content-Type': 'application/json',
    'Intercom-Version': '2.11'
  },
  body: JSON.stringify({
    message_type: 'comment',
    type: 'user',
    intercom_user_id: contactIntercomId,
    body: desk365Reply.body_html,
    created_at: Math.floor(new Date(desk365Reply.created_at).getTime() / 1000),
    attachment_urls: desk365Reply.attachments || []
  })
});
```

### The timestamp constraint

The Tickets API supports `created_at` as a Unix timestamp on replies — this preserves original reply dates from Desk365.

The Conversations API does not allow backdating `created_at` on conversation creation. A ticket from 2022 will appear as if it was created today, which destroys historical reporting and clutters the agent inbox. To preserve temporal context on the Conversations path:

1. **Use custom attributes.** Create a custom attribute called `original_created_at` and store the Desk365 timestamp in Unix epoch format.
2. **Prepend metadata** in the message body:

```json
{
  "body": "<strong>[Historical Record]</strong><br>Original Date: 2023-04-12<br>Original Desk365 ID: #4492<br><br>Original message body here...",
  "custom_attributes": {
    "legacy_ticket_id": "4492",
    "original_created_at": 1681296000
  }
}
```

This is a primary reason to prefer the Tickets path over Conversations for Desk365 migrations.

### Status mapping

| Desk365 Status | Intercom Ticket State | Notes |
|---|---|---|
| Open | `in_progress` | |
| Pending | `waiting_on_customer` | |
| Resolved | `resolved` | |
| Closed | `resolved` | Intercom has no "closed" state |
| Custom statuses | Map to nearest state | Store original as custom attribute |

> [!WARNING]
> **Intercom has no "Closed" ticket state.** Desk365 distinguishes between Resolved and Closed — Desk365 has a default rule that automatically closes resolved tickets after 120 hours if no further activity occurs. In Intercom, both map to `resolved`. If the distinction matters for your reporting or SLA calculations, store the original Desk365 status as a custom attribute (`desk365_original_status`). Be aware that any Intercom Workflow triggered on `ticket resolved` will count both Resolved and Closed Desk365 tickets as the same event — audit your Workflows for this before re-enabling them post-migration.

### Error handling

Build explicit handling for these error codes across both the Desk365 extraction and Intercom import phases:

| HTTP Code | Source | Cause | Handling |
|---|---|---|---|
| 401 | Either API | Invalid or expired API key | Halt immediately; verify credentials |
| 403 | Either API | Insufficient permissions for endpoint | Log and skip; check plan-level access |
| 404 | Desk365 | Ticket/contact deleted between extraction and import | Log as skipped; do not retry |
| 409 | Intercom | Contact or ticket already exists | Retrieve existing ID and continue |
| 413 | Intercom | Attachment exceeds 40MB | Log, omit attachment, append text note to ticket body |
| 429 | Intercom | Rate limit exceeded | Exponential backoff starting at 1s; log all 429s for replay |
| 500/503 | Either API | Server error | Retry up to 3 times with 5s delay; log if persistent |

Log every failed request with its full payload. You will need these logs to replay failures without re-running the entire migration from scratch.

## Step 6: Handle Attachments

Attachments are consistently the trickiest part of helpdesk migrations (a challenge we also highlight in our [Zoho Desk to Intercom migration guide](https://clonepartner.com/blog/blog/zoho-desk-to-intercom-migration-the-complete-technical-guide/)).

Desk365 stores attachments in Azure Blob Storage, and the API returns secure, time-limited URLs. You cannot pass these URLs directly to Intercom — they will expire or become inaccessible once your Desk365 account is closed.

The migration flow:

1. **Download** the file from the Desk365 URL to an intermediary server or cloud storage bucket (S3, Azure Blob, GCS).
2. **Upload** the file to cloud storage with a publicly accessible URL.
3. **Pass the public URL** to Intercom's `attachment_urls` parameter on ticket replies.

```javascript
async function migrateAttachment(desk365AttachmentUrl) {
  const fileBuffer = await downloadFile(desk365AttachmentUrl);
  const s3Url = await uploadToS3(fileBuffer, generateFilename());
  return s3Url;
}
```

Key constraints to handle:

- **10-attachment limit per reply.** Intercom's ticket reply API accepts up to 10 attachment URLs per reply. If a Desk365 reply has more than 10 attachments, split them across multiple sequential Intercom reply parts. Label each continuation part (e.g., " [Attachments continued — part 2 of 3]").
- **40MB per-file limit.** Intercom enforces a 40MB limit on file uploads via the API. Desk365 allows larger files depending on your M365 configuration. Your script must catch `413 Payload Too Large` errors and append a text note to the ticket body indicating the oversized attachment was omitted and its original filename.
- **Attachment at ticket creation.** Historically, it was not possible to add attachments directly to a ticket via the Intercom API at creation time. The reliable workaround remains: create the ticket first, then immediately add a reply part with the attachments. Verify against the current Intercom API changelog before building your pipeline.
- **Estimate your intermediary storage costs.** If you are migrating 100,000 attachments averaging 2MB each, that is 200GB of intermediary storage. Budget accordingly and set a retention policy to delete intermediary files after confirming successful migration.

## Step 7: Migrate Knowledge Base Articles

Desk365 offers a knowledge base with a branded self-service portal, multi-folder structure with agent review and publish workflows, and AI-powered article generation. Articles can be public or internal.

Intercom's Articles API handles this well:

1. **Create Collections first** — these mirror Desk365's KB categories. You can organize Articles into Collections, which are top-level containers that can contain other Collections up to three levels deep.
2. **Create Articles** — POST each article to `POST https://api.intercom.io/articles` with `title`, `body` (HTML), `author_id`, `parent_id`, and `parent_type`.
3. **Handle images** — Upload images and attachments to Intercom and replace the image URLs in your article HTML so they sit on Intercom's CDN.

### HTML sanitization: what Intercom accepts and what it rejects

Desk365 articles frequently contain Microsoft Word-style HTML generated by copying and pasting content. This HTML is incompatible with Intercom's Articles renderer.

**Tags Intercom's Articles API accepts:**
`<p>`, `<h1>`, `<h2>`, `<h3>`, `<strong>`, `<em>`, `<a>`, `<img>`, `<ul>`, `<ol>`, `<li>`, `<blockquote>`, `<pre>`, `<code>`, `<br>`, `<hr>`

**Tags and patterns that must be stripped before import:**
- All `style=""` inline attributes (especially `mso-*` properties from Microsoft Office)
- All `class=""` attributes referencing non-Intercom stylesheets
- `<span>` tags with only styling (safe to unwrap; preserve content)
- `<div>` wrappers with only layout styling (unwrap; preserve content)
- `<table>` used for layout (convert to `<ul>` lists or `<p>` blocks)
- `<font>` tags (replace with `<strong>` or `<em>` as appropriate)
- `<!--[if gte mso 9]>` conditional comments and their content
- `<o:p>` and other Office namespace elements
- `xmlns` and `xml:lang` attributes on any element

Use a sanitization library (DOMPurify in the browser, or `sanitize-html` in Node.js with a strict allowlist) configured to strip everything not on the accepted list above. Do not use a blocklist approach — Microsoft Word HTML introduces new patterns unpredictably. Allowlist only.

**Internal vs. public articles:** Desk365 supports internal-only articles for agent reference. Intercom lets you control article visibility through audience targeting. Set migrated internal articles to a restricted collection not surfaced in your public Help Center.

> [!NOTE]
> **Collection limit:** There is a maximum limit of 500 articles per Help Center collection. To manage more articles, split your content across multiple collections or sub-collections.

## Step 8: Respect Intercom API Rate Limits

Rate limiting is the primary bottleneck for large migrations.

Private apps have a default rate limit of 10,000 API calls per minute per app and 25,000 API calls per minute per workspace. The burst constraint is stricter: the permitted limit is distributed into 10-second windows, meaning 10,000 per minute equates to roughly 1,666 requests per 10-second window.

### Estimating your total API call count before you start

Use this formula to project total API calls and expected migration time before writing a single line of code:

```
Total calls = (tickets × 2)                        // create + status update
            + (tickets × avg_replies × 1)           // reply parts
            + (contacts × 2)                        // search + create
            + (companies × 1)                       // create
            + (attachments × 2)                     // download + re-upload (external, not API)
            + (KB articles × 1)                     // create article
```

**Example:** 10,000 tickets, average 5 replies each, 8,000 contacts, 500 companies, 2,000 KB articles:
- Tickets: 10,000 × 2 = 20,000
- Replies: 10,000 × 5 = 50,000
- Contacts: 8,000 × 2 = 16,000
- Companies: 500 × 1 = 500
- Articles: 2,000 × 1 = 2,000
- **Total: ~88,500 API calls**

At 1,600 requests per 10-second window with 30% overhead buffer: approximately 55 seconds of API time alone. In practice, including processing overhead, network latency, and rate-limit pauses, expect 20–40 minutes for this volume.

### Rate limit implementation

- **Build exponential backoff** — on a 429 response, retry after 1s, then 2s, 4s, 8s, up to a maximum of 60s.
- **Run batches sequentially within a single thread** — multi-threaded imports without a centralized rate-limit manager will cause cascading 429s.
- **Log every 429** with the full request payload so you can replay only failed requests.
- **Do not share your migration app's rate limit with live production traffic** — create a dedicated private app for the migration with its own rate limit allocation.

> [!NOTE]
> **Intercom test workspace rate limits differ from production.** Test workspaces have lower rate limits and data caps. Run your initial sandbox validation with a representative sample (500–1,000 tickets) rather than a full dataset, then extrapolate timing before committing to a full production run.

## What Cannot Be Migrated

Several Desk365 features have no Intercom counterpart:

- **Time Entries** — Desk365's time entries feature lets agents add notes, adjust time spent, and mark entries billable. Intercom has no time-tracking capability. Export as CSV for archival. If time tracking is operationally necessary, evaluate a dedicated time-tracking integration (Harvest, Toggl) connected to Intercom via Zapier or a custom Webhook.
- **Asset Management** — Desk365 supports asset management with lifecycle tracking, vendor management, and linking assets to users and tickets. No Intercom equivalent. If asset tracking is needed post-migration, consider a dedicated ITAM tool.
- **Approval / Change Management** — ITSM-specific workflows that must be rebuilt in a dedicated ITSM tool if still needed.
- **SLA Policies** — Must be manually recreated in Intercom's SLA settings. Configurations don't transfer via API. Document all Desk365 SLA rules before closing the account.
- **Automation Rules** — Desk365 automations are rule-based with time triggers. Intercom uses Workflows with a visual builder. Manual recreation required. Audit all active Desk365 automations and map them to Intercom Workflow equivalents before cutover.
- **Microsoft Teams Bot integration** — Desk365's Teams Agent Bot and Support Bot have no direct Intercom equivalent. Plan for a messenger transition period with end-user communication.

## Validation and Cutover Strategy

A migration is only successful if the data is trusted by the team using it.

### The sandbox run

Always perform a full migration against an Intercom test workspace first. Never execute your first run in production. Use a representative sample of 500–1,000 tickets that includes edge cases: tickets with 50+ replies, tickets with internal notes, tickets with attachments, tickets with custom field values at all levels, and tickets in each status.

### Validation checklist

After migration, validate systematically:

- [ ] **Contact count** — total contacts in Intercom matches Desk365 (minus intentional exclusions)
- [ ] **Company count** — all companies created with correct contact associations
- [ ] **Ticket count** — every migrated ticket exists in Intercom
- [ ] **Reply count per ticket** — spot-check 20+ tickets across different volumes (1 reply, 5 replies, 50+ replies)
- [ ] **Attachment integrity** — verify attachments render and are downloadable on at least 10% of attachment-bearing tickets
- [ ] **Timestamp accuracy** — replies appear in correct chronological order with original dates
- [ ] **Custom field values** — spot-check that mapped attributes contain correct values
- [ ] **Knowledge base articles** — all articles render correctly with images intact and correct collection assignment
- [ ] **Status mapping** — resolved/closed tickets show correct Intercom state
- [ ] **Tag preservation** — tags applied correctly to tickets and contacts
- [ ] **No phantom automation triggers** — verify no autoresponder emails were sent to historical contacts during import
- [ ] **Idempotency verification** — re-run the script against 50 already-migrated tickets and confirm zero duplicate records are created

### The delta sync

Because a migration takes time, your agents will continue working in Desk365 while the import runs. Build a **delta sync** script that runs on the final cutover weekend. This script queries Desk365 for tickets updated *after* the initial extraction timestamp and applies only the latest changes to Intercom. Without this step, you will lose any tickets created or updated during the migration window.

The delta sync query uses the same `/tickets` endpoint with an additional `updated_after` filter set to the Unix timestamp of your initial extraction. Run it within 24 hours of cutover to minimize the delta volume.

## Timeline and Effort Estimates

| Scenario | Tickets | Estimated Timeline | Build vs. Buy |
|---|---|---|---|
| Small team, simple fields | < 5,000 | 3–5 days | DIY feasible |
| Mid-size, custom fields, KB | 5,000–20,000 | 1–2 weeks | DIY possible, managed faster |
| Large, heavy attachments | 20,000–100,000 | 2–4 weeks | Managed recommended |
| Enterprise, multi-department | 100,000+ | 4–6 weeks | Managed strongly recommended |

The timeline multiplier is almost always attachments and custom field complexity, not raw ticket count. A 5,000-ticket migration with 50 custom fields and heavy attachments takes longer than a 50,000-ticket migration with default fields and no attachments. Use the API call estimation formula in Step 8 to size your specific migration before committing to a timeline.

## When to DIY vs. When to Get Help

**Build in-house if:**

- You have a developer who can dedicate 1–2 weeks
- Ticket volume is under 10,000
- You are not migrating attachments or are comfortable archiving them separately
- Custom fields are simple (text, single-level dropdowns — no multi-level)
- You have a staging Intercom workspace for testing
- Your Intercom contact count post-migration will not push you into a higher pricing tier

**Get help if:**

- You need zero data loss with full conversation history and attachments
- You have multi-level custom fields, complex department structures, or a large KB
- Your team cannot afford a week of engineering time on a one-time project
- You have already attempted a DIY migration and hit issues (more common than you would expect)
- You need a defensible audit trail for compliance purposes

We've completed migrations across helpdesk platforms, including Desk365 exports and Intercom imports. If you want to skip the trial-and-error phase, we can scope and execute the entire migration in days. Check out our [Intercom Migration Checklist](https://clonepartner.com/blog/blog/intercom-migration-checklist/) or our guide on [Zero-Downtime Help Desk Data Migration](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/) for more tactical details.

> Need a Desk365 to Intercom migration done right? Our team handles the full extraction, mapping, and import — including attachments, conversation history, and knowledge base. Book a free 30-minute scoping call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I export all my data from Desk365?

Desk365 offers CSV exports from the UI for ticket metadata and API v3 for full programmatic access including ticket descriptions in HTML and plain text. The CSV export does not include individual replies, attachments, or internal notes — use the API for complete data extraction.

### How long does a Desk365 to Intercom migration take?

For teams with under 5,000 tickets and simple fields, expect 3–5 days. Mid-size migrations (5K–20K tickets) take 1–2 weeks. Larger datasets with attachments and complex custom fields can take 2–6 weeks depending on scope.

### Does Intercom have a built-in import tool for Desk365?

No. Intercom does not offer a native Desk365 import. You need to extract data via Desk365's API or CSV export, transform it to match Intercom's data model, and import using Intercom's REST API (Contacts, Tickets, Articles endpoints).

### What Desk365 data cannot be migrated to Intercom?

Time entries, asset management records, approval/change management workflows, and SLA policies have no Intercom equivalent. These should be exported as CSV or JSON archives. Automation rules and Microsoft Teams bot configurations must be manually rebuilt.

### How do I keep historical timestamps when importing into Intercom?

If you import Desk365 tickets as Intercom Tickets, the reply endpoint supports a created_at parameter for preserving original dates. If you use Conversations instead, the creation timestamp cannot be backdated — store the original date in a custom attribute and prepend it to the message body.
