---
title: "Enchant to Desk365 Migration: A Technical Guide"
slug: enchant-to-desk365-migration-a-technical-guide
date: 2026-08-04
author: Raaj
categories: [Enchant, Migration Guide, Help Desk]
excerpt: "Technical guide for migrating from Enchant to Desk365. Covers API constraints, object mapping, label-to-category conversion, rate limits, and dependency order."
tldr: "Enchant to Desk365 migration requires API-based extraction (100 credits/min), careful label-to-category mapping, and awareness of Desk365's tight rate limits (100/hr on Standard). Plan for a transformation layer between the two data models."
canonical: https://clonepartner.com/blog/enchant-to-desk365-migration-a-technical-guide/
---

# Enchant to Desk365 Migration: A Technical Guide


# Enchant to Desk365 Migration: A Technical Guide

Moving from [Enchant](https://clonepartner.com/help-desk-data-migration/enchant) to [Desk365](https://clonepartner.com/help-desk-data-migration/desk365) means transitioning from a lightweight shared inbox to a structured, Microsoft 365-integrated helpdesk with custom fields, SLAs, company management, and ticket types. The data models differ enough that a direct lift-and-shift won't work — you need a transformation layer between the two systems.

As noted in our [FuseDesk to Enchant migration guide](https://clonepartner.com/blog/blog/fusedesk-to-enchant-migration-a-technical-guide/), Enchant is built around the shared inbox philosophy: minimal, fast, and reliant on labels and folders rather than strict relational data models. Desk365 enforces strict relationships between Agents, Contacts, Groups, and Tickets, with custom fields and ticket types driving routing, SLAs, and reporting. You're moving from a flat data structure to a highly relational one. A naive API script that loops through Enchant tickets and pushes them to Desk365 will lose conversation history, break agent attribution, and orphan attachments.

Desk365 supports both CSV import (via `/api/v3/tickets/import`) and API-based ticket creation. The CSV importer handles basic ticket properties and contact imports but does not reconstruct full conversation threads with replies, notes, and attachments. For any migration that needs to preserve history, the API is the only viable path.

This guide covers the object mapping, [dependency order](https://clonepartner.com/blog/blog/freshdesk-to-enchant-migration-guide/), API constraints on both sides, error handling patterns, and the failure modes documented in real migrations. API details reflect Enchant API v1 and Desk365 API v3; verify against official documentation before running production migrations.

## Why Teams Move from Enchant to Desk365

The most common technical drivers:

- **Microsoft 365 integration.** Desk365 supports native Teams ticketing, Entra ID (Azure AD) agent sync, and a Power Automate connector. Enchant has no Microsoft 365 integrations.
- **Custom fields and structured data.** Enchant has no custom fields on tickets — only flat labels for categorization and a single free-text `summary` field on customers. Desk365 supports custom ticket fields (dropdowns, text, checkbox, date, number, nested dropdowns), custom ticket types, and categories/subcategories with up to three levels of hierarchy.
- **Company-level management.** Enchant has no Companies entity — customers are standalone records. Desk365 has a full Companies object with associated contacts, company-specific SLA policies, and department-level management.
- **SLA enforcement.** Enchant has basic SLA support through rules. Desk365 has native SLA policies with configurable first response and resolution targets, business hours configuration, per-company SLA overrides, and breach escalation rules.

## Data Model Comparison: Enchant vs. Desk365

Understanding the structural gap is the most important pre-work. Every mapping decision flows from it.

| Enchant Object | Desk365 Equivalent | Notes |
|---|---|---|
| Inbox | Group | Enchant routes tickets to inboxes; Desk365 routes to groups. 1:1 mapping works for most setups. |
| User (agent) | Agent | Agents must exist in Desk365 before tickets reference them. |
| Customer | Contact | Enchant customers carry `first_name`, `last_name`, `summary`, and contacts. Desk365 contacts carry name, email, phone, title, company association, and custom fields. |
| Contact (email/phone/twitter) | Contact email / phone | Enchant supports multiple contact types per customer. Twitter handles have no direct Desk365 equivalent. |
| _(no equivalent)_ | Company | Desk365 has a dedicated Companies entity. You'll need to decide whether to create companies from email domains or skip this entirely. |
| Label | Category / Subcategory / Type / Custom Field | Enchant uses labels for everything. Desk365 has structured fields. A mapping table is required. |
| Ticket | Ticket | Core migration object. State and field mapping required. |
| Message (reply in/out, note) | Conversation (contact reply, agent reply, private/public note) | Structural transformation needed. |
| Attachment | Attachment | Must be downloaded from Enchant and re-uploaded to Desk365 via multipart form. |

### Ticket State Mapping

Enchant tickets have five possible states. Desk365 has four default statuses plus custom statuses:

| Enchant State | Desk365 Status | Notes |
|---|---|---|
| `open` | Open | Direct map |
| `hold` | Pending | Closest equivalent — both mean "waiting on something" |
| `closed` | Closed | Direct map |
| `snoozed` | Pending | Desk365 has no native snooze. Map to Pending, or create a custom "Snoozed" status. The `snoozed_until` timestamp will be lost unless stored in a custom field. |
| `archived` | Closed | Archived in Enchant is a terminal state. Map to Closed. |

### Priority Mapping

Enchant does not have a priority field on tickets. Desk365 uses numeric priority values: 1 (Low), 5 (Medium), 10 (High), 20 (Urgent). You'll need to either set a default priority for all migrated tickets or derive priority from Enchant labels (e.g., if you used a "VIP" or "Urgent" label in Enchant, map those to `20` in Desk365 via your label mapping table).

### Source Mapping

Enchant ticket types map to Desk365 source values:

| Enchant Type | Desk365 Source Value | Notes |
|---|---|---|
| `email` | 1 (Email) | Direct map |
| `chat` | 13 (Web Widget) | Closest match |
| `web` | 12 (Web Form) | Direct map |
| `phone`, `call` | 7 (Phone/Other) | Direct map |
| `sms`, `whatsapp`, `fb_messenger`, `instagram_dm`, `twitter`, `twitter_dm` | 7 (Phone/Other) | No direct Desk365 source for social/messaging channels |

> [!WARNING]
> **Social channel data loss risk.** Enchant supports tickets from Twitter, Facebook Messenger, Instagram DM, WhatsApp, and SMS. Desk365's source values don't include these channels natively. If preserving the original channel is important, store the Enchant `type` value in a Desk365 custom field (e.g., `cf_original_channel`) before migration.

## Pre-Migration Preparation

You cannot migrate data into Desk365 if the structural foundation doesn't exist. The following must be configured before running any migration scripts.

### Configure Groups and Agents

Desk365 groups need to mirror your Enchant Inboxes. If Desk365 groups are mapped to Microsoft Teams channels, configure that alignment before migration begins. All agents must be invited and active in Desk365 before ticket import starts — Desk365 rejects ticket creation if the assigned agent ID doesn't exist.

> [!WARNING]
> **Handling inactive agents:** If you have historical tickets assigned to agents who have since left the company, Desk365 will return a `422 Unprocessable Entity` error if you attempt to assign the ticket to a non-existent agent. Either create a "Migration Archive" dummy agent in Desk365 to hold these tickets, or map all inactive agents to a specific active administrator in your agent mapping table.

### Design Custom Fields, Types, and Categories

Review your Enchant labels. If you use labels like `Bug`, `Feature Request`, or `Billing`, don't dump these into Desk365 as plain tags. Create Ticket Types, Categories, Subcategories, or dropdown Custom Fields in Desk365. Your migration script needs transformation logic that reads the Enchant label array and maps it to the corresponding Desk365 field IDs.

### Disable Notifications and Automations

Desk365 has triggers and automations that fire when tickets are created or updated. Disable all outbound notifications, SLA timers, and webhooks in Desk365 before beginning the import. To disable: navigate to **Settings → Automations → Notification Rules** and deactivate all rules. Also disable **Settings → SLA Policies** and any active **Webhooks** under **Settings → Integrations**. Failing to do this will trigger thousands of email notifications to customers, informing them that their three-year-old tickets have been "created."

### Pre-Migration Checklist

- [ ] Audit Enchant data: total tickets, customers, attachments, labels, and any deleted/merged ticket records
- [ ] Confirm Desk365 plan tier — Standard (100 calls/hour) vs. Plus/Premium (50 calls/minute) — and upgrade if needed for your data volume
- [ ] Create all Groups in Desk365 (mapped from Enchant inboxes)
- [ ] Create all Agents in Desk365 with matching email addresses
- [ ] Design and create custom fields, ticket types, categories, and subcategories
- [ ] Build the label → category/type/custom field mapping table
- [ ] Set up SLA policies in Desk365
- [ ] Disable all outbound notifications, automations, SLA timers, and webhooks
- [ ] Initialize a local tracking database (SQLite recommended) with schema: `enchant_id`, `desk365_id`, `status`, `migrated_at`
- [ ] Run a test migration with 50–100 tickets to validate mapping logic
- [ ] Verify conversation threading, attachments, and contact associations in test data

## Migration Dependency Order

Dependencies between objects dictate the order you create them in Desk365. Get this wrong and your ticket imports will fail with missing reference errors.

1. **Agents** (Manual / Azure AD sync)
2. **Companies** (if applicable — create from email domains)
3. **Contacts** (from Enchant Customers)
4. **Tickets**
5. **Messages** (Replies and Notes, in ascending `created_at` order)
6. **Attachments**

## Extracting Data from Enchant

Enchant's REST API (v1) is your only extraction path. There is no bulk export feature.

### Authentication

Enchant uses bearer token authentication. Obtain an access token by installing the API app from your Enchant account settings.

```bash
curl -H 'Authorization: Bearer YOUR_TOKEN' \
  https://site.enchant.com/api/v1/tickets
```

### Rate Limits

Enchant's API is rate limited to **100 credits per minute** per account, across all endpoints, users, and tokens. There is also a **burst limiter of 6 requests per second**. Embedding related resources costs an additional credit per embed type — fetching a ticket with embedded messages costs 2 credits instead of 1.

Response headers provide real-time rate limit state:

```
Rate-Limit-Limit: 100
Rate-Limit-Remaining: 98
Rate-Limit-Used: 2
Rate-Limit-Reset: 20
```

For a 10,000-ticket migration, with each ticket requiring at minimum a list call plus an embed call for messages, plan for the extraction phase alone to take 4–8 hours under normal conditions.

> [!NOTE]
> **Rate limiting strategy:** Implement exponential backoff when receiving `429` responses. Read the `Retry-After` header if provided; otherwise back off exponentially (2s, 4s, 8s, 16s) on successive 429s. Fixed sleep timers are fragile — rate limit reset windows vary based on account activity.

### Enchant API Error Codes

| HTTP Status | Meaning | Action |
|---|---|---|
| `200 OK` | Success | Continue |
| `400 Bad Request` | Malformed request syntax | Log and skip; review payload |
| `401 Unauthorized` | Invalid or expired token | Refresh token; abort migration |
| `403 Forbidden` | Insufficient permissions | Check API app scope |
| `404 Not Found` | Resource deleted or merged | Log as skipped; continue |
| `429 Too Many Requests` | Rate limit exceeded | Exponential backoff; retry |
| `500 Internal Server Error` | Enchant-side error | Retry up to 3 times; then log and skip |

### Extracting Customers

Paginate through all customers. Default page size is 10; max is 100. For accounts with more than 10,000 customers, use `since_created_at` cursor-style pagination instead of `page` offset to avoid skipping records:

```bash
# Page-based (works up to ~10,000 records)
GET /api/v1/customers?per_page=100&sort=created_at&page=1

# Cursor-based (use for large datasets)
GET /api/v1/customers?per_page=100&sort=created_at&since_created_at=2024-01-15T12:00:00Z
```

Each customer includes embedded contacts (email, twitter, phone). Store the full response — you'll need contact values to look up or create Desk365 contacts, and the mapping of `Enchant customer_id → primary email` is needed for ticket creation.

### Extracting Tickets with Messages

Use the embed parameter to pull messages inline with each ticket:

```bash
GET /api/v1/tickets/TICKET_ID?embed=messages
```

For bulk extraction, first list all ticket IDs with minimal fields, then fetch each ticket individually with `embed=messages`. This two-pass approach allows checkpointing:

```bash
# Pass 1: Get all ticket IDs and metadata (2 credits per page of 100)
GET /api/v1/tickets?per_page=100&sort=created_at&since_created_at=2020-01-01T00:00:00Z

# Pass 2: Fetch full ticket with conversation (2 credits per ticket)
GET /api/v1/tickets/TICKET_ID?embed=messages
```

For accounts with more than 10,000 tickets, use `since_created_at` pagination. The `page` parameter stops working reliably beyond 10,000 records due to Enchant's cursor limitations.

> [!TIP]
> **Checkpoint your extraction.** Record each successfully fetched ticket ID in your local tracking database immediately after download. If rate limits or network issues interrupt the run, resume from the last checkpoint rather than restarting from zero.

### Handling Deleted and Merged Tickets

Enchant's API returns `404` for deleted or merged tickets. Your script must handle these gracefully: log the ticket ID as skipped with reason `"deleted_or_merged"`, increment a skip counter, and continue. Do not treat `404` as a fatal error during bulk extraction.

### Extracting Attachments

Attachments are nested inside message objects. Each attachment has an `id`, `name`, `size`, and `type`. Enchant stores attachments on AWS S3 and the API provides temporary signed URLs. These URLs expire — download attachments immediately after fetching the ticket, not in a separate deferred pass.

```json
{
  "id": 12345,
  "name": "screenshot.png",
  "size": 204800,
  "type": "image/png",
  "url": "https://s3.amazonaws.com/enchant-attachments/..."
}
```

Store attachments locally with their original filenames and MIME types. The signed URL will be invalid by the time you attempt to re-upload to Desk365 if you defer this step.

## Loading Data into Desk365

Desk365 offers two import paths: CSV import and the REST API v3.

### CSV Import: Contacts and Companies Only

Desk365's CSV import works reliably for bulk-loading contacts and companies. Download the sample CSV template from the Desk365 portal, populate it, and upload via **Settings → Import**. For tickets, CSV import creates basic ticket records but does **not** support importing conversation threads, attachments, or private notes. Use CSV only if migrating ticket metadata without history.

### API Import: Required for Full-Fidelity Migration

The Desk365 API v3 is the only path that supports tickets with full conversation history.

**Base URL:** `https://<yoursubdomain>.desk365.io/apis/v3/`

**Authentication:** API Key in the `Authorization` header.

```bash
curl -H 'Authorization: YOUR_API_KEY' \
  https://yoursubdomain.desk365.io/apis/v3/tickets
```

### Desk365 API Rate Limits

This is where many migrations stall. Desk365's rate limits are plan-specific:

| Plan | Monthly Price | Rate Limit |
|---|---|---|
| Standard | $12/agent/month | 100 API calls **per hour** |
| Plus | $20/agent/month | 50 API calls **per minute** |
| Premium | $32/agent/month | 50 API calls **per minute** |

> [!CAUTION]
> **The Standard plan limit of 100 calls per hour is prohibitive for any meaningful migration.** At that rate, loading 10,000 tickets would take over 100 hours of API time, excluding conversations and attachments. Upgrade to Plus or Premium before starting migration, and downgrade after completion. At 50 calls/minute (Plus/Premium), migrating 10,000 tickets with conversations still takes 12–20 hours.

### Desk365 API Error Codes and Retry Behavior

| HTTP Status | Meaning | Retryable? | Action |
|---|---|---|---|
| `200 OK` / `201 Created` | Success | — | Continue |
| `400 Bad Request` | Invalid field value or missing required field | No | Log payload; fix mapping; skip |
| `401 Unauthorized` | Invalid API key | No | Abort; verify key |
| `404 Not Found` | Referenced resource (agent, group, contact) doesn't exist | No | Fix dependency; retry after creating missing resource |
| `422 Unprocessable Entity` | Business logic violation (e.g., invalid agent assignment, duplicate contact email) | No | Log and handle per case |
| `429 Too Many Requests` | Rate limit exceeded | Yes | Exponential backoff with jitter |
| `500 / 503` | Desk365-side error | Yes | Retry up to 3 times with backoff |

A `422` response on ticket creation most commonly means: the assigned agent doesn't exist, the group name doesn't match exactly, or a required custom field is missing. Log the full response body — Desk365 returns a structured error object identifying which field failed.

### Contact Deduplication in Desk365

Desk365 uses email address as the primary deduplication key for contacts. If two Enchant customers share the same email address (which can happen if agents manually created duplicate customer records), the second `POST /contacts` call will return a `422` with a duplicate email error rather than merging the records. Handle this by:

1. Before migration, deduplicate Enchant customers by email address
2. Decide which record is authoritative (typically the one with more tickets)
3. Map all tickets from the duplicate customer to the primary customer's email

If Desk365 returns a `422` on contact creation due to duplicate email, treat it as a successful lookup — fetch the existing contact by email and use its ID for ticket association.

## Migrating Contacts

Create Desk365 contacts from Enchant customers via the API or CSV import. The contact's email is the primary identifier in Desk365 — it links contacts to tickets.

Map Enchant fields as follows:

| Enchant Customer Field | Desk365 Contact Field | Notes |
|---|---|---|
| `first_name` | First name | Direct map |
| `last_name` | Last name | Direct map |
| `contacts [].value` (type: email) | Email | Primary deduplication key |
| `contacts [].value` (type: phone) | Phone | Direct map |
| `summary` | Notes or custom field | No native Notes field; use a custom text field |
| `contacts [].value` (type: twitter) | Custom field `cf_twitter_handle` | No native Desk365 equivalent |

Store the mapping of `Enchant Customer ID → Desk365 Contact Email` in your local tracking database. You'll need this for every ticket creation call.

If you want to group contacts under companies, extract unique email domains from Enchant customers and create Desk365 Company records first. The API endpoint for company creation is `POST /apis/v3/companies`.

## Migrating Tickets

This is the most complex phase. You must fetch tickets from Enchant, transform the payload, and push them to Desk365 while preserving conversation history.

### Core Ticket Field Mapping

| Enchant Field | Desk365 Field | Transformation |
|---|---|---|
| `id` | `custom_fields.cf_enchant_ticket_number` | Store as string in a custom field for cross-reference |
| `subject` | `subject` | Direct string map |
| `state` | `status` | Map using state table above |
| `customer_id` | `contact_email` | Lookup from your customer→email mapping table |
| `user_id` | `agent_id` | Lookup from your agent mapping table |
| `type` | `source` | Map using source table above |
| `labels` | `custom_fields`, `category`, `type` | Apply label mapping table |
| `created_at` | `custom_fields.cf_original_created_at` | See timestamp note below |

A complete migration payload example:

```json
{
  "subject": "Original Enchant ticket subject",
  "contact_email": "customer@example.com",
  "description": "<p>Original first message body</p>",
  "status": "Closed",
  "priority": 5,
  "group": "Support",
  "type": "Question",
  "source": "1",
  "custom_fields": {
    "cf_enchant_ticket_number": "2209",
    "cf_original_channel": "email",
    "cf_original_created_at": "2021-03-14T09:22:11Z"
  }
}
```

### Timestamp Preservation

Desk365's API v3 sets `created_on` to the time the API call is made, not the original timestamp from the source system. As of the time of writing, there is no API parameter to override `created_on` during ticket or conversation creation. To preserve original timestamps:

1. Store `created_at` from Enchant in a custom date field (`cf_original_created_at`) on the ticket
2. Prepend the original timestamp to each conversation entry body: `[Original timestamp: 2021-03-14 09:22 UTC]`
3. For compliance or audit requirements, export Enchant ticket history to CSV as an archival record before account closure

This is not ideal, but it is the current constraint of the Desk365 API. Verify with Desk365 support before migration whether a `created_on` override parameter has been added to the API.

### Preserving Conversation History

Desk365 expects the initial customer message as the ticket `description`. In Enchant, use the first inbound message (`direction: in`) from the messages array as the description during ticket creation.

For the rest of the thread, iterate chronologically through the remaining Enchant messages in ascending `created_at` order and POST each to the Desk365 conversations endpoint (`POST /apis/v3/tickets/{ticket_id}/conversations`).

| Enchant Message | Desk365 Conversation Type | API Value |
|---|---|---|
| `type: reply`, `direction: in` | Contact reply | `"contact"` |
| `type: reply`, `direction: out` | Agent reply | `"agent"` |
| `type: note` | Private note | `"note"` |

> [!CAUTION]
> **Private note mapping is critical.** If you accidentally map Enchant internal notes (`type: note`) to agent replies instead of private notes, customers will see internal team discussions in their support portal. Test this mapping explicitly on your 50-ticket pilot before running the full migration.

### HTML vs. Plain Text

Enchant messages have a `htmlized` boolean field. When `htmlized: true`, the `body` field contains HTML. Desk365 conversation entries support both HTML (`body`) and plain text (`body_text`). Always check the `htmlized` flag:

```python
if message["htmlized"]:
    payload["body"] = message["body"]
else:
    payload["body_text"] = message["body"]
```

Passing HTML content to `body_text` will expose raw markup to customers. Passing plain text to `body` will double-encode characters.

### Idempotency and Crash Recovery

Neither Enchant nor Desk365 provides idempotent ticket creation — there is no `idempotency_key` parameter. If your script crashes mid-migration, a naive retry will create duplicate tickets. Prevent this with a local tracking database:

```sql
CREATE TABLE migration_log (
  enchant_ticket_id INTEGER PRIMARY KEY,
  desk365_ticket_id INTEGER,
  status TEXT,  -- 'pending', 'created', 'conversations_done', 'complete', 'failed'
  error_message TEXT,
  migrated_at TIMESTAMP
);
```

Before creating each ticket in Desk365, check whether `enchant_ticket_id` already exists in `migration_log` with `status = 'created'`. If it does, skip ticket creation and proceed to conversation migration using the stored `desk365_ticket_id`. This makes every phase of the migration resumable from the last checkpoint.

## Attaching Files to Desk365: Code Example

Attachments require a separate `multipart/form-data` upload to Desk365 after the conversation entry is created. Here is a working example in Python:

```python
import requests

def upload_attachment_to_desk365(ticket_id, conversation_id, file_path, 
                                   file_name, mime_type, api_key, subdomain):
    url = f"https://{subdomain}.desk365.io/apis/v3/tickets/{ticket_id}/conversations/{conversation_id}/attachments"
    
    with open(file_path, 'rb') as f:
        files = {'file': (file_name, f, mime_type)}
        headers = {'Authorization': api_key}
        response = requests.post(url, headers=headers, files=files)
    
    if response.status_code == 201:
        return response.json()
    elif response.status_code == 413:
        # File too large — log and append note to conversation
        raise FileTooLargeError(f"{file_name} exceeds Desk365 size limit")
    else:
        response.raise_for_status()
```

> [!CAUTION]
> **File size limits.** Desk365 enforces attachment size limits (typically 15–25MB depending on plan; verify in your account settings). The API returns `413 Request Entity Too Large` for oversized files. Your script must check `attachment ["size"]` before downloading from Enchant. If the file exceeds the limit, skip the upload and append a text note to the message body: `[Attachment stripped during migration: filename.ext (32.4MB) — exceeded size limit]`.

## Label-to-Category Mapping Strategy

This is the most design-intensive part of the migration. Enchant uses flat labels for everything — categorization, status tracking, priority, routing, and custom tags. Desk365 has structured fields: Category, Subcategory, Type, Priority, and custom fields.

### Mapping Process

1. **Export all unique labels** from your Enchant ticket history using `GET /api/v1/labels`
2. **Classify each label** into one of: Category, Subcategory, Type, Priority, Custom Field, or Discard
3. **Create the corresponding structures** in Desk365 before running migration
4. **Encode the mapping** in a configuration file your migration script reads at runtime

Example mapping table:

| Enchant Label | Desk365 Field | Desk365 Value | Desk365 API Field |
|---|---|---|---|
| Billing | Category | Billing | `category` |
| Refund | Subcategory | Refund | `sub_category` |
| Bug | Type | Problem | `type` |
| VIP | Custom field | VIP | `custom_fields.cf_tier` |
| Urgent | Priority | 20 | `priority` |
| Internal | Discard | — | — |

Tickets with multiple labels may map to multiple Desk365 fields simultaneously — that is expected. A ticket with labels `["Bug", "Billing", "VIP"]` would set `type: "Problem"`, `category: "Billing"`, and `custom_fields.cf_tier: "VIP"` in a single ticket creation payload.

## Handling Inline Images

Inline images (images embedded directly in email bodies) are stored as HTML `<img>` tags pointing to Enchant-hosted URLs. When you close your Enchant account, those URLs return 404, leaving broken images throughout your Desk365 ticket history.

To prevent this, your script must rewrite inline image URLs:

1. Parse the HTML body of every message using an HTML parser (BeautifulSoup in Python, Cheerio in Node.js)
2. Find all `<img src="...">` tags where `src` points to an Enchant or S3 domain
3. Download the image from the signed URL (immediately — it may expire)
4. Upload the image as an attachment to the Desk365 conversation entry
5. Replace the original `src` value in the HTML with the new Desk365 attachment URL

```python
from bs4 import BeautifulSoup
import re

ENCHANT_IMG_PATTERN = re.compile(r'enchant\.com|s3\.amazonaws\.com/enchant')

def rewrite_inline_images(html_body, ticket_id, conversation_id, api_key, subdomain):
    soup = BeautifulSoup(html_body, 'html.parser')
    for img in soup.find_all('img'):
        src = img.get('src', '')
        if ENCHANT_IMG_PATTERN.search(src):
            # Download from Enchant, re-upload to Desk365, update src
            image_data = download_from_url(src)
            new_url = upload_inline_image(image_data, ticket_id, 
                                          conversation_id, api_key, subdomain)
            img['src'] = new_url
    return str(soup)
```

This step adds significant processing time — budget an additional 30–50% on top of your base migration timeline for accounts with high image volume.

## Executing the Delta Migration

Migrations are rarely one-time script executions. Because your team needs to continue working during transition, execute in three phases.

### Phase 1: Historical Sync

Migrate all closed tickets older than a defined freeze date (e.g., everything closed more than 30 days ago). This moves the bulk of data without affecting daily operations. Depending on Desk365 API rate limits, this phase can take several days.

### Phase 2: Active Sync

Migrate remaining open tickets and recent history. Your team continues working in Enchant throughout this phase. New tickets created in Enchant during Phase 2 will be picked up in the delta sync.

### Phase 3: Cutover and Delta Sync

On cutover day:
1. Update DNS MX records and email forwarding rules to route incoming email to Desk365
2. Instruct agents to stop working in Enchant and switch to Desk365
3. Run a final delta sync — query Enchant for any tickets or messages with `updated_at` or `created_at` after your Phase 2 snapshot timestamp, and push them to the corresponding Desk365 tickets (using your `migration_log` table to match Enchant IDs to Desk365 IDs)

This approach guarantees no data loss during the DNS propagation window (typically 24–48 hours for TTL expiry).

## Validating the Migration

Never assume a `201 Created` response means the data is correctly structured in Desk365.

**Count validation:**
- Total non-spam, non-trash Enchant tickets = total Desk365 tickets
- Count by status: Enchant `closed` count should match Desk365 `Closed` count
- Contacts: Enchant customer count ≥ Desk365 contact count (may be lower if deduplication removed duplicates)

**Content validation:**
- Spot-check 20–30 tickets across open, closed, and pending states
- Verify conversation order is chronological
- Confirm private notes are not visible in the customer portal
- Test that attachments are downloadable
- Render complex HTML tickets and verify tables, bullet points, and inline images display correctly

**Systematic issues:** If you find a mapping error that affected more than a handful of tickets (e.g., all notes were created as agent replies), it is faster to wipe the Desk365 environment and rerun the corrected script than to patch data via the API. This is why the test pilot on 50–100 tickets is mandatory, not optional.

## Edge Cases and Failure Modes

**Multi-contact customers.** Enchant customers can have multiple email addresses. Desk365 contacts support multiple emails, but ticket creation requires a single `contact_email` as the primary key. Decide which email is primary before migration — typically the first email contact in the Enchant `contacts` array, or the one with the most tickets.

**Spam and trash tickets.** Enchant tickets have `spam` and `trash` boolean flags. These are filtered from the default ticket list API response but can be fetched explicitly. Most teams skip migrating these. Document your decision either way.

**Social handles as contacts.** Enchant contact type `twitter` has no Desk365 equivalent on the contact record. Store in a custom contact field (`cf_twitter_handle`) or accept the data loss. Log the count of Twitter contacts for your migration report.

**Tickets exceeding Enchant pagination limits.** The Enchant `page` parameter stops reliably paginating beyond 10,000 records. Switch to `since_created_at` cursor pagination before you hit this boundary. Set `sort=created_at&since_created_at=<last_seen_timestamp>` and advance the cursor with each page response.

**Desk365 bulk creation endpoint.** Desk365 API v3 does not expose a batch ticket creation endpoint — all ticket creation is serial, one ticket per API call. Plan your rate limit math accordingly: at 50 calls/minute on Plus/Premium, you have a ceiling of 3,000 tickets/hour before accounting for conversation and attachment calls.

**Enchant canned responses.** These are not accessible via the Enchant API and cannot be migrated programmatically. Export manually from the Enchant UI and recreate in Desk365. Use the migration as an opportunity to audit and remove outdated templates.

## Realistic Timeline Estimates

| Data Volume | Extraction (Enchant) | Transformation | Loading (Desk365 Plus/Premium) | Total |
|---|---|---|---|---|
| 1,000 tickets | 1–2 hours | 1–2 hours | 2–4 hours | 1 day |
| 10,000 tickets | 8–12 hours | 4–6 hours | 12–20 hours | 2–3 days |
| 50,000 tickets | 2–3 days | 1 day | 3–5 days | 1–2 weeks |

These estimates assume Plus or Premium plan (50 calls/minute) and include conversation and attachment loading. On the Standard plan (100 calls/hour), multiply the Desk365 loading time by approximately 30×. Accounts with high attachment volume or heavy inline image usage add 30–50% to loading time.

## What Not to Migrate

Just as we advise when [migrating from Enchant to Intercom](https://clonepartner.com/blog/blog/enchant-to-intercom-migration-guide/), some objects are better rebuilt from scratch in Desk365:

- **Canned responses.** Not accessible via Enchant's API. Recreate manually in Desk365 under **Settings → Canned Responses**. Use this as an opportunity to retire outdated templates.
- **Rules and automations.** Enchant's rule engine and Desk365's automation engine have different trigger models and condition structures. Rebuild from scratch based on current workflow requirements.
- **Satisfaction ratings.** Enchant and Desk365 use different CSAT survey frameworks — ratings are not structurally portable. Export historical satisfaction data from Enchant to CSV for archival before account closure.
- **Knowledge base articles.** If you use Enchant's knowledge base, migrate these separately. The Desk365 API v3 exposes endpoints for KB categories (`/kb/categories`), folders (`/kb/folders`), and articles (`/kb/articles`) — these can be scripted, but are outside the scope of the ticket migration.

For further reading on related migrations, see our guides on [Enchant to Deskpro](https://clonepartner.com/blog/blog/enchant-to-deskpro-migration-a-technical-guide/) and [how to export data from Desk365](https://clonepartner.com/blog/blog/how-to-export-data-from-desk365-methods-api-limits-portability/).

> Need help migrating from Enchant to Desk365? ClonePartner specializes in helpdesk data migrations. We handle extraction, transformation, loading, validation, and cutover coordination — contact us to discuss your data volume and timeline.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I migrate Enchant tickets to Desk365 using CSV import?

Desk365's CSV import handles basic ticket properties and contacts, but it cannot import full conversation threads, notes, or attachments. For a complete migration with history preserved, you need the Desk365 API (v3).

### What are the Desk365 API rate limits for migration?

Desk365 rate limits are plan-specific: Standard plan allows 100 API calls per hour, Plus and Premium plans allow 50 API calls per minute. The Standard plan limit is too restrictive for most migrations — upgrade to Plus or Premium during the migration window.

### How do Enchant labels map to Desk365 fields?

Enchant uses flat labels for all categorization. In Desk365, you map labels to structured fields: Category, Subcategory, Type, Priority, or custom fields. Build a mapping table before migration and create the corresponding Desk365 structures first.

### What happens to tickets assigned to agents who no longer work here?

Desk365 requires a valid agent ID for assignment. You must either create a dummy 'Migration Archive' agent profile in Desk365 to hold these historical tickets or map them to an active administrator's account.

### Does Desk365 preserve original ticket timestamps during migration?

By default, the Desk365 API sets created_on to the time of the API call, not the original Enchant timestamp. Check with Desk365 support about overriding timestamps, or store the original timestamp in a custom field or message body.
