Desk365 to Enchant Migration: The Complete Technical Guide
Technical guide to migrating from Desk365 to Enchant via API. Covers object mapping, custom field handling, rate limits, attachments, and common failure modes.
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
Desk365 to Enchant Migration: The Complete Technical Guide
Moving from Desk365 to Enchant is not a lift-and-shift. You are moving from a structured, Microsoft 365-integrated helpdesk — with custom fields, SLAs, companies, and ticket types — into a lightweight shared inbox built around simplicity and speed. The direction of this move, from a highly relational data model to a deliberately flat one, creates specific challenges around data loss, field compression, and identity mapping.
Enchant does not have native custom fields, company objects, ticket types, SLA policies, or categories. If those structures carry operational meaning in your Desk365 instance, you need a concrete plan for how to preserve, flatten, or discard that information before you write any migration code.
This guide covers the object mapping between both platforms, API extraction and loading, dependency order, rate limit constraints, attachment handling, and the edge cases that break naive migrations. API details reflect Desk365 API v3 and the Enchant REST API as of the guide's last review; verify against official documentation before running production migrations, as endpoint behavior and rate limits are subject to change.
Scope note: This guide addresses migrations requiring full conversation history, agent attribution, and ongoing editability of migrated tickets. If you only need read-only historical reference, see the Managed Import vs. API decision matrix section.
Why Teams Move from Desk365 to Enchant
The most common drivers:
- Simplicity over structure. Teams that don't use Desk365's SLAs, custom ticket types, or company-level routing find the overhead unnecessary. Enchant's shared inbox model removes that friction.
- Multi-brand support. Enchant supports multiple inboxes, knowledge bases, and messengers per account, each with independent branding — useful for teams managing several customer-facing brands from one platform.
- Channel consolidation. Enchant natively supports email, live chat, phone (via Twilio), SMS, WhatsApp, Facebook Messenger, Instagram DM, and Twitter DM in a single inbox view.
- Cost. For smaller teams, Enchant's per-user pricing can be significantly lower than Desk365's Plus or Premium tiers, especially when Microsoft 365 integration isn't needed.
It is not a universal win. If your current operation depends on Desk365 companies, department-level visibility, Watchers, Share To, or heavily customized intake forms, expect redesign work. Those concepts do not map 1:1 into Enchant. (help.desk365.io)
The Data Model Shift
Desk365 is a structured helpdesk. Every ticket has a mandatory association with a Contact and can be linked to a Company, Group, Agent, Category, Sub-Category, Type, Priority, Status, SLA, and any number of custom fields (prefixed cf_ in the API). Desk365 enforces relational integrity — tickets require valid contacts, groups map to teams, and companies act as organizational containers. (help.desk365.io)
Enchant is inbox-centric. Its core objects are Tickets, Messages, Customers, Contacts, Users (agents), Inboxes, and Labels. A ticket always belongs to an inbox. Customers carry a first_name, last_name, summary field, and one or more contacts (email, phone, or Twitter). There are no custom fields on tickets, no company object, no ticket types, and no SLA management. Categorization is handled entirely through labels. (dev.enchant.com)
The table below maps the core objects:
| Desk365 Object | Enchant Equivalent | Notes |
|---|---|---|
| Agent | User | Map by email. Create users manually in Enchant before migration. |
| Contact | Customer + Contact | Enchant splits identity (Customer) from channel (Contact). |
| Company | None | Flatten to customer summary or a label. No company object exists. |
| Group | Inbox | One Desk365 group → one Enchant inbox. Not always 1:1 — Enchant inboxes also carry channels, permissions, and language settings. |
| Ticket | Ticket | Only email type can be created via API. |
| Reply / Note | Message | Enchant messages are typed as reply (with direction: in or out) or note. |
| Attachment | Attachment | Must be base64-encoded and uploaded before linking to a message. |
| Category / Sub-Category | Label | Create labels in Enchant, map category names to label IDs. |
| Ticket Type | Label | Desk365 Type is a business classification (Incident, Problem, Request). Enchant type is the channel (email, chat, phone). Do not confuse them. |
Custom Field (cf_*) |
None | Append to first message body, store in customer summary, or discard. |
| SLA Policy | None | Not migrated. Enchant does not support SLAs. |
| Priority | Label | No direct priority field. Use labels (e.g., Priority: High). |
| Status | State | Map to open, hold, or closed. See status mapping below. |
| Watchers | Workflow redesign | Enchant's closest concept is following a ticket, which is not equivalent. Treat as a process decision. |
| Share To | Workflow redesign | No direct Enchant equivalent. Plan separately. |
Do not map Desk365 Type to Enchant type. Desk365 uses Type as a business classification (Question, Incident, Problem, Request). Enchant uses type as the channel (email, chat, phone, SMS, WhatsApp). If you copy values straight across, your target data is wrong on day one. Use Enchant labels for the business classification instead. (help.desk365.io)
Define Your Migration Scope
Before writing code, decide what moves and what doesn't.
Migrate via API:
- Customers and contacts
- Tickets with full conversation history (replies, notes, attachments)
- Labels (mapped from Desk365 categories, types, and priorities)
Create manually in Enchant:
- Inboxes (mapped from Desk365 groups)
- Users (agents) — Enchant does not expose a user creation endpoint in its public API
- Labels
- Canned responses / macros
- Knowledge base articles (as detailed in our Zendesk to Enchant migration guide, Enchant lacks a bulk import API for Help Center content)
- Automation rules (Enchant uses a different paradigm called Rules — see Automation Mapping below)
Cannot be migrated:
- SLA policies (no Enchant equivalent)
- Custom fields as structured, filterable fields (must be flattened or discarded)
- Company objects (flatten to customer summary or labels)
- Ticket forms and form configurations
- Time entries
- Surveys and satisfaction ratings
- Watchers and Share To (require workflow redesign)
- Original creation and modification timestamps (Enchant's API does not expose writable
created_atfields — see Timestamp Limitation)
Should you migrate all tickets? If your Desk365 instance has years of closed tickets, consider whether your team actually needs them in Enchant. Migrating only open, pending, and recently closed tickets (last 12–24 months) can cut migration time dramatically—a strategy we also recommend when migrating from Freshdesk to Enchant. Archive the rest as a CSV export or database dump for compliance reference.
Managed Import vs. API Decision Matrix
Enchant offers two paths for bringing in historical data: a managed import feature and the REST API. They are not interchangeable.
| Criterion | Managed Import | API Migration |
|---|---|---|
| Tickets editable after import | No — read-only | Yes |
| Appears in live folders | No | Yes |
| Included in reports | No | Yes |
| Can be run multiple times | No — one-time only | Yes |
| Attachment history | May be trimmed for large imports | Complete if migrated correctly |
| Suitable ticket volume | Low, historical-only datasets | Any volume requiring editability |
| Engineering effort | Low | High |
Use the managed import only for historical reference that does not need to be editable or reportable. If your agents need to continue working tickets after cutover — responding to customers, reassigning, or closing — those tickets must be recreated through the API.
The decision threshold in practice: if more than 5% of the tickets you plan to import are in an open or hold state at migration time, use the API path for the entire dataset. Mixing methods creates split inventory that is operationally confusing.
API Access and Constraints
Desk365 API v3
Desk365's v3 API is your primary extraction path. The base URL follows the pattern:
https://<<yoursubdomain>>.desk365.io/apis/v3/
Authentication uses a Bearer token generated from Settings → Integrations → API in the Desk365 admin portal:
Authorization: Bearer YOUR_DESK365_API_KEYKey constraints:
- Pagination: You can retrieve 30, 50, or 100 tickets per API call using the
ticket_countparameter. The maximum throughput is 10,000 tickets per hour. - Rate limits vary by plan: Standard plan allows 100 API calls per hour. Plus and Premium plans allow 50 API calls per minute. If you extract through the Microsoft connector instead of raw HTTP, Microsoft Learn documents a different throttle of 100 calls per 60 seconds per connection — that difference changes how aggressive your extraction can be. (help.desk365.io)
- Descriptions not included by default: The
include_descriptionparameter must be set to include ticket descriptions in list responses. The conversations endpoint does not include the ticket description — you must pull it from the ticket record separately. - Custom fields: All custom fields are returned with the
cf_prefix (e.g.,cf_department,cf_location).
Freeze Desk365 field dictionaries before export. Desk365 lets admins rename statuses and type choices, and those changes flow through existing tickets, activities, and automations. Late edits can invalidate your mapping sheet. (help.desk365.io)
Enchant REST API
Enchant's API base URL:
https://site.enchant.com/api/v1
Replace site with your Enchant helpdesk identifier. Authentication uses Bearer tokens:
curl -H 'Authorization: Bearer YOUR_TOKEN' https://site.enchant.com/api/v1/ticketsKey constraints:
- Rate limit: 100 credits per minute across the entire account, all endpoints, users, and tokens. Embedded resources and count queries consume additional credits, so validation jobs that repeatedly call
embed=customer,labels,messageswill throttle faster than flat reads. (dev.enchant.com) - Burst limit: 6 requests per second maximum.
- Pagination: 0–100 results per page, 10 by default. For datasets exceeding 10,000 records, iterate using
since_created_atinstead of page numbers. - Ticket creation: Only tickets of
type: "email"can be created via the API. Desk365 tickets originally created through Teams, web forms, or other channels will be recreated as email-type records. - Attachment upload: Attachments must be base64-encoded and uploaded individually via
POST /api/v1/attachmentsbefore being linked to a message byattachment_ids. - Customer deduplication: When creating a ticket, you can pass a
customerobject instead ofcustomer_id. Enchant will look up existing customers by contact value and create a new one if no match is found. - No API endpoints for inboxes, users, or labels. These must be created manually in the Enchant admin UI before your migration script runs.
Timestamp Limitation
Enchant's published API documentation does not expose writable created_at fields on tickets or messages. In testing, all migrated records carry the migration execution timestamp rather than the original Desk365 timestamp. This is the most operationally significant data loss in the migration — agents lose the ability to sort or filter tickets by original submission date.
Two mitigations:
- Body annotation (always do this): Append the original Desk365 creation date to the first message body as a visible HTML block (e.g.,
<p><em>Original submission date: 2022-11-14 09:32 UTC</em></p>). This preserves the information in a human-readable form regardless of what the API allows. - Confirmed API behavior: Contact Enchant support before committing to an API-only path to confirm whether private or partner API access exposes writable timestamp fields. Document the response. If writable timestamps are available under any access tier, this is worth negotiating before migration begins.
If preserving original timestamps is a hard compliance requirement, treat this as a blocker until confirmed. Do not assume the body annotation alone satisfies audit requirements.
Timestamp limitation: Enchant's published API docs do not document writable created_at fields on tickets or messages. All migrated records may carry the migration execution timestamp instead of the original Desk365 timestamp. Confirm with Enchant support before committing to an API-only path. As a fallback, append the original date to the message body as a visible annotation. (dev.enchant.com)
Migration Dependency Order
Enchant enforces referential dependencies during creation. You cannot create a ticket without knowing the Inbox ID and Customer ID. You cannot create a message without a valid User ID. Load data in this order:
- Inboxes — Create manually in Enchant admin, mapped from Desk365 Groups.
- Users (Agents) — Create manually. Match by email address. Build an agent-to-user ID mapping table.
- Labels — Create manually. One label per Desk365 category, type, priority level, and any company names you want to preserve.
- Customers — Create via
POST /api/v1/customers. Include all contacts (emails, phone numbers) in the initial payload. - Tickets — Create via
POST /api/v1/ticketswith the initial message. - Additional Messages — For replies and notes beyond the initial message, use
POST /api/v1/tickets/{id}/messages. - Attachments — Upload via
POST /api/v1/attachmentsfirst, then referenceattachment_idsin the message creation payload.
Maintain a local key-value store (Redis, SQLite, or a JSON file) during the migration to map legacy Desk365 IDs to new Enchant IDs.
Handling Inactive Agents
If a Desk365 agent is inactive or has left the company, Enchant still requires a valid user_id to attribute their historical messages. Two options:
- Pay for legacy seats in Enchant just to hold the data (cost-prohibitive at scale).
- Create a single "Legacy Agent" user in Enchant. Map all inactive Desk365 agents to this ID, and prepend their real name to the body of their historical messages (e.g.,
[Original Author: Jane Doe]).
The second approach is preferred in almost every case. The first option is practical only if fewer than 3–4 former agents have significant open ticket histories that active agents need to review in context.
Multi-Group Ticket Handling
Desk365 allows tickets to move across groups during their lifecycle — via manual reassignment, automation rules, or CC/forwarding patterns. Enchant's inbox model means a ticket exists in exactly one inbox at any given time. Moving a ticket to a different Enchant inbox automatically unassigns it from its current agent.
For tickets with a group-movement history, apply this rule: migrate the ticket to its final group at time of migration, not its origin group. The movement history is not reproducible in Enchant's inbox model. If the audit trail of group changes matters, include a structured note on the ticket listing each group transition with timestamps before closing the source record.
Field-Level Mapping and Transformation
Status Mapping
Desk365 uses configurable statuses. The defaults are Open, Pending, Resolved, and Closed. Enchant's writable ticket states are open, hold, and closed.
| Desk365 Status | Enchant State |
|---|---|
| Open | open |
| Pending | hold |
| Waiting on Customer / Waiting for Vendor | hold |
| Resolved | closed |
| Closed | closed |
| Custom statuses | Map to open, hold, or closed based on intent |
Enchant does not distinguish between "Resolved" and "Closed." If that distinction matters for your reporting, preserve the original Desk365 status as a label (e.g., status:resolved) before migration.
Build an explicit mapping configuration before writing your script:
{
"status_map": {
"Open": "open",
"Pending": "hold",
"Waiting for Vendor Reply": "hold",
"Resolved": "closed",
"Closed": "closed"
},
"type_to_labels": {
"Incident": ["incident"],
"Problem": ["problem"],
"Request": ["request"]
},
"preserve_in_note": [
"desk365_ticket_number",
"desk365_company",
"desk365_department",
"original_created_on",
"original_closed_on"
]
}This makes your mapping decisions reviewable and testable before any data moves.
Handling Custom Fields
Desk365 supports unlimited custom fields (cf_ prefix). Enchant has none. Three options:
Option 1: Labels. For boolean or dropdown custom fields (e.g., Tier: 1, Product: Mobile), create corresponding Labels in Enchant and attach them to the ticket during creation.
Option 2: Body injection. For text-heavy custom fields, dates, or data that doesn't fit as a label, inject an HTML block into the first message of the ticket. This ensures historical context is immediately visible to the agent opening the migrated ticket.
# Appending Desk365 custom fields to the Enchant message body
custom_fields = {
"cf_department": "Engineering",
"cf_product": "Widget Pro",
"cf_region": "EMEA"
}
cf_block = "\n".join([f"<b>{k.replace('cf_', '')}:</b> {v}" for k, v in custom_fields.items() if v])
original_body = ticket["description"]
enhanced_body = f"{original_body}<hr><p><em>Migrated Desk365 custom fields:</em></p><p>{cf_block}</p>"Option 3: Discard. If the custom field data has no operational value post-migration, don't migrate it. Document what was dropped.
Body injection preserves data visibility but makes it unsearchable. Labels are searchable but don't work well for free-text values. Most migrations use a combination of both — labels for structured dropdown fields, body injection for free-text and date fields.
Handling Companies
Desk365's Company object groups contacts by organization. Enchant has no equivalent. Options ranked by utility:
- Label-based: Create a label for each company (e.g.,
Company: Acme Corp) and apply it to all tickets from that company's contacts. This preserves filterability. - Customer summary: Append the company name, account tier, and contract IDs to each customer's
summaryfield. This is immediately visible when an agent opens a customer profile. - Sidebar app: For B2B teams with complex account data, consider using Enchant's sidebar app feature to load live company data from your system of record instead of storing it statically in the helpdesk.
- All of the above: Use labels for filtering, summary for at-a-glance context, and a sidebar app for live detail.
For most B2B migrations with 50+ companies, the combined approach (label + summary) is the minimum viable replacement. The sidebar app is worth the engineering investment if company data changes frequently post-migration.
Handling Priority
Desk365 has a structured priority field (Low, Medium, High, Urgent). Enchant does not. Map priorities to labels:
| Desk365 Priority | Enchant Label |
|---|---|
| Low | Priority: Low |
| Medium | Priority: Medium |
| High | Priority: High |
| Urgent | Priority: Urgent |
Automation Rule Mapping
Desk365 supports automations triggered by ticket events, time conditions, and field values. Enchant's equivalent is its Rules engine. The paradigms differ: Desk365 automations can trigger on SLA breach (which has no Enchant equivalent), while Enchant rules are event-driven and apply within a single inbox context.
The five most common Desk365 automation patterns and their Enchant equivalents:
| Desk365 Automation Pattern | Enchant Rule Equivalent | Notes |
|---|---|---|
| Auto-assign by group | Inbox-level assignment rule on ticket creation | Configure per inbox; no global cross-inbox auto-assignment |
| Priority escalation on keyword | Rule: when ticket subject/body contains keyword → apply label | Labels replace priority field; set Priority: Urgent label |
| SLA breach escalation | No equivalent | Enchant has no SLA engine. Replicate with a time-based rule if available, or replace with an external monitoring integration |
| Auto-close after N days inactive | Rule: when ticket has no activity for N days → close | Available in Enchant Rules; verify trigger availability in your plan |
| Status change notification to requester | Rule: when ticket state changes → send email | Enchant handles this natively via notification settings per inbox |
Automation rules must be rebuilt manually. Export your Desk365 automation list from Settings → Automations before migration and map each rule against this table. Flag any rule that depends on SLA breach or custom field values — those require redesign, not just rebuilding.
Extraction: Getting Data Out of Desk365
Two-Pass Extraction
Extract Desk365 data in two passes:
Pass one: Pull ticket-level data — subject, description, contact, company, group, status, type, category, subcategory, assignee, and every custom field you need. Also extract the full list of agents, groups, companies, and contacts.
Pass two: Pull conversation history (replies, notes) and attachment metadata for each ticket.
This split matters because Desk365's conversations endpoint does not include the ticket description. The description is a separate field on the ticket record. If you rebuild the first message from the conversations endpoint alone, you will lose the original ticket description. (apps.desk365.io)
If you use company or contact data operationally, capture the exact API labels early. Desk365's contact and company field docs note that API labels are used for updates through APIs and Entra sync, and once saved they cannot be edited. (help.desk365.io)
Extraction Order
- Agents / Groups — Extract first. You need these to map
assigned_toandgroupvalues to Enchant users and inboxes. - Companies — Extract if you plan to preserve company data as customer summaries or labels.
- Contacts — Extract all contacts with their associated company and custom contact fields.
- Tickets — Extract with descriptions (
include_descriptionparameter), custom field values, and all metadata. - Conversations — Extract replies and notes for each ticket.
- Attachments — Download all attachment files referenced in conversations.
CSV Export Limitations
Desk365's CSV export (from the ticket list view in the Agent Portal) exports ticket metadata — ticket number, subject, status, priority, contact info, custom fields. It does not include conversation threads (replies, notes, or attachments). For any migration that needs conversation history, the API is the only viable extraction path.
Desk365 does not support idempotency keys on extraction. If your script fails mid-run, track progress externally (e.g., store the last successfully extracted ticket ID in your state file) to avoid re-processing.
Store all extracted data in local staging (JSON files or a database) to decouple extraction from loading. This lets you re-run the load phase without hitting Desk365's rate limits again.
Throughput Benchmarks
Based on migrations using the Desk365 Standard plan API:
- Ticket metadata (Pass 1): ~100 list calls/hour × 100 tickets/call = ~10,000 ticket metadata records/hour.
- Conversation extraction (Pass 2): Each ticket requires at least 1 additional API call. A ticket with 10 conversation messages may require 2–3 calls depending on pagination. Realistic throughput: 2,000–4,000 fully hydrated tickets per hour on Standard plan.
- Desk365 Plus/Premium plan: 50 calls/minute = ~3,000 calls/hour theoretical, but conversation depth and attachment volume are the practical bottleneck. Expect 8,000–12,000 hydrated tickets per hour under ideal conditions.
- Enchant load side: At 100 credits/minute, a ticket with 5 messages and 3 attachments consumes approximately 9–11 credits (1 ticket create + 5 message creates + 3 attachment uploads + query overhead). Sustainable throughput: roughly 500–600 fully hydrated tickets per hour on the Enchant side.
Enchant's 100 credits/minute limit is the binding constraint on load throughput for any migration with significant conversation history or attachments. Plan accordingly — a 50,000-ticket migration with average attachment density will take 80–100 hours of load time. Extract everything first, then schedule the load phase across multiple days.
Loading: Getting Data Into Enchant
Create Customers First
Create Enchant customers via POST /api/v1/customers before creating tickets. While Enchant can auto-create customers during ticket creation if you pass a customer object, precreating gives you control over deduplication.
Desk365 supports contacts with multiple secondary emails. (help.desk365.io) Enchant customer profiles also support multiple email addresses and phone numbers. Migrate every verified contact point into one canonical customer record to avoid duplicate customer entries.
For company data: Desk365 has first-class companies and department-level structures. Enchant's customer model is lighter, so company name, account tier, and contract IDs typically belong in the customer summary field, labels, or a sidebar app.
Recreate Tickets and Conversations
The clean sequence for each ticket:
- Create the Enchant ticket in its final inbox with
type: "email", subject, customer, and assignee. - Upload each attachment to
POST /api/v1/attachments(base64-encoded). - Create replies and notes in chronological order via
POST /api/v1/tickets/{id}/messages, attaching the returnedattachment_idsto the correct message. - Patch the ticket to its final state and labels.
When creating messages, differentiate between public replies and internal notes:
- Map Desk365 public replies to Enchant messages with
direction: "in"(customer) ordirection: "out"(agent). - Map Desk365 internal notes to Enchant messages with type
note. - Inbound replies require
from_name,from(email address),direction: "in",body, andhtmlized. Missing any of these returns a 422 validation error. - Set
htmlized: trueif the message body contains HTML. Desk365 descriptions often include HTML formatting.
CC and BCC handling: Desk365 stores CC and BCC recipients on the ticket or individual message level. Enchant handles CCs at the message level. When migrating a public reply, extract the CC array from Desk365 and include it in the Enchant message payload to ensure secondary stakeholders stay looped in.
If your old Desk365 workflows reassign tickets across groups, design that carefully. In Enchant, moving a ticket to another inbox automatically unassigns it. The safe pattern is usually: choose the final inbox first, then apply the final assignee. (help.enchant.com)
Suppressing Outgoing Emails
When you POST a message to an Enchant ticket via the API, the system may attempt to email the customer.
You must explicitly disable email sending during migration. Check Enchant's API documentation for the current flag to suppress notifications on message creation. Failing to do this will result in customers receiving thousands of historical emails within minutes. This is the most operationally damaging failure mode in the entire migration.
Attachment Processing
Attachments must be physically moved from Desk365 to Enchant:
- Download: GET the attachment from Desk365. Pass your Desk365 Bearer token — attachments are authenticated.
- Encode: Base64-encode the file content.
- Upload: POST to Enchant's
POST /api/v1/attachmentswithname,type(MIME type), anddata(base64 string). - Link: Capture the returned attachment
idand include it in theattachment_idsarray when creating the corresponding message.
Unassociated attachments in Enchant are automatically deleted. Always create the message immediately after uploading its attachments. If your script fails between upload and message creation, those attachments are lost and must be re-uploaded. Design your retry logic to re-upload attachments on any message creation failure — do not assume the attachment still exists.
Rate limit impact: Each attachment upload consumes one API credit. A ticket with 5 attachments across 3 messages costs at minimum 9 credits (5 uploads + 3 message creates + 1 ticket create), before counting query overhead. At 100 credits per minute, attachment-heavy migrations will be the bottleneck. Pre-inventory your attachment volume from Desk365 before estimating migration duration.
The Inline Image Trap
Desk365 users frequently paste screenshots directly into the editor. These become inline images with <img> tags pointing to Desk365 URLs. If you push this HTML directly to Enchant, the images will break — the Desk365 URLs require authentication, and eventually your Desk365 instance will be decommissioned.
Parse the HTML body of every Desk365 message before pushing it to Enchant. The safe pattern is to download each inline image, upload it to Enchant as a standalone attachment, and replace the original src with a reference Enchant will render. The exact inline referencing method (CID vs. attachment URL) varies by platform implementation — confirm with Enchant support before finalizing this pattern:
const cheerio = require('cheerio');
async function processInlineImages(htmlBody, desk365Token, enchantAuth) {
const $ = cheerio.load(htmlBody);
const images = $('img');
for (let i = 0; i < images.length; i++) {
const img = images[i];
const src = $(img).attr('src');
if (src && src.includes('desk365.io')) {
// 1. Download from Desk365 (authenticated)
const fileBuffer = await downloadFromDesk365(src, desk365Token);
// 2. Upload to Enchant as a standalone attachment
// Returns an attachment ID and, depending on Enchant's implementation,
// either a public URL or a CID reference for inline use.
// Confirm the returned URL format with Enchant support before production.
const { attachmentId, attachmentUrl } = await uploadToEnchant(fileBuffer, enchantAuth);
// 3. Replace src with Enchant-hosted URL
$(img).attr('src', attachmentUrl);
}
}
return $.html();
}Additionally, run all Desk365 HTML through a strict sanitizer (DOMPurify or sanitize-html) to strip proprietary Microsoft classes, styles, and tags — Teams-originated replies in particular contain heavy XML namespaces (<o:p>, VML shapes, nested table structures) that break Enchant's UI rendering. Strip Microsoft-specific markup while retaining core formatting (bold, italics, lists, links).
Rate Limit Strategy
You are dealing with two independent rate limiters:
| Platform | Rate Limit | Burst Limit |
|---|---|---|
| Desk365 (Standard) | 100 calls/hour | Not documented |
| Desk365 (Plus/Premium) | 50 calls/minute | Not documented |
| Enchant | 100 credits/minute | 6 requests/second |
For most migrations, Enchant's rate limit is the binding constraint on the load side, while Desk365's Standard plan limit is the binding constraint on the extract side.
Recommendations:
- Extract all Desk365 data to local storage first, then load into Enchant as a separate phase. This decouples the two rate limiters.
- Use exponential backoff on 429 responses from both APIs.
- Enchant returns
Rate-Limit-RemainingandRate-Limit-Resetheaders — use them to throttle proactively rather than reactively. - Consider libraries like
axios-retry(Node.js) orTenacity(Python) for resilient HTTP clients. Configure them to retry only on429and5xxcodes while failing immediately on401or404. This prevents your script from endlessly retrying a request for a permanently deleted resource.
Error Handling and Idempotency
Neither Desk365 nor Enchant provides native idempotency keys. Your migration script must handle:
- Duplicate detection. Before creating a customer, check if one already exists by querying
GET /api/v1/customers?contacts.type=email&contacts.value={email}. Enchant's auto-deduplication during ticket creation also helps, but precreating customers gives you more control. - Partial failure recovery. Track every successfully created Enchant record (customer ID, ticket ID, message ID) in a local state file or database. If the script crashes, resume from the last successful record.
- Validation errors. Enchant returns 422 with specific error messages. Log the full response body and the source Desk365 record for manual review.
- Attachment orphaning. If a message creation fails after attachments were uploaded, those attachments are auto-deleted by Enchant. Re-upload them on retry.
- Dead-letter queue. Log all permanent failures to a dead-letter queue (a simple JSON file or database table). If a specific ticket fails due to a malformed email or corrupted attachment, the script should log the failure, the specific phase of failure (e.g., "failed during attachment upload" vs. "failed during ticket creation"), and the source Desk365 ticket ID, then move on rather than crashing the entire migration.
# Resumable migration with state tracking
import json
state_file = "migration_state.json"
state = load_state(state_file)
for ticket in desk365_tickets:
if ticket["id"] in state["completed_tickets"]:
continue
try:
enchant_ticket = create_enchant_ticket(ticket)
for message in ticket["messages"]:
attachment_ids = upload_attachments(message["attachments"])
create_enchant_message(enchant_ticket["id"], message, attachment_ids)
state["completed_tickets"].append(ticket["id"])
save_state(state_file, state)
except RateLimitError:
wait_for_reset()
continue
except ValidationError as e:
log_error(ticket["id"], e, phase="message_creation")
continueCommon Failure Modes
| Failure | Cause | Fix |
|---|---|---|
| 422 on ticket creation | Missing customer_id or type not set to email |
Ensure every ticket has a valid customer and type: "email" |
| 422 on inbound reply | Missing from or from_name |
Extract sender email and name from Desk365 conversation data |
| Orphaned attachments | Message creation failed after attachment upload | Re-upload attachments on retry; do not assume uploaded attachment persists |
| Duplicate customers | Same contact email created twice in parallel | Query-before-create or use customer object in ticket creation |
| Missing conversations | Used Desk365 CSV export instead of API | Always use API for conversation history |
| Missing ticket description | Pulled from conversations endpoint only | Description is a separate ticket field — pull it explicitly with include_description |
| Rate limit exceeded | Standard plan's 100 calls/hour is easy to hit | Extract to staging first, pace requests |
| Wrong inbox assignment | Group-to-inbox mapping not validated | Build and validate mapping table before migration |
| Broken inline images | Desk365 image URLs require authentication | Download and re-upload inline images; replace src before loading |
| UI rendering failure on Teams replies | Microsoft-proprietary HTML pushed directly to Enchant | Strip <o:p>, VML, and Microsoft XML namespaces before loading |
| Timestamp confusion | Migration timestamp appears on all records | Append original dates to message body as visible annotation |
| Inactive agent attribution failure | No valid Enchant user_id for departed agents |
Use Legacy Agent pattern; prepend original author name to message body |
What You Lose in the Migration
Be explicit with stakeholders about what doesn't survive:
- SLA history and policies — Enchant has no SLA engine.
- Custom field structure — Data can be preserved as text, but not as structured, filterable fields.
- Company hierarchy — No organizational grouping in Enchant.
- Original timestamps — Enchant's published API does not document writable
created_atfields; migrated records will show the migration execution date. - Ticket types as a structured field — Flattened to labels.
- Priority as a structured field — Flattened to labels.
- Time entries — No equivalent in Enchant.
- Surveys / satisfaction ratings — Not transferable.
- Automation rules — Must be rebuilt using Enchant's rules engine; see Automation Rule Mapping.
- Watchers and Share To — Require workflow redesign, not field mapping.
- Group movement history — Tickets are migrated to their final group; historical inbox reassignment trail is not reproducible.
This isn't a failure of the migration — it's the trade-off of moving to a simpler system. If any of these are non-negotiable, revisit whether Enchant is the right target. As we covered in our Enchant to Desk365 migration guide, the data model gap between these two platforms is real in both directions.
Pre-Migration Checklist and Validation
Before You Start
- Audit Desk365 data. Count tickets, contacts, companies, and attachments. Use the throughput benchmarks above to estimate migration time.
- Audit Desk365 automations. Export the full automation list. Map each rule against the Automation Rule Mapping table and flag those requiring redesign.
- Create Enchant account and configure inboxes. Map each Desk365 group to an Enchant inbox. Verify that Enchant has no sandbox/staging environment in your plan tier before running dry runs against production — if staging is unavailable, run dry runs on a small set of non-critical tickets first.
- Add users (agents) to Enchant. Match by email address. Build an agent ID mapping table. Create a "Legacy Agent" user for inactive agents.
- Create labels in Enchant. One label per Desk365 category, type, priority level, and any company names you want to preserve.
- Decide on custom field handling. Document which fields get appended to messages, stored in summaries, converted to labels, or discarded.
- Freeze Desk365 field dictionaries. No more renaming statuses or types until extraction is complete.
- Inventory integrations. Export the list of Desk365 webhooks, Power Automate flows, and third-party app connections. Map each to a Enchant webhook equivalent or identify integrations that require redesign. "Plan replacements" is not sufficient — identify the specific Enchant webhook events and payloads that correspond to each Desk365 trigger before cutover.
- Confirm timestamp behavior with Enchant support. If original timestamps are a compliance requirement, get written confirmation of available options before committing to the API path.
- Run a test migration. Migrate 10–20 tickets spanning each major group, status, attachment pattern, and form variant. Verify conversation threading, attachments, agent attribution, label assignment, and timestamp annotation.
Validation Checks Before Cutover
Do not stop at record counts. Check a sample across each mapped status, each major group, each form variant, and each attachment pattern:
- Description check: The original Desk365 ticket description is visible in the first message — remember the conversations endpoint does not include it.
- Attachment check: Every sampled file downloads from the message you expect. Inline images render without broken-image placeholders.
- Timestamp annotation check: Original creation and closure dates are visible in the message body for each sampled ticket.
- State check: Every Desk365 custom status collapsed into the intended Enchant state, with the original status preserved as a label where needed.
- Customer check: Secondary emails and phone numbers merged into the right Enchant customer instead of splintering into duplicates.
- Workflow check: Tickets that depended on Watchers, Share To, or department routing have a documented replacement workflow.
- Automation check: SLAs, routing rules, macros, and notifications are rebuilt in Enchant rather than assumed to migrate with the data. Test each rebuilt rule against a live ticket before cutover.
- Live reply check: Migrated open tickets can receive and send normal replies in Enchant without triggering notification suppression.
- Multi-group check: Tickets that moved across groups during their Desk365 lifecycle landed in the correct final inbox, with a structured note documenting their movement history.
The Path Forward
A good Desk365 to Enchant migration does not try to preserve every source construct literally. It keeps live work live, preserves the business meaning of statuses and categories, and makes the gaps explicit. Companies, Watchers, Share To, custom form logic, historical timestamps, and source ticket numbers all need a deliberate plan — documented in a mapping configuration before a single API call runs. Design those choices up front and the technical work becomes predictable. Skip that step and the API script is the easy part — the operational cleanup is what hurts.
Frequently Asked Questions
- Can I migrate Desk365 tickets to Enchant using CSV export?
- Only partially. Desk365's CSV export includes ticket metadata but not conversation threads, replies, notes, or attachments. For a full migration with conversation history, you must use the Desk365 API v3 for extraction and the Enchant REST API for loading.
- What happens to Desk365 custom fields when migrating to Enchant?
- Enchant does not support custom fields. You can append custom field values to the ticket's first message body as formatted text, store customer-level data in Enchant's customer summary field, convert boolean or dropdown values to labels, or discard the data. The structured, filterable nature of the fields is lost regardless.
- Does Enchant preserve original ticket timestamps during API migration?
- Enchant's published API docs do not document writable created_at fields on tickets or messages. Migrated records may carry the timestamp of when they were created during migration. Confirm with Enchant support if this is a hard requirement, or append original dates to message bodies as annotations.
- Does Desk365 Type map to Enchant type?
- No. Desk365 Type is a business classification (Question, Incident, Problem, Request). Enchant type is the channel (email, chat, phone, WhatsApp). Copying values straight across will corrupt your data. Use Enchant labels for the business classification instead.
- What are the API rate limits for Desk365 and Enchant during migration?
- Desk365 Standard plan allows 100 API calls per hour; Plus and Premium plans allow 50 calls per minute. Enchant allows 100 credits per minute with a burst limit of 6 requests per second. Extract Desk365 data to local staging first, then load into Enchant as a separate phase to decouple the two rate limiters.