SolarWinds Service Desk to Enchant Migration: Technical Guide
Technical guide for migrating from SolarWinds Service Desk to Enchant. Covers API constraints, data mapping, custom field handling, rate limits, and the step-by-step process.
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
Migrating from SolarWinds Service Desk (SWSD) to Enchant means moving from an ITIL-centric service management platform to a streamlined shared inbox. The two systems are architecturally different — SWSD manages incidents, problems, changes, releases, assets, and a CMDB; Enchant manages tickets, messages, customers, and inboxes.
This is not a 1:1 helpdesk swap. You are flattening a relational ITSM database into a conversation-first model where custom fields, asset tracking, and multi-tier workflows do not exist. Every decision in this migration flows from that structural gap.
If you need the short version: export your SWSD incidents via the REST API, transform them into Enchant's ticket schema, create customers first, then load tickets with their message history. Everything else — problems, changes, assets, CMDB — either gets archived or stays behind because Enchant has no equivalent objects.
This guide covers how to plan the migration, map objects between the two platforms, handle API constraints on both sides, and avoid the pitfalls that break data integrity.
Why teams migrate from SolarWinds Service Desk to Enchant
The most common reasons:
- Cost reduction. SWSD carries enterprise ITSM licensing costs. If your team only uses basic ticketing, you are overpaying for incident-problem-change workflows you never touch. Enchant's pricing starts significantly lower per agent. (enchant.com)
- Simplicity. SWSD requires categorization, impact analysis, and state-machine approval workflows. Enchant removes that friction for teams that just need fast email-based support.
- Shared inbox workflow. Enchant is designed around the shared inbox pattern — email, chat, social — which maps well to customer support teams that do not need IT service management.
- Pivoting to external support. Companies shifting from internal IT support to external B2B/B2C customer support often find ITSM tools too rigid for client communication.
- Smaller team size. SWSD was built for IT departments with change advisory boards. A five-person support team does not need that weight.
The decision to migrate is almost always a scope reduction. If your team actively uses SWSD's CMDB, asset management, change workflows, or service catalog, Enchant is not a replacement — it is a different category of tool.
Core differences in data model
Understanding these structural differences early prevents failed migrations:
| Concept | SolarWinds Service Desk | Enchant |
|---|---|---|
| Record types | Incidents, Problems, Changes, Releases, Service Requests | Tickets (single type) |
| Categorization | Categories, Subcategories, Departments, Sites | Inboxes, Labels |
| People model | Users (agents), Requesters (end-users), Departments, Sites | Users (agents), Customers with Contacts |
| Asset management | Full CMDB with CIs, dependency mapping | None |
| Service catalog | Full service catalog with approval workflows | None |
| Custom fields | Extensive custom fields on all object types | Customer summary field only; no custom fields on tickets via API |
| Workflow states | Configurable statuses per category | open, hold, closed, snoozed, archived |
| SLA management | Full SLA with escalation rules, response/resolution targets per priority | Basic SLA (response time targets configurable per inbox) |
| Knowledge base | Built-in knowledge base | Built-in (separate from tickets; no import API) |
| API base | api.samanage.com |
site.enchant.com/api/v1 |
The biggest gap is custom fields. SWSD lets you define custom fields on incidents, problems, assets, and nearly every object type. Enchant's API exposes a single summary text field on customers and no custom fields on tickets at all. Any custom field data from SWSD either goes into the ticket body as a note, gets stored in the customer summary, or is archived outside Enchant.
Historical import vs. operational migration
This distinction matters and is easy to miss. Enchant offers a managed historical import that brings in old tickets for search. But imported tickets are read-only, excluded from reports, and not shown in live folders. (help.enchant.com)
If agents need to keep working migrated tickets after cutover, you need native Enchant tickets created through the API — not a historical import. That is the difference between an archive reference and an operational migration.
For most teams, the right approach is: use the API to create native tickets for active and recent work, and use Enchant's historical import (or an external archive) for everything older than your cutoff date.
Migration approaches
1. API-based migration (recommended)
How it works: Extract data from SWSD using the Samanage REST API (api.samanage.com), transform the JSON payloads to match Enchant's schema, and load via Enchant's REST API.
When to use: Any migration where you need to preserve ticket-to-customer relationships, message history, and attachments — and where tickets must remain editable in Enchant.
Pros:
- Full control over data transformation
- Preserves relationships between customers and tickets
- Can handle attachments (base64 encoded)
- Repeatable and testable
Cons:
- Enchant's rate limit is 100 credits/minute with a burst limit of 6 requests/second — large datasets take time
- SWSD documents API access limits of 100 calls/minute on the Essentials plan; higher-tier plans may differ (documentation.solarwinds.com)
- Requires custom code
- Attachment handling requires downloading from SWSD and re-uploading as base64 to Enchant
Scalability: Works for any size, but datasets over 10,000 tickets require careful pagination. Enchant recommends iterating using since_created_at instead of page numbers for large result sets.
2. CSV export/import (partial)
How it works: Export data from SWSD using CSV exports from index pages or reports, then use Enchant's API to load the transformed data.
When to use: Small archive-only projects or data auditing before a full migration. Not for production operational migrations.
Pros:
- Fast to get data out of SWSD
- No API token setup needed on the extract side
- Good for data auditing before a full migration
Cons:
- SWSD CSV exports are flat — no nested relationships (comments and attachments are not included)
- Enchant has no native CSV import feature; you still need the API for the load phase
- SWSD warns that exported CSVs differ from import templates, and cells exceeding 32,767 characters can break Excel workflows (documentation.solarwinds.com)
- Large exports can take hours; SWSD sends an email when complete
Scalability: Poor for anything beyond a few hundred records.
3. Third-party migration tools
How it works: Services like Help Desk Migration offer pre-built connectors for SolarWinds Service Desk. Enchant also offers its own managed historical import.
When to use: When a pre-built connector exists for both source and target and the migration scope is limited to standard objects.
Pros:
- No custom code
- Usually includes a mapping UI and demo migration
Cons:
- Enchant is rarely supported as a destination in mainstream migration tools
- Tool vendors focus on standard objects (users, contacts, tickets). SolarWinds-specific objects like assets, changes, problems, and CI dependencies are usually ignored (help-desk-migration.com)
- Per-record pricing can get expensive at scale
- Black-box transformation — hard to debug field-level mapping errors
Scalability: Medium. Limited by the tool's support for Enchant.
4. Custom ETL pipeline
How it works: Build a dedicated extract-transform-load pipeline with separate phases, intermediate storage (JSON files or a staging database), crosswalk tables, and idempotent operations.
When to use: Enterprise migrations with 50,000+ incidents, complex custom field mappings, or compliance requirements that mandate audit trails and rollback as first-class concerns.
Pros:
- Full audit trail and logging
- Idempotent — can re-run safely
- Intermediate storage for validation before loading
- Handles deduplication and relationship rebuilding
Cons:
- Significant engineering investment (typically 2–4 weeks for a senior engineer)
- Requires maintenance during the migration window
- Overkill for small datasets
Scalability: Best option for large datasets.
5. Middleware platforms (Zapier, Make)
How it works: Use automation platforms to sync records between SWSD and Enchant via their APIs.
When to use: Low-volume ongoing sync during a phased cutover, or post-cutover enrichment. Not for historical backfills.
Pros:
- No code
- Quick to set up
Cons:
- Extremely slow for bulk operations
- Per-task pricing makes large migrations expensive
- Limited error handling and retry logic
- Cannot handle complex transformations (flattening custom fields into notes, handling ITSM object collapse)
- No built-in rollback
Scalability: Only viable for very small datasets (under 500 records).
Migration approach comparison
| Approach | Data Fidelity | Complexity | Scale | Best For |
|---|---|---|---|---|
| API-based | High | Medium–High | Any | Most migrations; active ticket continuity |
| CSV export | Low | Low | Small | Data audit, archive reference |
| Third-party tools | Medium | Low | Medium | Standardized helpdesk moves (if Enchant supported) |
| Custom ETL | Highest | High | Large | Enterprise, compliance, multi-instance |
| Middleware | Low | Low–Medium | Tiny | Ongoing sync, delta during cutover |
Recommendations:
- Small team, <5,000 tickets: API-based migration with a simple script. One engineer, a few days.
- Enterprise, >50,000 tickets: Custom ETL pipeline with staging database and validation layer.
- Archive only: Use Enchant's managed historical import for searchable history, manually recreate only active tickets as native records.
- No engineering bandwidth: Managed migration service (from ClonePartner or similar providers).
- One-time + delta sync: Use the API for the historical bulk load, and middleware strictly for the delta sync period between initial migration and final cutover.
Pre-migration planning
Data audit checklist
Before writing any migration code, inventory what exists in SWSD and what needs to move:
- Incidents — Total count, status distribution, date range
- Service Requests — Separate from incidents in SWSD; both map to tickets in Enchant. Distinguish them post-migration using a label like
service-request. - Problems and Changes — Do you need these in Enchant? If yes, they become tickets with distinguishing labels (
problem,change). If no, archive. - Users (Agents) — Active agents who need accounts in Enchant
- Requesters (End-users) — Map to Enchant customers
- Custom fields — List every custom field, its type, and whether it is still in use. Typical SWSD instances have 10–40 custom fields across incidents and service requests; each one requires a mapping decision.
- Attachments — Count and total size; check for inline images vs. file attachments. Note: any individual file over 10 MB will need to be handled separately (see attachment size limits below).
- Knowledge base articles — Enchant has its own knowledge base but no import API. Article count determines whether manual recreation is feasible (under ~50 articles) or requires a script-assisted workflow (copy-paste with formatting adjustments).
- Assets and CMDB — These have no home in Enchant. Archive or move to a dedicated tool (Snipe-IT, Lansweeper, or similar).
- Tasks — As of recent SWSD releases, Task Management v2 has no API support, so task extraction needs early verification. Check whether your tasks are accessible via
GET /incidents/:id/tasks.jsonor only through the UI. (documentation.solarwinds.com) - SLA policies — Must be reconfigured manually in Enchant. Enchant supports response time SLAs per inbox but does not have resolution time SLAs or escalation tiers.
- Automations and workflows — Cannot be migrated; must be rebuilt in Enchant's automation engine.
Define migration scope
Decide explicitly what moves, what gets archived, and what gets dropped:
- Move: Active and recent incidents/service requests (last 12–24 months), all active requesters, active agents
- Archive: Closed incidents older than 24 months, problems, changes, releases, assets
- Drop: Spam, test data, auto-generated monitoring alerts (server down notifications), orphaned records
SWSD instances often contain thousands of auto-generated alerts from monitoring tools (Nagios, PRTG, SolarWinds Orion integrations). Exclude these to keep Enchant clean. Filter by category, requester email domain, or subject pattern during extraction.
Migration strategy
For most SWSD-to-Enchant migrations, a big bang approach works. The migration typically completes in a day or less for datasets under 50,000 tickets. Run it over a weekend, execute a delta sync for any tickets created during the migration window, validate Monday morning, and point your email routing to Enchant.
Phased migrations make sense for large multi-brand or multi-inbox teams. Incremental migrations work when you need a longer validation window or dual-running period.
Risk mitigation
Always have a rollback plan. Do not delete data from SWSD immediately. Retain a read-only admin license for at least 90 days post-migration. Export a full CSV and API dump of SWSD before starting and store it somewhere permanent (cloud storage with versioning enabled).
Data model and object mapping
This is the highest-risk section. Getting the mapping wrong means broken data.
Object-level mapping
| SolarWinds Service Desk | Enchant | Notes |
|---|---|---|
| Incident | Ticket | Primary migration target |
| Service Request | Ticket | Tag with label service-request to distinguish from incidents |
| Problem | Ticket (optional) | Tag with label problem; add note referencing linked incident IDs |
| Change | Ticket (optional) | Tag with label change; add note referencing linked incident IDs |
| User (Agent) | User | Map by email; create manually in Enchant before migration |
| Requester | Customer | Create with Contacts via API |
| Department | Inbox or Label | Use inboxes if departments map to distinct support teams; labels otherwise |
| Category / Subcategory | Label | Flatten into labels (e.g., category:network, subcategory:vpn) |
| Site | Label | No direct equivalent; use labels like site:nyc-office |
| Comment (public) | Message (reply) | Set direction based on author role |
| Comment (private) | Message (note) | Internal note |
| Attachment | Attachment | Upload as base64 via POST /attachments, link to message via attachment_ids |
| Custom field | Note on ticket or Customer summary | No custom field support in Enchant API |
| Asset / CI | None | Archive separately |
| Knowledge Base Article | Knowledge Base (manual) | No import API; recreate manually or via assisted workflow |
| Task | Note or archive | Task API support may be limited in Task Management v2 |
Multi-inbox migration topology
When your SWSD instance uses Departments or Sites to route tickets to different teams, map these to separate Enchant inboxes:
- Create one Enchant inbox per support team or brand. Each inbox has its own forwarding email address.
- During extraction, tag each SWSD incident with its department/site.
- During loading, specify the
inbox_idon the ticket creation payload to route it to the correct inbox. - Configure email forwarding so each support address (e.g.,
it-support@company.com,hr-support@company.com) points to its respective Enchant inbox forwarding address.
If your SWSD departments are purely organizational (not customer-facing), use labels instead of inboxes to avoid over-fragmenting your Enchant workspace.
Field-level mapping for Incidents → Tickets
| SWSD Field | Enchant Field | Transformation |
|---|---|---|
id |
(internal reference) | Store mapping in crosswalk table for relationship rebuilding |
name (incident title) |
subject |
Direct map |
state |
state |
Map: New/Assigned→open, On Hold/Awaiting Input→hold, Resolved/Closed→closed |
requester.email |
customer (lookup/create) |
Create customer with contact first |
assignee.email |
user_id |
Lookup by email in Enchant via GET /users |
category.name |
label_ids |
Create label via POST /labels, attach to ticket |
subcategory.name |
label_ids |
Create label via POST /labels, attach to ticket |
priority |
label_ids |
Enchant has no priority field; use labels like priority-high, priority-medium, priority-low |
impact / urgency |
label_ids or note |
Flatten impact/urgency matrices into simple priority labels or include in metadata note |
due_at |
Note | Enchant tickets have no due date field; include in metadata note |
created_at |
Note (metadata) | See callout below |
description / description_body |
First message body |
Create as inbound reply with direction: "in" |
comments |
Messages | Public comments → replies; private comments → notes |
custom_fields_values |
Note on ticket | Serialize as key-value pairs in note body |
Created timestamps: Enchant's API does not expose created_at as a writable field on ticket creation. Migrated tickets will show the migration date as the creation date, not the original SWSD date. Work around this by including the original creation date in a metadata note or the ticket subject line (e.g., [2023-03-15] Original Subject). (dev.enchant.com)
Handling custom fields
SWSD custom fields have no API equivalent in Enchant. Three options:
- Flatten into a note. Create an internal note on each ticket with all custom field values formatted as key-value pairs. Preserves the data but makes it unsearchable via Enchant's search. Best for ticket-level fields like "affected system" or "business unit."
- Flatten into customer summary. For customer-level custom fields (e.g., "contract tier," "account manager"), use Enchant's
summaryfield on the customer record. Limited to a single text field per customer — format as structured text. - Export and archive. Store custom field data in a separate system (spreadsheet, database, or JSON file) linked by original SWSD incident ID. Use this for compliance, reporting, or reference needs that Enchant cannot serve.
For teams with extensive custom field usage (20+ fields), option 1 combined with option 3 is typical: notes preserve context for agents, and the external archive preserves queryable data.
CRM-like objects
SWSD is ITSM-first. Enchant does not expose a CRM-style company, opportunity, or pipeline layer via its public API. If your SWSD instance tracks procurement requests or hardware upgrade opportunities via custom modules:
- Accounts / Departments / Sites → Use Enchant inbox segmentation, labels, customer summary, or sidebar apps. No first-class company object exists.
- Contacts / Requesters → Map directly to Enchant Customers with Contacts. Enchant supports multiple contacts (email addresses) per customer record. (help.enchant.com)
- Leads / Opportunities → Keep in your CRM. Do not force pipeline data into Enchant tickets.
- Activities / Tasks → Public comments become replies; private comments become notes; standalone tasks become notes or archive references.
Migration architecture and API constraints
Data flow
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ SolarWinds SD │────▶│ Transform Layer │────▶│ Enchant │
│ (Samanage API) │ │ (Python/Node) │ │ (REST API) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
Extract via - Schema mapping Load via
GET endpoints - Field flattening POST endpoints
JSON responses - Deduplication JSON bodies
Pagination - ID remapping Rate-limited
- Crosswalk table
- Staging DB/files
SolarWinds Service Desk API
- Base URL:
https://api.samanage.com - Auth:
X-Samanage-Authorization: Bearer <JWT>header. Generate a JSON Web Token in Setup → Users → Actions → Generate JSON Web Token. The token inherits the creator's permissions; resetting or deleting it breaks the integration. (documentation.solarwinds.com) - Format: JSON (append
.jsonto endpoints) - Pagination: Offset-based with
pageparameter; up to 200 records per page viaper_page;X-Total-Countheader returns total record count - Date filtering: Use
created_atorupdated_atparameters for date-range queries. Syntax:created_at []=YYYY-MM-DDT00:00:00Z&created_at []=YYYY-MM-DDT23:59:59Zfor a range. - Rate limits: Documented at 100 calls/minute on the Essentials plan. Higher-tier plans may differ. Build in exponential backoff regardless. A
429 Too Many Requestsresponse includes aRetry-Afterheader. - Key endpoints:
GET /incidents.json— List incidents (paginated)GET /incidents/:id.json— Single incident with comments, attachmentsGET /users.json— List users/requestersGET /hardwares.json— List assetsGET /categories.json— List categories and subcategories
- Example response (incident, abbreviated):
{
"id": 12345,
"name": "VPN connection failing",
"state": "Assigned",
"priority": "High",
"requester": {"email": "jane@company.com", "name": "Jane Doe"},
"assignee": {"email": "agent@company.com", "name": "Agent Smith"},
"category": {"name": "Network"},
"subcategory": {"name": "VPN"},
"created_at": "2024-01-15T09:30:00Z",
"description_body": "<p>Cannot connect to VPN since this morning...</p>",
"custom_fields_values": [
{"name": "Affected System", "value": "Corporate VPN"},
{"name": "Location", "value": "NYC Office"}
],
"comments": [
{"body": "Have you tried restarting the client?", "is_private": false, "created_at": "2024-01-15T10:00:00Z", "commenter": {"email": "agent@company.com"}},
{"body": "Escalating to network team", "is_private": true, "created_at": "2024-01-15T10:15:00Z", "commenter": {"email": "agent@company.com"}}
]
}Enchant API
- Base URL:
https://site.enchant.com/api/v1 - Auth:
Authorization: Bearer <token>(install the API app in Enchant to get the token) - Format: JSON
- Pagination:
pageandper_page(max 100); for >10,000 records, usesince_created_atinstead of page numbers to avoid offset-based pagination performance degradation - Rate limits: 100 credits/minute per account; burst limit of 6 requests/second. Using
count=truecosts an extra credit. A429response includes aRate-Limit-Resetheader (seconds until reset). - Key endpoints:
POST /customers— Create customerGET /customers?email=x— Lookup customer by email (for deduplication)POST /labels— Create labelGET /labels— List labels (for mapping)POST /tickets— Create ticket (type must beemail)PATCH /tickets/:id— Update ticket (set state, labels, assignee)POST /tickets/:id/messages— Create message on ticketPOST /attachments— Upload attachment (base64 data)GET /users— List users (agents)
- Example response (ticket creation):
{
"id": 67890,
"subject": "VPN connection failing",
"state": "open",
"type": "email",
"customer_id": 111,
"user_id": 222,
"label_ids": [1, 2],
"created_at": "2024-06-15T14:00:00Z",
"inbox_id": 5
}- Common error responses:
400— Malformed request body (check JSON syntax, required fields)401— Invalid or expired API token404— Resource not found (invalid ticket/customer ID)422— Validation error (e.g., missingcustomer_id, invalidstatevalue). Response body includes anerrorsarray with field-level details.429— Rate limit exceeded.Rate-Limit-Resetheader indicates seconds to wait.
Ticket type constraint: Enchant's API only allows creating tickets of type email. If SWSD contains phone, chat, or social media tickets, they will all become email type tickets in Enchant. Preserve the original channel as a label (e.g., channel:phone, channel:chat).
Attachment size limit: SolarWinds allows 25 MB per attached file in comments. (documentation.solarwinds.com) Enchant's documentation caps the total attachment size per message at 10 MB, and the API requires attachments to be uploaded before the message is created. Treat 10 MB total-per-message as the safe operating limit. Files exceeding this limit should be stored externally (e.g., cloud storage) with a link in the ticket note.
Rate limit math
At 100 credits/minute and assuming 3–4 API calls per ticket (create ticket, add metadata note, set state, possibly upload an attachment), you can migrate roughly 25–30 tickets per minute. A 10,000-ticket migration takes approximately 6 hours. A 50,000-ticket migration needs an overnight window (approximately 28–33 hours). Real-world throughput is typically 10–20% lower due to retries, attachment downloads, and validation pauses. Plan accordingly.
Label creation via API
Labels are central to the mapping strategy — they replace SWSD categories, subcategories, priorities, sites, and record type distinctions. Create all labels before loading tickets:
def ensure_labels(label_names, existing_labels):
"""Create any labels that don't already exist in Enchant.
Returns a dict of label_name -> label_id."""
label_map = {l["name"]: l["id"] for l in existing_labels}
for name in label_names:
if name not in label_map:
result = create_with_retry("labels", {"name": name})
label_map[name] = result["id"]
return label_map
# Fetch existing labels
existing = requests.get(
f"{ENCHANT_BASE}/labels",
headers=ENCHANT_HEADERS
).json()
# Build complete label set from SWSD data
all_label_names = set()
for incident in incidents:
if incident.get("category", {}).get("name"):
all_label_names.add(f"category:{incident['category']['name']}")
if incident.get("subcategory", {}).get("name"):
all_label_names.add(f"subcategory:{incident['subcategory']['name']}")
if incident.get("priority"):
all_label_names.add(f"priority-{incident['priority'].lower()}")
label_map = ensure_labels(all_label_names, existing)Step-by-step migration process
Step 1: Extract data from SolarWinds Service Desk
Pull all required records via the Samanage API. Paginate through all pages and store locally as JSON files.
import requests
import json
import time
import os
SWSD_TOKEN = "your-jwt-token"
BASE_URL = "https://api.samanage.com"
HEADERS = {
"X-Samanage-Authorization": f"Bearer {SWSD_TOKEN}",
"Accept": "application/json"
}
def extract_all(endpoint, output_file, date_filter=None):
"""Extract all records from a SWSD endpoint with pagination and rate limit handling.
Args:
endpoint: API endpoint (e.g., 'incidents')
output_file: Path to save JSON output
date_filter: Optional tuple of (start_date, end_date) in ISO format
"""
all_records = []
page = 1
params = {"per_page": 200}
if date_filter:
params["created_at[]"] = list(date_filter)
while True:
params["page"] = page
resp = requests.get(
f"{BASE_URL}/{endpoint}.json",
headers=HEADERS,
params=params
)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 30))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
continue
resp.raise_for_status()
data = resp.json()
if not data:
break
all_records.extend(data)
total = int(resp.headers.get("X-Total-Count", 0))
print(f"Extracted {len(all_records)}/{total} from {endpoint}")
if len(all_records) >= total:
break
page += 1
time.sleep(0.6) # Stay under 100 calls/minute
with open(output_file, "w") as f:
json.dump(all_records, f, indent=2)
print(f"Saved {len(all_records)} records to {output_file}")
return all_records
# Extract core objects
incidents = extract_all("incidents", "swsd_incidents.json")
users = extract_all("users", "swsd_users.json")
categories = extract_all("categories", "swsd_categories.json")For enterprise datasets (50,000+ records), extract in parallel using date-range filters to avoid deep pagination timeouts:
from concurrent.futures import ThreadPoolExecutor
import datetime
def extract_by_month(endpoint, year, month):
start = f"{year}-{month:02d}-01T00:00:00Z"
if month == 12:
end = f"{year+1}-01-01T00:00:00Z"
else:
end = f"{year}-{month+1:02d}-01T00:00:00Z"
return extract_all(
endpoint,
f"swsd_{endpoint}_{year}_{month:02d}.json",
date_filter=(start, end)
)Step 2: Transform data to Enchant schema
Map SWSD records to Enchant's schema. This is where the heavy lifting happens.
def transform_requester_to_customer(requester):
"""Transform a SWSD requester into an Enchant customer payload."""
summary_parts = []
if requester.get("department", {}).get("name"):
summary_parts.append(f"Department: {requester['department']['name']}")
if requester.get("site", {}).get("name"):
summary_parts.append(f"Site: {requester['site']['name']}")
if requester.get("role", {}).get("name"):
summary_parts.append(f"Role: {requester['role']['name']}")
# Include any custom fields on the requester
for cf in requester.get("custom_fields_values", []):
if cf.get("value"):
summary_parts.append(f"{cf['name']}: {cf['value']}")
return {
"first_name": requester.get("first_name", ""),
"last_name": requester.get("last_name", ""),
"summary": "\n".join(summary_parts) if summary_parts else "",
"contacts": [
{"type": "email", "value": requester["email"]}
]
}
def transform_incident_to_ticket(incident, customer_id_map, user_id_map, label_map):
"""Transform a SWSD incident into an Enchant ticket payload."""
requester_email = incident.get("requester", {}).get("email", "")
assignee_email = incident.get("assignee", {}).get("email")
messages = []
if incident.get("description_body"):
messages.append({
"type": "reply",
"direction": "in",
"from": requester_email,
"from_name": incident.get("requester", {}).get("name", ""),
"body": incident["description_body"],
"htmlized": True
})
# Build label IDs
ticket_label_ids = []
cat = incident.get("category", {}).get("name")
if cat and f"category:{cat}" in label_map:
ticket_label_ids.append(label_map[f"category:{cat}"])
subcat = incident.get("subcategory", {}).get("name")
if subcat and f"subcategory:{subcat}" in label_map:
ticket_label_ids.append(label_map[f"subcategory:{subcat}"])
priority = incident.get("priority")
if priority and f"priority-{priority.lower()}" in label_map:
ticket_label_ids.append(label_map[f"priority-{priority.lower()}"])
# Metadata note with original timestamps and custom fields
meta_lines = [
"--- Migrated from SolarWinds Service Desk ---",
f"Original ID: {incident['id']}",
f"Original Created: {incident.get('created_at')}",
f"Original Updated: {incident.get('updated_at')}",
f"Priority: {incident.get('priority', 'N/A')}",
f"Impact: {incident.get('impact', 'N/A')}",
f"Urgency: {incident.get('urgency', 'N/A')}",
f"Category: {incident.get('category', {}).get('name', 'N/A')}",
f"Subcategory: {incident.get('subcategory', {}).get('name', 'N/A')}",
f"State: {incident.get('state', 'N/A')}",
]
for cf in incident.get("custom_fields_values", []):
if cf.get("value"):
meta_lines.append(f"{cf['name']}: {cf['value']}")
return {
"type": "email",
"subject": incident.get("name", "No subject"),
"customer_id": customer_id_map.get(requester_email),
"user_id": user_id_map.get(assignee_email),
"label_ids": ticket_label_ids,
"messages": messages,
"_meta_note": "\n".join(meta_lines),
"_state": map_state(incident.get("state")),
"_swsd_id": incident["id"],
"_comments": incident.get("comments", []),
"_attachments": incident.get("attachments", [])
}
def map_state(swsd_state):
state_map = {
"New": "open",
"Assigned": "open",
"Awaiting Input": "hold",
"On Hold": "hold",
"Resolved": "closed",
"Closed": "closed"
}
return state_map.get(swsd_state, "open")Step 3: Load into Enchant
Load in strict dependency order: Users → Customers → Labels → Tickets → Messages → Attachments. Respect rate limits. Track progress for idempotent re-runs.
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
logger = logging.getLogger(__name__)
ENCHANT_TOKEN = "your-enchant-token"
ENCHANT_BASE = "https://yoursite.enchant.com/api/v1"
ENCHANT_HEADERS = {
"Authorization": f"Bearer {ENCHANT_TOKEN}",
"Content-Type": "application/json"
}
# Crosswalk file for idempotent re-runs
CROSSWALK_FILE = "crosswalk.json"
def load_crosswalk():
if os.path.exists(CROSSWALK_FILE):
with open(CROSSWALK_FILE) as f:
return json.load(f)
return {"customers": {}, "tickets": {}, "labels": {}}
def save_crosswalk(crosswalk):
with open(CROSSWALK_FILE, "w") as f:
json.dump(crosswalk, f, indent=2)
def create_with_retry(endpoint, payload, max_retries=3):
"""POST to Enchant with retry logic for rate limits and transient errors."""
for attempt in range(max_retries):
resp = requests.post(
f"{ENCHANT_BASE}/{endpoint}",
headers=ENCHANT_HEADERS,
json=payload
)
if resp.status_code == 429:
reset = int(resp.headers.get("Rate-Limit-Reset", 20))
logger.warning(f"Rate limited on {endpoint}. Waiting {reset}s...")
time.sleep(reset)
continue
if resp.status_code == 422:
logger.error(f"Validation error on {endpoint}: {resp.json()}")
return None
if resp.status_code in (200, 201):
return resp.json()
resp.raise_for_status()
raise Exception(f"Failed after {max_retries} retries: {endpoint}")
crosswalk = load_crosswalk()
# 1. Create customers (skip if already in crosswalk)
for requester in requesters:
email = requester["email"]
if email in crosswalk["customers"]:
continue # Already created in a previous run
payload = transform_requester_to_customer(requester)
result = create_with_retry("customers", payload)
if result:
crosswalk["customers"][email] = result["id"]
save_crosswalk(crosswalk)
logger.info(f"Created customer: {email} -> {result['id']}")
time.sleep(0.2) # Stay under 6 req/sec burst limit
# 2. Create tickets with initial message
for incident in incidents:
swsd_id = str(incident["id"])
if swsd_id in crosswalk["tickets"]:
continue # Already created
ticket_data = transform_incident_to_ticket(
incident, crosswalk["customers"], user_id_map, label_map
)
if not ticket_data["customer_id"]:
# Assign to placeholder for orphaned tickets
ticket_data["customer_id"] = crosswalk["customers"].get(
"deleted-user@migration.local"
)
logger.warning(f"Orphaned ticket SWSD-{swsd_id}: no matching customer")
ticket = create_with_retry("tickets", {
"type": ticket_data["type"],
"subject": ticket_data["subject"],
"customer_id": ticket_data["customer_id"],
"user_id": ticket_data.get("user_id"),
"label_ids": ticket_data.get("label_ids", []),
"messages": ticket_data.get("messages", [])
})
if not ticket:
logger.error(f"Failed to create ticket for SWSD-{swsd_id}")
continue
enchant_ticket_id = ticket["id"]
crosswalk["tickets"][swsd_id] = enchant_ticket_id
save_crosswalk(crosswalk)
# Add metadata note
create_with_retry(f"tickets/{enchant_ticket_id}/messages", {
"type": "note",
"body": ticket_data["_meta_note"],
"htmlized": False,
"user_id": admin_user_id
})
# Set final state (tickets are created as 'open' by default)
if ticket_data["_state"] != "open":
requests.patch(
f"{ENCHANT_BASE}/tickets/{enchant_ticket_id}",
headers=ENCHANT_HEADERS,
json={"state": ticket_data["_state"]}
)
logger.info(f"Migrated SWSD-{swsd_id} -> Enchant-{enchant_ticket_id}")
time.sleep(0.2)Step 4: Migrate attachments
Attachments require downloading from SWSD and re-uploading as base64 to Enchant. Upload the attachment first, then reference it in the message.
import base64
def migrate_attachment(swsd_attachment_url, enchant_ticket_id, filename, mime_type):
"""Download attachment from SWSD, re-upload to Enchant as base64."""
# Download from SWSD
file_resp = requests.get(swsd_attachment_url, headers=HEADERS)
file_resp.raise_for_status()
file_size = len(file_resp.content)
if file_size > 10 * 1024 * 1024: # 10 MB limit
logger.warning(
f"Attachment {filename} ({file_size/1024/1024:.1f} MB) exceeds "
f"Enchant's 10 MB limit. Skipping — store externally."
)
# Add a note with a reference instead
create_with_retry(f"tickets/{enchant_ticket_id}/messages", {
"type": "note",
"body": f"Oversized attachment not migrated: {filename} "
f"({file_size/1024/1024:.1f} MB). "
f"Original URL: {swsd_attachment_url}",
"htmlized": False,
"user_id": admin_user_id
})
return None
encoded = base64.b64encode(file_resp.content).decode("utf-8")
# Upload to Enchant
attachment = create_with_retry("attachments", {
"name": filename,
"type": mime_type,
"data": encoded
})
if not attachment:
logger.error(f"Failed to upload attachment {filename}")
return None
# Attach to ticket via a note
create_with_retry(f"tickets/{enchant_ticket_id}/messages", {
"type": "note",
"body": f"Migrated attachment: {filename}",
"htmlized": False,
"user_id": admin_user_id,
"attachment_ids": [attachment["id"]]
})
return attachment["id"]Attachment edge case: SWSD's API has been reported to lack endpoints for downloading certain attachment types (particularly asset attachments). If you hit this, you may need direct database access for on-premise instances or a support request for SaaS instances.
Step 5: Migrate comments as messages
SWSD separates the initial incident description from the comments array. In Enchant, the entire thread is a series of messages.
- The SWSD
description_bodybecomes the first Enchant message (inbound reply withdirection: "in"). - Iterate through SWSD
commentsin chronological order. Ifis_privateis true, create an Enchantnote. If false, create an Enchantreply. - Set the message
directionbased on whether the author is a requester (in) or an agent (out). Check the commenter's email against your agent list. - Preserve comment ordering: SWSD comments have
created_attimestamps. Sort by this field before creating messages in Enchant to maintain thread order.
def migrate_comments(swsd_comments, enchant_ticket_id, agent_emails):
"""Migrate SWSD comments to Enchant messages in chronological order."""
sorted_comments = sorted(swsd_comments, key=lambda c: c.get("created_at", ""))
for comment in sorted_comments:
commenter_email = comment.get("commenter", {}).get("email", "")
is_agent = commenter_email in agent_emails
if comment.get("is_private"):
msg_type = "note"
else:
msg_type = "reply"
payload = {
"type": msg_type,
"body": comment.get("body", ""),
"htmlized": True
}
if msg_type == "reply":
payload["direction"] = "out" if is_agent else "in"
if not is_agent:
payload["from"] = commenter_email
payload["from_name"] = comment.get("commenter", {}).get("name", "")
else:
payload["user_id"] = user_id_map.get(commenter_email, admin_user_id)
create_with_retry(f"tickets/{enchant_ticket_id}/messages", payload)
time.sleep(0.2)Step 6: Validate
After loading, run validation checks against both source and target. See the Validation section below.
Edge cases and challenges
Duplicate requesters. SWSD often has duplicate requesters if SSO was not strictly enforced, or if the same person has multiple email addresses (e.g., john@company.com and john.doe@company.com). Enchant uses email as the customer identifier. Deduplicate by primary email address during the transform phase. Enchant supports merging customer profiles and multiple contacts per customer — add secondary emails as additional contacts on the same customer record. (help.enchant.com)
Missing requester data. Some SWSD incidents may have deleted requesters. Enchant will reject the payload if no valid customer_id is provided (returns 422). Create a placeholder customer (deleted-user@migration.local) and assign orphaned tickets to it. Log every instance for manual review.
Multi-level relationships. SWSD links incidents to problems, problems to changes, changes to releases. These hierarchies do not exist in Enchant. Flatten them: migrate each as an independent ticket, add a note referencing the original parent ID (e.g., Migrated from SWSD. Original Problem ID: PRB-9942. Linked Incident IDs: INC-1234, INC-5678).
Inline images. SWSD incident descriptions may contain inline images as embedded HTML referencing SWSD-hosted URLs (e.g., <img src="https://api.samanage.com/attachments/...">). These URLs will break after migration or when SWSD access expires. During transformation, download each inline image, upload as an Enchant attachment, and rewrite the <img> src attributes in the HTML body to point to the new Enchant attachment URL.
Broken HTML in old tickets. Tickets from years ago often contain malformed HTML. Use a library like bleach (Python) or DOMPurify (Node) to sanitize HTML before sending to Enchant. Your transformation layer needs to handle — or at least not crash on — broken tags, missing closing elements, and non-standard character encodings.
API failures and idempotency. Network blips happen. Your script must have state management (a crosswalk file or database tracking which records have been processed) to resume exactly where it failed without creating duplicate tickets in Enchant. Use SWSD IDs as idempotency keys — before creating any record, check if the SWSD ID already exists in your crosswalk.
Channel type mismatch. If SWSD contains phone, chat, or social records, the Enchant API only creates email type tickets. Preserve the original channel as a label (e.g., channel:phone) so agents know the context.
Timezone handling. SWSD timestamps are in UTC. Ensure your transformation layer does not inadvertently apply timezone offsets when formatting dates for metadata notes.
Limitations and constraints
Be explicit with stakeholders about what Enchant cannot do that SWSD can:
- No custom fields via API. This is the single biggest constraint. All custom field data must be flattened into notes, customer summaries, or archived externally.
- No asset management. CMDB data stays in SWSD or moves to a dedicated tool.
- No problem/change/release management. ITIL workflows end at the migration. These can be migrated as tagged tickets, but the relational structure is lost.
- No service catalog. If you use SWSD's catalog with approval workflows, that capability is gone.
- Ticket type restriction. The API only allows creating tickets of type
email. - No
created_atoverride. Migrated tickets will show the import date. Include the original date in a metadata note. - Tight rate limits. 100 credits/minute with a 6 req/sec burst. For context, Zendesk's API allows 700 requests/minute on Team plans — Enchant's limit is roughly 7x stricter.
- Historical import is read-only. Enchant's official import creates tickets that are read-only, excluded from reports, and not shown in live folders. (help.enchant.com)
- No bulk delete API. If a migration run produces bad data, you cannot programmatically clean it up. Contact Enchant support or start with a fresh instance.
- Simplified categorization. Highly nested category/subcategory trees must be flattened into labels.
- Impact/urgency matrices must be reduced to simple priority labels.
- Enchant's API is unversioned. Breaking changes come with advance notice, but there is no version pinning. Pin your migration to a specific date window and test immediately before production runs.
Validation and testing
Do not assume a 200 OK response means the data is correct.
Record count comparison
| Object | SWSD Count | Enchant Count | Match? |
|---|---|---|---|
| Incidents → Tickets | |||
| Requesters → Customers | |||
| Comments → Messages | |||
| Attachments | |||
| Agents → Users |
For Enchant, count=true on GET endpoints returns totals but costs an extra rate-limit credit. Run count queries once during validation, not on every loop iteration.
Field-level validation
Sample 5–10% of migrated tickets (minimum 50 tickets). For each, verify:
- Subject matches original incident name
- Customer is correctly linked (email matches original requester)
- All comments are present and in correct chronological order
- Internal notes are not visible as customer-facing replies
- Attachments are downloadable and not corrupted
- State is correct (
open,hold,closed) - Labels match original categories, subcategories, and priority
- Metadata note contains accurate original timestamps and custom field values
Automated validation script
def validate_migration(crosswalk, swsd_incidents):
"""Compare source and target records for data integrity."""
errors = []
sample = swsd_incidents[:max(50, len(swsd_incidents) // 10)]
for incident in sample:
swsd_id = str(incident["id"])
enchant_id = crosswalk["tickets"].get(swsd_id)
if not enchant_id:
errors.append(f"SWSD-{swsd_id}: not found in crosswalk")
continue
resp = requests.get(
f"{ENCHANT_BASE}/tickets/{enchant_id}",
headers=ENCHANT_HEADERS
)
if resp.status_code != 200:
errors.append(f"SWSD-{swsd_id}: Enchant ticket {enchant_id} not found")
continue
ticket = resp.json()
if ticket["subject"] != incident.get("name", "No subject"):
errors.append(f"SWSD-{swsd_id}: subject mismatch")
expected_state = map_state(incident.get("state"))
if ticket["state"] != expected_state:
errors.append(f"SWSD-{swsd_id}: state {ticket['state']} != {expected_state}")
return errorsUAT process
- Run a test migration against a staging Enchant instance (use a trial account — Enchant offers a 30-day free trial)
- Have 2–3 agents spot-check their assigned tickets, focusing on recent high-priority incidents
- Verify customer-facing ticket history reads correctly (no internal notes exposed)
- Confirm search returns expected results for known ticket subjects and customer emails
- Test that reply functionality works on migrated tickets (sends email to the correct customer)
- Sign off before production migration
Rollback planning
Enchant does not have a bulk delete API. If the migration goes wrong, your options are:
- Delete the Enchant trial/instance and start fresh
- Contact Enchant support for a data wipe
- Maintain SWSD access until validation is complete — do not cancel your SWSD subscription until you have validated
Partial failure recovery: If a migration run fails at ticket 15,000 of 30,000, your crosswalk file tracks exactly which records were created. Re-run the script — it will skip already-created records and resume from where it stopped. For duplicate detection on the Enchant side, query GET /tickets?subject=<exact subject>&customer_id=<id> before creating.
Write a rollback plan before you switch mail flow. For a deeper dive on post-migration testing, see our Post-Migration QA Checklist.
Post-migration tasks
Rebuild automations
SWSD automation rules, workflows, and escalation rules do not migrate. Recreate the ones you need in Enchant using its automation and trigger features. Treat this as an opportunity to simplify — most teams find they were over-automating in SWSD. Focus on the 5–10 automations that handle 80% of your ticket routing.
Configure inboxes and routing
Set up Enchant inboxes to mirror the team structure you need. Map your support email addresses to the correct inboxes via email forwarding. Each Enchant inbox provides a unique forwarding address — configure your mail server or provider to forward to these addresses. (help.enchant.com)
User onboarding
Enchant's UI is significantly simpler than SWSD. Most agents ramp up in under a day. Focus training on:
- How labels replace categories, subcategories, and priorities
- Where custom field data now lives (metadata notes and customer summaries)
- How sidebar apps can surface external data (CRM, asset databases) in real time
- The knowledge base workflow (creating and linking articles)
- The difference between
holdandsnoozedstates
DNS and email cutover
Update your email forwarding to point incoming support emails at your Enchant forwarding addresses. Test this with a single non-critical mailbox first before switching all addresses. Monitor for:
- SPF/DKIM alignment issues on forwarded emails
- Email forwarding loops during the first 48 hours
- Emails landing in the wrong Enchant inbox
Monitor for data inconsistencies
Run daily spot-checks for the first two weeks. Watch for:
- Customers contacting support about missing ticket history
- Agents unable to find expected records
- Attachment download failures
- Reply emails not threading correctly with migrated tickets
- Label assignments that do not match expected categories
For a complete help desk data migration checklist, see our dedicated guide.
Best practices
- Back up everything first. Export a full CSV and API dump of SWSD before starting. Store it in versioned cloud storage (S3, GCS, Azure Blob) with a retention policy.
- Run test migrations. Never go straight to production. Use Enchant's 30-day free trial for your dry run. Run at least two full test migrations before the production cutover.
- Migrate in dependency order. Users → Customers → Labels → Tickets → Messages → Attachments. Every time. Breaking this order causes foreign key failures.
- Build idempotent scripts. If a migration run fails halfway, you should be able to re-run without creating duplicates. Use a crosswalk file keyed on SWSD IDs.
- Log every API call. For every request, log the endpoint, request payload, response status code, and the mapping between old and new IDs. You will need this during validation and debugging.
- Do not migrate what you do not need. If nobody has looked at tickets from 2019, export to CSV for compliance and move on. Set a clear cutoff date (e.g., last 18 months of active tickets).
- Validate incrementally. Validate the first 500 tickets before running the remaining dataset. Catch mapping errors early.
- Plan for the rate limit wall. At Enchant's 100 credits/minute, a 10K ticket migration takes ~6 hours. Schedule large migrations overnight or over a weekend.
- Create the placeholder customer early.
deleted-user@migration.localcatches all orphaned tickets. Review this customer's tickets after migration to reassign or archive. - Test email flow end-to-end before cutover. Send a test email to your support address after configuring forwarding. Verify it creates a ticket in the correct Enchant inbox with the correct customer association.
Making the right call
Migrating from SolarWinds Service Desk to Enchant is a simplification exercise. You are trading ITIL depth for inbox speed. The technical migration is straightforward once you accept that custom fields, assets, and multi-tier ITSM workflows will not survive the move.
The hard part is not the code — it is the decisions. Which data matters? How do you preserve custom field context without custom fields? What happens to your change management process? Get those answers before you write your first API call.
If you have been through helpdesk migrations before, the API-based approach described above is a weekend project for datasets under 10,000 tickets. If this is your team's first migration and you would rather not discover Enchant's rate limits and schema constraints the hard way, let us handle it.
Frequently Asked Questions
- Can I migrate custom fields from SolarWinds Service Desk to Enchant?
- No. Enchant's API does not support custom fields on tickets. The best workaround is to serialize custom field values into an internal note on each ticket, or store customer-level data in Enchant's customer summary field. Any data that doesn't fit must be archived externally.
- Can agents keep working tickets after Enchant imports them?
- Not via Enchant's documented historical import. Imported tickets are read-only, not visible in live folders, and excluded from reports. If agents need active-ticket continuity, you must create native Enchant tickets through the API or use a custom managed migration.
- Does Enchant support importing tickets with original creation dates?
- The Enchant API does not expose created_at as a writable field when creating tickets. Migrated tickets will show the import date. Work around this by including the original creation date in a metadata note or the ticket subject line.
- What are Enchant's API rate limits for data migration?
- Enchant enforces 100 credits per minute per account with a burst limit of 6 requests per second. At roughly 3-4 API calls per ticket, expect to migrate about 25-30 tickets per minute. A 10,000-ticket migration takes approximately 6 hours.
- How long does a SolarWinds Service Desk to Enchant migration take?
- For datasets under 10,000 tickets, expect 1-2 days including testing. Larger datasets (50,000+) may take 3-5 days due to Enchant's rate limits. Building and testing the migration script itself typically takes one engineer a few days for straightforward cases.

