Skip to content

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.

Nachi Nachi · · 28 min read
SolarWinds Service Desk to Enchant Migration: Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

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

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.json or 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:

  1. Create one Enchant inbox per support team or brand. Each inbox has its own forwarding email address.
  2. During extraction, tag each SWSD incident with its department/site.
  3. During loading, specify the inbox_id on the ticket creation payload to route it to the correct inbox.
  4. 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
Warning

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:

  1. 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."
  2. Flatten into customer summary. For customer-level custom fields (e.g., "contract tier," "account manager"), use Enchant's summary field on the customer record. Limited to a single text field per customer — format as structured text.
  3. 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 .json to endpoints)
  • Pagination: Offset-based with page parameter; up to 200 records per page via per_page; X-Total-Count header returns total record count
  • Date filtering: Use created_at or updated_at parameters for date-range queries. Syntax: created_at []=YYYY-MM-DDT00:00:00Z&created_at []=YYYY-MM-DDT23:59:59Z for 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 Requests response includes a Retry-After header.
  • Key endpoints:
    • GET /incidents.json — List incidents (paginated)
    • GET /incidents/:id.json — Single incident with comments, attachments
    • GET /users.json — List users/requesters
    • GET /hardwares.json — List assets
    • GET /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: page and per_page (max 100); for >10,000 records, use since_created_at instead 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=true costs an extra credit. A 429 response includes a Rate-Limit-Reset header (seconds until reset).
  • Key endpoints:
    • POST /customers — Create customer
    • GET /customers?email=x — Lookup customer by email (for deduplication)
    • POST /labels — Create label
    • GET /labels — List labels (for mapping)
    • POST /tickets — Create ticket (type must be email)
    • PATCH /tickets/:id — Update ticket (set state, labels, assignee)
    • POST /tickets/:id/messages — Create message on ticket
    • POST /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 token
    • 404 — Resource not found (invalid ticket/customer ID)
    • 422 — Validation error (e.g., missing customer_id, invalid state value). Response body includes an errors array with field-level details.
    • 429 — Rate limit exceeded. Rate-Limit-Reset header indicates seconds to wait.
Info

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).

Warning

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"]
Warning

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_body becomes the first Enchant message (inbound reply with direction: "in").
  • Iterate through SWSD comments in chronological order. If is_private is true, create an Enchant note. If false, create an Enchant reply.
  • Set the message direction based 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_at timestamps. 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_at override. 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 errors

UAT process

  1. Run a test migration against a staging Enchant instance (use a trial account — Enchant offers a 30-day free trial)
  2. Have 2–3 agents spot-check their assigned tickets, focusing on recent high-priority incidents
  3. Verify customer-facing ticket history reads correctly (no internal notes exposed)
  4. Confirm search returns expected results for known ticket subjects and customer emails
  5. Test that reply functionality works on migrated tickets (sends email to the correct customer)
  6. 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 hold and snoozed states

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.local catches 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.

More from our Blog

Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration
Help Desk

Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration

Ensure your help desk migration is a success with this comprehensive 20-point post-migration QA checklist. This expert guide details the 20 essential tests needed to validate your data integrity, system functionality, user-friendliness, and performance . Learn exactly how to check everything from ticket data, attachments, and knowledge base articles to critical workflows, automations, and integrations before you go live. This process is your final line of defense against lost tickets, broken workflows, and unhappy customers.

Raaj Raaj · · 8 min read