Skip to content

LiveAgent to SolarWinds Service Desk Migration: Technical Guide

Technical guide to migrating from LiveAgent to SolarWinds Service Desk. Covers API extraction, ITIL object mapping, rate limits, and step-by-step loading.

Abdul Abdul · · 33 min read
LiveAgent to SolarWinds Service Desk 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

LiveAgent to SolarWinds Service Desk Migration: Technical Guide

Warning

Before you commit: LiveAgent is a multi-channel customer support platform with ticketing, live chat, and knowledge base. SolarWinds Service Desk (SWSD) is an ITIL-based ITSM platform with incidents, problems, changes, CMDB, and asset management. This is not a like-for-like swap — you are mapping a customer-facing support model into an IT service management framework. Every object, field, and relationship requires deliberate mapping decisions.

API versions referenced: LiveAgent API v3, SolarWinds Service Desk (Samanage) REST API v2.1. Regional API endpoints: US api.samanage.com, EU apieu.samanage.com, APAC apiau.samanage.com.

Migrating from LiveAgent to SolarWinds Service Desk moves your help desk data — tickets, contacts, companies, knowledge base articles, tags, chat history, and attachments — from a multi-channel customer support platform into an ITIL-aligned service management system built around incidents, problems, changes, and a CMDB. The core challenge is structural: LiveAgent organizes data around tickets and contacts; SWSD organizes data around incidents, users, and configuration items. Every migration decision flows from that architectural gap.

The short version: extract tickets and contacts from LiveAgent via the v3 REST API, transform them into SWSD's incident and user schema, load users first, then incidents with comments and attachments, and rebuild your knowledge base as Solutions in SWSD. Automations, SLA rules, chat widgets, and IVR configurations cannot be migrated and must be rebuilt manually. The data must be migrated in a strict hierarchy — Users → Departments → Incidents → Comments → Attachments — because SWSD enforces relational dependencies (e.g., an incident must have a valid requester).

Why Teams Migrate from LiveAgent to SolarWinds Service Desk

Organizations typically make this transition when their internal IT or HR operations outgrow a standard shared inbox or customer service tool. Common drivers:

  • ITIL alignment. Teams that started with LiveAgent for external support now need incident-problem-change workflows, CMDB, and asset management. SWSD provides the full ITIL lifecycle — incident → problem → change → release — with approval workflows and risk assessment.
  • Asset management and CMDB. LiveAgent has no asset tracking or CMDB. SWSD integrates asset discovery with service management, letting you link hardware and software to incidents.
  • Internal service catalogs. Moving from public-facing knowledge bases to structured internal employee service portals and formalized service request workflows.
  • Enterprise compliance. Organizations requiring formal change advisory boards, audit trails on configuration items, and SLA enforcement across multiple service providers often outgrow LiveAgent's scope.
  • Consolidation. Companies running LiveAgent alongside a separate ITSM tool consolidate into SWSD to reduce tool sprawl.

If your current LiveAgent account is acting like a shared inbox with ticket history, this move can make sense. If you mainly want a lighter support stack, it usually does not.

Core Data Model Differences

Understanding the structural gap between these two platforms is the single most important pre-migration step.

LiveAgent is a flat, ticket-centric system. The Companies feature manages multiple customers from the same organization, providing an additional layer of categorization. Companies are collections of Contacts, and each contact can be added to only one company. Tickets hold messages from email, chat, social, and phone channels in a single thread.

SolarWinds Service Desk is an ITIL-centric, relational system. Its core modules include Incidents, Problems, Changes, Releases, Solutions, Service Catalog, CMDB, Assets (Computers, Software, Printers, Mobile devices, Network devices, Other Assets), Tasks, Procurement, and Risk management. The CMDB stores up-to-date information about all technological components used by an organization and the relationships between those components, with each component defined as a Configuration Item (CI) often associated with other CIs.

LiveAgent Object SolarWinds Service Desk Equivalent Notes
Tickets Incidents / Service Requests Tickets map to incidents or service requests depending on type
Contacts Users (Requesters) SWSD users serve as both agents and requesters
Companies Departments, Sites, or custom field No direct "Company" object — see decision tree below
Agents Users (Agent/Admin roles) Role-based, not a separate object
Departments Groups LiveAgent departments map to SWSD groups for routing
Tags Categories (partial) Tags are free-form; SWSD categories are hierarchical
Knowledge Base Articles Solutions Different structure — Solutions use categories, not portals
Chat Transcripts Incident Comments Flattened into comment threads, losing real-time metadata
Custom Contact Fields Custom User Fields Must be manually created in SWSD first
Custom Ticket Fields Custom Incident Fields Type compatibility must be verified
Call Recordings Attachments on Incidents Audio files migrate as attachments, losing call metadata
Internal Notes Private Comments Preserved as private (agent-only) comments
Forums / Feedback No equivalent SWSD has no community forum feature

Migration Approaches

1. Native CSV Export/Import

How it works: LiveAgent allows you to export tickets in HTML, PDF, or CSV format. You can choose the ticket data columns you want to include in your export file. On the SWSD side, you can import users, incidents, and solutions via CSV templates, then use the API for anything the CSV path cannot handle.

When to use it: Small datasets under 5,000 tickets where you only need ticket metadata — no attachments, no chat transcripts, no multi-message threads.

Pros: Low engineering effort. Fast to pilot.

Cons: CSV export from LiveAgent captures ticket metadata but not individual messages, attachments, or chat histories. SWSD's CSV incident import only brings in text — attachments and screenshots are not imported directly. Sites and departments cannot be imported via CSV and must already exist. Updating custom fields via CSV is not supported. Excel breaks on long threads because cells over 32,767 characters can misalign rows. SWSD's CSV import supports only New and Closed as out-of-box states — custom states like Awaiting Input or On Hold require prior configuration and testing in a sandbox before relying on CSV for detailed status mapping.

Scalability: Small datasets only.

2. API-Based Migration (LiveAgent v3 → SWSD Samanage API)

How it works: Extract data programmatically from LiveAgent's REST API v3, transform it to match SWSD's schema, then load via the Samanage REST API. The LiveAgent API is accessible at https://YOURDOMAIN.ladesk.com/docs/api/v3/#/ with scopes for agents, tickets, and other objects.

Extraction endpoints:

  • GET /api/v3/tickets — paginated ticket list
  • GET /api/v3/tickets/{ticketId}/messages — individual messages
  • GET /api/v3/customers — contact records
  • GET /api/v3/companies — company records
  • GET /api/v3/agents — agent list
  • GET /api/v3/tags — tag list

Loading endpoints (SWSD):

  • POST https://api.samanage.com/users.json — create users
  • POST https://api.samanage.com/incidents.json — create incidents
  • POST https://api.samanage.com/incidents/{id}/comments.json — add comments
  • POST https://api.samanage.com/solutions.json — create KB articles

When to use it: Mid-size to large migrations (10,000–500,000+ records) where you need full fidelity — messages, attachments, relationships.

Pros: Full control over data transformation. Preserves message threads, attachments, and relationships. Supports delta sync and idempotent re-runs.

Cons: Requires engineering effort. Rate limits constrain throughput on both sides. Error handling and retry logic must be built. LiveAgent limits API requests to 180 per minute per API key and caps /tickets list calls at 10,000 records per filter, so large extracts must be windowed by date range.

Scalability: Enterprise-grade with proper batching and rate-limit management.

3. Third-Party Migration Tools

Tools like Help Desk Migration offer automated wizards for LiveAgent data export. These tools can run a LiveAgent CSV export or shift data to other platforms in a few clicks. However, SolarWinds Service Desk is not commonly supported as a target in most third-party migration tools because it is an ITSM platform, not a typical help desk. LiveAgent officially highlights Help Desk Migration/Relokia for migrations into LiveAgent, while SolarWinds documents a free migration tool specifically for Web Help Desk to SWSD — not for LiveAgent to SWSD.

When to use it: Only if the specific tool explicitly supports both LiveAgent as source and SWSD as target. Verify before purchasing.

Pros: Reduced engineering effort if supported.

Cons: Limited mapping flexibility. May not support SWSD-specific objects (problems, changes, CMDB). Ticket-centric tools often struggle when the target is an ITSM schema with sites, departments, solutions, assets, and custom states.

Scalability: Small to medium.

4. Custom ETL Pipeline

How it works: Build an extract-transform-load pipeline using Python, Node.js, or an ETL framework (Airflow, Prefect). Extract via LiveAgent API, stage in a local database (PostgreSQL, SQLite), apply transformations, then load into SWSD via the Samanage API.

When to use it: Large or complex migrations with custom business logic — e.g., splitting tickets by department, mapping tags to SWSD categories, merging duplicate contacts, multi-brand consolidations, or migrations where you need a permanent archive alongside a live target.

Pros: Maximum flexibility. Staging database enables validation before loading. Replayable. Best auditability.

Cons: Highest engineering investment. Requires maintaining the pipeline until cutover.

Scalability: Enterprise.

5. Middleware Platforms (Zapier, Make)

When to use it: Low-volume post-cutover sync between LiveAgent and SWSD during a transition period. Not for historical bulk migration. SolarWinds documents Zapier as an app-automation path, and LiveAgent's official Zapier support is limited to a small set of customer and conversation actions.

Pros: No-code setup. Good for triggering incident creation from new LiveAgent tickets.

Cons: Not designed for bulk historical migration. Zapier has execution limits and per-task pricing that makes large migrations cost-prohibitive. LiveAgent's documented Zapier trigger/action set is far too limited for bulk historical extraction. Weak control over ordering, attachment handling, and retries.

Scalability: Tiny for migration, acceptable for narrow ongoing sync.

Approach Comparison

Approach Best For Historical Fidelity Ongoing Sync Scalability Complexity
CSV Export/Import < 5K tickets, metadata only Low No Small Low
API-Based 10K–500K+ records, full data High Yes Enterprise High
Third-Party Tools Standardized ticket/contact moves Medium Sometimes Small–Medium Low–Medium
Custom ETL Enterprise, compliance, archive needs Highest Yes Enterprise High
Middleware (Zapier/Make) Post-cutover automation only Low Yes Small flows Low

Recommendations by Scenario

  • Small business (< 5K tickets, no dev team): CSV export for metadata plus SWSD API for loading, or a managed migration service.
  • Enterprise (50K+ records, complex data): API-based or custom ETL pipeline. Budget 4–8 weeks.
  • One-time migration: API-based with staged loading and validation.
  • Ongoing sync during transition: Middleware for new records plus batch API migration for historical data.
  • Multi-brand LiveAgent accounts: If you run multiple brands with separate inboxes, decide upfront whether to consolidate into one SWSD instance (using groups and categories for separation) or maintain logical boundaries via sites. Map each brand's department to a distinct SWSD group, and tag migrated incidents with the source brand in a custom field to preserve reporting segmentation.

When to Use a Managed Migration Service

DIY migrations between platforms with fundamentally different data models carry real risks:

  • Broken relationships. Loading incidents before creating their associated users results in orphaned records. Loading order matters, and it is easy to get wrong.
  • Data loss from schema mismatches. LiveAgent's multi-channel ticket threads (email + chat + call in one ticket) must be flattened into SWSD incident comments. Losing message ordering or attribution breaks audit trails.
  • Rate-limit cascades. LiveAgent's API rate limit is 180 requests per minute per API key. SWSD enforces its own per-plan rate limits. A naive script hitting both limits turns a 2-day migration into 2 weeks.
  • Hidden engineering cost. What looks like a weekend project becomes a 6-week effort once you account for pagination handling, attachment download/upload, error retries, deduplication, and validation. If the script fails halfway through, rolling back a partial SWSD import is a manual, error-prone process.

If your team's engineering bandwidth is better spent on core product work, a managed migration service handles the full pipeline — extraction, transformation, loading, relationship rebuilding, and validation. ClonePartner runs help-desk-to-ITSM migrations as a managed service. If that fits your situation, see how we run migrations.

Pre-Migration Planning

Do not pull data before defining what actually needs to move. A messy LiveAgent instance will result in a messy SWSD instance.

Data Audit Checklist

Before writing any migration code, inventory your LiveAgent data:

Object What to Count What to Check
Tickets Total count, by status, by department Active vs. resolved, spam volume
Contacts Total, duplicates, incomplete records Missing emails, orphaned contacts
Companies Total, contacts per company Companies with no linked contacts
Agents Active vs. deactivated Role assignments
Tags Total, usage frequency Unused tags (candidates for cleanup)
Knowledge Base Article count, categories Internal vs. public, outdated articles
Chat Transcripts Volume, date range Whether chat-only tickets exist (no associated email)
Attachments Total size, count Files > 25 MB that will hit SWSD upload limits
Custom Fields Contact fields, ticket fields Data types, picklist values, usage rates
Brands Count, separate inboxes/domains Whether brands map to SWSD groups or sites

Define Migration Scope

Not everything needs to move. Common exclusions:

  • Spam tickets — Filter these out before extraction.
  • Resolved tickets older than 2–3 years — Consider archiving instead of migrating.
  • Test/internal tickets — Clean up sandbox data.
  • Unused custom fields — Audit field usage before recreating in SWSD.
  • Forum posts and feedback boards — SWSD has no equivalent. Archive separately.
  • Deleted tickets — Often better archived than migrated into an ITSM target.

For guidance on what to keep and what to leave, see our Help Desk Data Migration Playbook.

Cutover Strategy

Choose a cutover pattern early:

  • Big bang: Migrate everything in a single cutover window. Works for small datasets (< 20K records). Requires a maintenance window.
  • Phased: Migrate by department, region, or date range. Reduces risk but requires running both systems in parallel.
  • Incremental (delta): Migrate historical data first, then do a delta sync of records created/modified between the initial migration and cutover. Best for large datasets where downtime is unacceptable.

For zero-downtime approaches, see our guide on zero-downtime help desk migration.

Data Model and Object Mapping

The mapping decisions here determine whether the migration is useful after go-live.

Tickets → Incidents

LiveAgent tickets become SWSD incidents or service requests. The mapping decision depends on your ITIL categorization:

  • Support issues, bug reports, outage reports → Incidents
  • How-to questions, access requests, hardware requests → Service Requests (via the SWSD Service Catalog)

In SWSD, tickets fit under one of two classifications: incidents or service requests. An incident is any unexpected interference to a service, disrupting normal operations. A service request is a request from a user for information, advice, a standard change, or access to a service.

LiveAgent ticket statuses (New, Open, Answered, Resolved, Postponed) must map to SWSD incident states (New, Assigned, Awaiting Input, Resolved, Closed). Note that Awaiting Input and On Hold are not default SWSD states in all configurations — verify they exist in your SWSD instance or create them as custom states before migration. LiveAgent's chat sessions, emails, and social media DMs all become standard incidents in SWSD — the platform has no native concept of a "WhatsApp" or "Facebook" channel ticket. Map the LiveAgent channel source to a custom SWSD field if you need to preserve that context.

Contacts → Users

LiveAgent contacts become SWSD users with the Requester role. Help desk contacts store essential information about customers, including their name, email, phone number, and other personal information.

Warning

Critical constraint: SWSD incidents require a registered requester_email on create. All contacts must be loaded as SWSD users before you create any incidents. Violating this order results in API errors on every incident creation call. LiveAgent allows contacts to exist with minimal data, but SWSD requires structured user profiles — ensure all LiveAgent contacts have valid email addresses before extraction. Contacts without email addresses must be assigned to a fallback "Legacy Migration User" (see Edge Cases).

Companies → Departments, Sites, or Custom Fields

SWSD does not have a "Company" object equivalent to LiveAgent's. Use this decision tree:

  • Use SWSD Departments if LiveAgent companies map to internal organizational units (e.g., "Engineering," "Marketing," "HR"). Departments in SWSD control assignment routing and reporting segmentation.
  • Use SWSD Sites if LiveAgent companies map to physical locations (e.g., "New York Office," "London HQ"). Sites are available on Business and Professional plans.
  • Use a custom field on the User record if LiveAgent companies represent external customers (e.g., "Acme Corp," "Widget Inc.") that do not correspond to any internal SWSD organizational structure. This is the most common scenario for teams migrating from customer-facing support.

LiveAgent lets each contact belong to only one company, while SWSD does not give you a direct company object on the requester record. If company identity matters for reporting, store it explicitly using whichever approach matches your organizational model.

Tags → Categories

LiveAgent tags are flat, free-form labels. SWSD categories are hierarchical (parent → child). You need to:

  1. Export all LiveAgent tags and their usage frequency.
  2. Decide which tags become SWSD categories, which become subcategories, and which get dropped.
  3. Create the category hierarchy in SWSD before loading incidents.
  4. Map tag names to category IDs in your transformation script.
  5. Store overflow tags in a custom field or in tag_list if you need to preserve all of them.

Knowledge Base → Solutions

LiveAgent knowledge base articles map to SWSD Solutions. Both support HTML content, but the organizational structure differs. LiveAgent uses portals with categories; SWSD uses a flat solutions list with categories.

Visibility mapping: LiveAgent KB articles can be public (customer-facing) or internal (agent-only). SWSD Solutions have corresponding visibility settings — public (visible in the self-service portal) and internal (visible only to agents). Map LiveAgent's article visibility to the matching SWSD Solution visibility during transformation. Verify visibility settings after loading by spot-checking both the agent interface and the self-service portal.

Rewrite any internal links that referenced LiveAgent URLs.

Chat Transcripts and Call Recordings

LiveAgent stores chat transcripts as part of ticket messages. These get flattened into SWSD incident comments. Call recordings migrate as file attachments. Real-time metadata (typing indicators, visitor page history, chat ratings) has no SWSD equivalent and is lost.

Chat-only tickets without contacts: LiveAgent can create tickets from anonymous chat sessions where no email address was captured and no contact record exists. These tickets cannot be assigned to a valid SWSD requester. Handle them by assigning to the fallback "Legacy Migration User" and prepending any available session data (visitor name, IP, chat start time) to the incident description.

If channel attribution matters, store the original channel as a prefix in each comment body (e.g., [CHAT], [EMAIL], [CALL]).

Custom Fields

LiveAgent supports custom fields on tickets and contacts. SWSD supports custom fields on incidents and users, but they must be strictly typed (e.g., Dropdown, Date, Text). LiveAgent enables you to create custom contact fields to store unique information about contacts, such as shoe size, car model, billing address, account number, or birthday.

You must pre-create these custom fields in the SWSD admin panel before running the API migration. SWSD does not support creating custom field definitions via the API — they must be configured manually in Settings → Custom Fields. Once created, note the field ID from the API response when listing custom fields (GET /custom_fields.json), and reference that ID in your incident/user payloads during loading.

Verify data type compatibility — LiveAgent text fields mapping to SWSD dropdown fields require picklist value creation in advance. Audit custom field usage and values before migration, not during.

Migration Architecture

Data Flow

LiveAgent API v3           Staging DB           SolarWinds Service Desk API
─────────────────         ───────────         ──────────────────────────────
GET /tickets        →     PostgreSQL    →     POST /users.json
GET /customers      →     (transform,   →     POST /incidents.json
GET /companies      →      validate,    →     POST /incidents/{id}/comments.json
GET /tags           →      deduplicate)  →     POST /solutions.json
GET /knowledgebase  →                   →     POST /incidents/{id}/attachments

Do not attempt to transform and load in the same active memory thread. If the script crashes, you lose your place. Store raw JSON in a staging database so you can replay from any point.

API Authentication

LiveAgent: API key passed in the apikey header. The API key is an identity of your LiveAgent account which you can use to build custom integration solutions. Generate keys at Configuration > System > API.

SWSD: Token-based authentication. Service Desk provides token-based authentication that encrypts credentials and enhances security while enabling API use. Only someone with a Service Desk administrator license can generate an API token. The token is passed as X-Samanage-Authorization: Bearer [TOKEN]. If the admin user is disabled or the token is reset, the integration breaks.

Timestamp Backdating

To preserve original creation dates on migrated incidents, pass the created_at field in your incident payload with the original LiveAgent timestamp in ISO 8601 format. The SWSD API accepts created_at on incident creation when the authenticated user has administrator permissions. Test this explicitly in your sandbox — if backdating is rejected (the API silently ignores the field or returns the current timestamp), your historical data will lose temporal ordering, which is a critical fidelity issue. If backdating is not supported for your plan tier, document this limitation and store the original creation date in a custom field as a workaround.

Rate Limits

Platform Rate Limit Pagination
LiveAgent 180 requests per minute per API key _page and _perPage (max 1,000 rows per response)
SWSD Varies by plan tier; monitor X-RateLimit-Remaining and Retry-After response headers page and per_page (default 25, max 100)

SWSD rate limits are not published per plan tier in their documentation. Monitor the X-RateLimit-Remaining header on every response to track your remaining quota, and implement automatic backoff when it approaches zero. If you hit a 429 status code, read the Retry-After header and pause execution. Failing to back off will result in temporary blocks.

SWSD returns total count in the X-Total-Count response header, which you can use to calculate the total number of pages needed.

The LiveAgent GET tickets API returns a maximum of 1,000 rows per call. The /tickets list endpoint is also capped at 10,000 records per filter, so large extracts must be windowed by date range — monthly, weekly, or daily slices depending on volume.

Info

Throughput math: At 180 req/min from LiveAgent with 50 tickets per page, you extract metadata for ~9,000 tickets per minute. But individual message retrieval requires one additional call per ticket, cutting effective throughput to ~90 tickets/min (metadata + messages). For 100K tickets, extraction alone takes ~18.5 hours minimum. Factor in attachment downloads and the total extraction phase for a 100K-ticket account can exceed 24 hours.

Cloud-hosted LiveAgent accounts have rate limits to prevent server overload. Standalone installations have the same rate limiter, but you can override it by inserting a row into the qu_g_settings table. If you run a self-hosted LiveAgent instance, increasing the rate limit before migration can speed up extraction.

Attachment Size Mismatch

SWSD enforces a 25 MB per-file attachment limit. LiveAgent cloud ticket attachments have a similar cap, but chat attachments can reach 128 MB. Files exceeding the SWSD limit must be externalized — archive them in cloud storage (S3, Azure Blob) and insert a download link in the incident comment body. Log all oversized files for post-migration review.

Step-by-Step Migration Process

Step 1: Extract Data from LiveAgent

Use the API v3 to paginate through all objects. You can use API v3 to export data — the call gives you tickets per page and allows you to set time ranges for which tickets to export. Once the page gets less than the max results, move on to the next day.

import requests
import time
import json
 
LA_BASE = "https://yourdomain.ladesk.com/api/v3"
API_KEY = "your-api-key-here"
headers = {"apikey": API_KEY}
 
def extract_tickets(start_date, end_date):
    """Extract tickets within a date window to stay under 10K filter cap."""
    tickets = []
    page = 1
    while True:
        params = {
            "_perPage": 50,
            "_page": page,
            "_filters": json.dumps([
                ["date_created", "D>", f"{start_date} 00:00:00"],
                ["date_created", "D<", f"{end_date} 23:59:59"]
            ])
        }
        resp = requests.get(f"{LA_BASE}/tickets", headers=headers, params=params)
        if resp.status_code == 429:
            time.sleep(60)
            continue
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        tickets.extend(data)
        if len(data) < 50:
            break  # last page
        page += 1
        time.sleep(0.35)  # stay under 180 req/min
    return tickets
 
def extract_messages(ticket_id):
    """Retrieve all messages for a single ticket."""
    resp = requests.get(
        f"{LA_BASE}/tickets/{ticket_id}/messages",
        headers=headers
    )
    resp.raise_for_status()
    return resp.json()
 
def extract_contacts():
    """Paginate through all customer contacts."""
    contacts = []
    page = 1
    while True:
        resp = requests.get(
            f"{LA_BASE}/customers",
            headers=headers,
            params={"_perPage": 50, "_page": page}
        )
        resp.raise_for_status()
        data = resp.json()
        if not data:
            break
        contacts.extend(data)
        if len(data) < 50:
            break
        page += 1
        time.sleep(0.35)
    return contacts

If you prefer a database dump over API export, LiveAgent can provide one — you need to provide secure SSH/SCP access or they can share via a Google Drive download link. This is faster for very large accounts but requires working with raw database tables. Database dumps contain raw data and LiveAgent recommends working with the API instead.

Step 2: Transform Data

Transformation handles schema mapping, data cleaning, and deduplication.

Key transformations:

  • Status mapping: LiveAgent New → SWSD New. LiveAgent Open → SWSD Assigned. LiveAgent Answered → SWSD Awaiting Input. LiveAgent Resolved → SWSD Resolved. LiveAgent Postponed → SWSD On Hold. Note: Awaiting Input and On Hold may be custom states in your SWSD instance — create them before migration and verify via GET /incidents/states.json.
  • Priority mapping: LiveAgent priorities (1–3 stars or custom) → SWSD priority levels (Low, Medium, High, Critical). Map 1-star → Low, 2-star → Medium, 3-star → High. If you use a Critical priority, assign it based on a tag or custom field rule.
  • Tag → Category: Flatten multi-tag tickets into a primary category. Store additional tags in a custom field.
  • Department → Group: Map LiveAgent department IDs to SWSD group IDs.
  • Contact deduplication: LiveAgent may create a new contact for each unique email-channel combination. A customer who emails and chats may have two contact records. SWSD users are unique by email — merge duplicates before loading, keeping the record with the most complete data (most fields populated, most recent activity).
  • ID swapping: Swap LiveAgent Contact IDs for newly created SWSD User IDs using your mapping table.
STATUS_MAP = {
    "N": "New",        # LiveAgent "New"
    "T": "Assigned",   # LiveAgent "Open" / "To solve"
    "A": "Awaiting Input",  # LiveAgent "Answered" — verify this state exists
    "R": "Resolved",   # LiveAgent "Resolved"
    "P": "On Hold",    # LiveAgent "Postponed" — requires custom state
}
 
PRIORITY_MAP = {
    "1": "Low",
    "2": "Medium",
    "3": "High",
}
 
def transform_ticket_to_incident(ticket, category_map, contact_to_user_map):
    """Map a LiveAgent ticket to a SWSD incident payload."""
    contact_email = ticket.get("contact_email", "")
    if not contact_email or contact_email not in contact_to_user_map:
        contact_email = "legacy-migration-user@yourdomain.com"
 
    messages = ticket.get("messages", [])
    description = messages[0]["body"] if messages else ""
 
    primary_tag = ticket.get("tags", ["General"])[0] if ticket.get("tags") else "General"
 
    return {
        "incident": {
            "name": ticket.get("subject") or "No Subject",
            "description": description,
            "requester": {"email": contact_email},
            "state": STATUS_MAP.get(ticket.get("status"), "New"),
            "priority": PRIORITY_MAP.get(str(ticket.get("priority", "1")), "Low"),
            "category": {"name": category_map.get(primary_tag, "General")},
            "created_at": ticket["date_created"],
            "custom_fields_values": {
                "source_liveagent_id_1234": str(ticket["id"]),
                "source_channel_5678": ticket.get("channel", "unknown"),
            }
        }
    }

Step 3: Load Users into SWSD

Create all users (former LiveAgent contacts and agents) before creating incidents. Disable email notifications during bulk loads to avoid flooding inboxes — in SWSD, navigate to Setup → Notification Settings and disable incident creation notifications before the import run.

SWSD_BASE = "https://api.samanage.com"
SWSD_TOKEN = "your-jwt-token"
swsd_headers = {
    "X-Samanage-Authorization": f"Bearer {SWSD_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/vnd.samanage.v2.1+json"
}
 
def create_swsd_user(contact, id_map):
    """Create a user in SWSD and store the ID mapping."""
    email = contact["email"].lower().strip()
 
    # Check if already created (idempotency)
    if email in id_map:
        return id_map[email]
 
    payload = {
        "user": {
            "name": contact.get("name") or "Unknown",
            "email": email,
            "phone": contact.get("phone", ""),
            "role": {"name": "End User"}
        }
    }
    resp = requests.post(
        f"{SWSD_BASE}/users.json",
        headers=swsd_headers,
        json=payload
    )
    if resp.status_code == 200:
        swsd_user = resp.json()
        id_map[email] = swsd_user["id"]
        return swsd_user["id"]
    else:
        raise Exception(f"User creation failed for {email}: {resp.status_code} {resp.text}")

Store a mapping table of LiveAgent_Contact_IDSWSD_User_ID (keyed by email). This table is essential for every subsequent step.

Step 4: Load Incidents into SWSD

def create_swsd_incident(incident_payload):
    """Create an incident in SWSD and return the new ID."""
    resp = requests.post(
        f"{SWSD_BASE}/incidents.json",
        headers=swsd_headers,
        json=incident_payload
    )
    if resp.status_code == 200:
        return resp.json()["id"]
    else:
        raise Exception(f"Incident creation failed: {resp.status_code} {resp.text}")

Capture the returned SWSD Incident IDs and store in your mapping table.

Step 5: Load Comments and Attachments

After creating each incident, add subsequent messages as comments:

def add_comment(incident_id, message, channel_prefix=""):
    """Add a comment to an existing SWSD incident."""
    body = message["body"]
    if channel_prefix:
        body = f"[{channel_prefix}] {body}"
 
    payload = {
        "comment": {
            "body": body,
            "is_private": not message.get("is_public", True)
        }
    }
    resp = requests.post(
        f"{SWSD_BASE}/incidents/{incident_id}/comments.json",
        headers=swsd_headers,
        json=payload
    )
    if resp.status_code not in (200, 201):
        raise Exception(f"Comment failed on incident {incident_id}: {resp.status_code}")

For attachments, download each file from LiveAgent (separate API call per file) and upload to the corresponding SWSD incident using multipart/form-data. Track file sizes — if a file exceeds SWSD's 25 MB limit, archive it externally and add a link in the comment body.

Step 6: Load Knowledge Base Articles as Solutions

LiveAgent KB articles map to SWSD Solutions:

def create_solution(article, visibility_map):
    """Create a Solution in SWSD from a LiveAgent KB article."""
    # Map LiveAgent visibility to SWSD
    is_internal = article.get("visibility") == "internal"
 
    payload = {
        "solution": {
            "name": article["title"],
            "content": article["body_html"],
            "visibility": "internal" if is_internal else "public"
        }
    }
    resp = requests.post(
        f"{SWSD_BASE}/solutions.json",
        headers=swsd_headers,
        json=payload
    )
    if resp.status_code not in (200, 201):
        raise Exception(f"Solution failed: {resp.status_code} {resp.text}")

Verify HTML rendering in the SWSD Solutions interface after loading. Check both the agent view and the self-service portal for public articles.

Error Handling and Logging

Every API call should log the request, response status, and response body. Implement:

  • Retry with exponential backoff for 429 (rate limit) and 5xx errors. Start with 1-second delay, double on each retry, cap at 60 seconds, max 5 retries.
  • Idempotency tracking — store a mapping of LiveAgent IDs to SWSD IDs so you can resume after failures without creating duplicates.
  • Error quarantine — log failed records to a separate file for manual review rather than stopping the entire migration.
  • Structured logging — for every write, log source ID, target ID, batch ID, payload hash, retry count, and final result. Treat validation mismatches as hard stops, not warnings.

Edge Cases and Challenges

The "System User" Fallback

In LiveAgent, a ticket might be created via a channel that did not capture an email address (e.g., a Twitter DM, an anonymous chat, or a phone call without caller ID). SWSD will reject an incident payload if the requester_id is null or invalid.

Solution: Create a generic "Legacy Migration User" in SWSD (e.g., legacy-migration-user@yourdomain.com). During transformation, if a LiveAgent ticket lacks a valid contact email, assign it to this fallback user and prepend the original contact info (name, channel, timestamp) into the incident description so the context is not lost. Tag these incidents with a missing_requester flag for post-migration review.

Duplicate Contacts

LiveAgent creates a new contact for each unique email-channel combination. A customer who emails and chats may have two contact records with the same email. SWSD enforces unique emails per user.

Solution: Before loading, group all LiveAgent contacts by email (lowercased, trimmed). For each group with multiple records, merge into a single user record: keep the name from the most recently active record, concatenate phone numbers if different, merge custom field values preferring non-null values. Log all merges for audit.

Multi-Channel Tickets

A single LiveAgent ticket can contain email messages, chat transcripts, and call recordings in one thread. SWSD incidents do not distinguish message channels within comments. Channel metadata is lost unless you store the original channel as a prefix in each comment body (e.g., [CHAT] 2024-03-15 14:22 — Customer: I can't log in).

Inline Images in HTML

LiveAgent emails often contain inline base64 images or hosted images. When pushed to SWSD, these image links will break if they point back to a secure LiveAgent instance that gets deprecated.

Solution: Parse the HTML of LiveAgent messages, identify <img> tags, download the assets, upload them to SWSD as attachments, and rewrite the HTML src attributes in the payload before posting the comment. For base64-encoded images, decode and upload as separate attachment files.

Custom Field Drift

Option values rarely match cleanly between systems. LiveAgent text fields mapping to SWSD dropdown fields require picklist value creation in advance. Audit custom field usage and values before migration, not during.

Reference Data Errors

One misspelled site or department value can create bad routing or failed updates in SWSD. Validate all reference data against the exact values created in SWSD before loading any records. Pull the canonical list via GET /departments.json and GET /sites.json and compare programmatically.

Ticket Merge History

LiveAgent supports merging tickets. Merged tickets reference a parent ticket. SWSD does not support merged incident records during import. If a LiveAgent ticket was merged, extract the full merged thread and load it as a single SWSD incident with all messages from all original tickets as sequential comments.

Limitations and Constraints

SolarWinds Service Desk Limitations

  • No true custom objects. SWSD has a fixed schema (incidents, problems, changes, assets, CMDB). You cannot create entirely new object types.
  • Category hierarchy is rigid. SWSD categories follow a parent-child structure. LiveAgent's flat tag model does not map cleanly.
  • No community forums. LiveAgent's forums and feedback boards have no equivalent.
  • Per-page limit on list endpoints. SWSD list endpoints use per_page with a max of 100. Large imports require careful pagination.
  • CSV import limitations. CSV incident import only brings in text — not attachments or screenshots. Sites and departments cannot be imported via CSV. Exported SWSD CSVs are not valid import templates. Updating custom fields via CSV is not supported.
  • Custom fields cannot be created via API. They must be configured manually in the SWSD admin panel before migration.
  • Sandbox provisioning. SWSD does not provide a self-service sandbox. Contact SolarWinds support to request a sandbox instance for migration testing, or use a separate trial account. Do not test against production.

LiveAgent Export Limitations

  • The GET tickets API returns a maximum of 1,000 rows per call. Use date-range filtering and pagination to work around this.
  • The /tickets list endpoint caps at 10,000 records per filter. Window extraction by date range.
  • CSV export captures metadata snapshots but not message bodies or attachments.
  • Database dumps contain raw data and LiveAgent recommends working with the API instead.

Data That Cannot Be Migrated

Data Type Reason
Chat visitor metadata (pages viewed, browser, geolocation) No SWSD equivalent
Ticket merge history SWSD does not support merged records on import
SLA timing data (time-to-first-response, resolution clock) SWSD SLAs start fresh; cannot import elapsed timers
Agent notes on contacts Must be mapped to SWSD user custom fields or incident comments
Automation rules Must be rebuilt manually in SWSD
Chat widget and IVR configurations No migration path
Forum posts and feedback boards No SWSD equivalent; archive separately
Call metadata (hold time, transfer count, IVR path) Lost; audio file migrates as attachment only

Validation and Testing

Record Count Validation

After loading, compare counts across every object type:

Object LiveAgent Count SWSD Count Delta Action
Contacts/Users X Y X - Y Investigate missing records; check dedup log
Tickets/Incidents X Y X - Y Check error quarantine log
Messages/Comments X Y X - Y Verify per-incident; check first-message-as-description offset
KB Articles/Solutions X Y X - Y Manual review
Attachments X Y X - Y Reprocess failures; check 25 MB exclusion log

Field-Level Validation

Sample 50–100 records across each object type and verify:

  • Subject/name matches
  • Status/state mapping is correct
  • Requester email links to the correct user
  • created_at timestamps match source (or document the delta if backdating was not supported)
  • Custom field values transferred correctly
  • Attachment count per incident matches source
  • Long message threads did not get truncated
  • Private comments remain private
  • Solution visibility (public vs. internal) matches source KB article visibility

UAT Process

  1. Migrate a test batch (500–1,000 records) to a SWSD sandbox environment.
  2. Have agents verify 20–30 specific tickets they remember — check that message threads are complete and correctly attributed.
  3. Validate that knowledge base articles render correctly in both the agent view and self-service portal.
  4. Test that SWSD automations trigger correctly on migrated incidents.
  5. Confirm that SLA policies apply as expected to newly created incidents.
  6. Verify that the fallback "Legacy Migration User" incidents are identifiable and tagged for review.

Rollback Planning

SWSD does not support bulk rollback. Your strategy:

  1. Keep LiveAgent active until migration is fully validated. Do not cut mail routing until delta counts are clean.
  2. Tag all migrated records in SWSD with a consistent marker (e.g., a migrated_from_liveagent tag or custom field) so you can identify and bulk-delete if needed.
  3. Maintain the ID mapping table so you can trace any SWSD record back to its LiveAgent source.

For a detailed post-migration QA process, see our Post-Migration QA Checklist.

Post-Migration Tasks

Once the data is verified in production, finalize the configuration.

Rebuild Automations and Workflows

SWSD automations are structurally different from LiveAgent rules. You must manually recreate:

  • Ticket routing rules → SWSD assignment rules based on category, group, or requester
  • SLA rules → SWSD SLA policies (configured per priority and category, with business hours defined fresh). Review your LiveAgent SLA targets and recreate equivalent thresholds — e.g., if LiveAgent had a 4-hour first-response SLA for priority 3, configure the same in SWSD under Setup → SLA → add policy with matching priority and time target.
  • Auto-response templates → SWSD email notification templates
  • Escalation rules → SWSD escalation policies
  • Tag-based automation → SWSD category-based or custom-field-based triggers

See our guide on migrating automations and workflows for patterns that apply across platforms.

Email Routing

Update your DNS and mail server settings to route support emails away from LiveAgent and into the SWSD drop box. Only switch routing after validation is complete. Keep LiveAgent receiving emails in parallel for at least 48 hours after cutover to catch any routing lag.

User Training

The ITIL paradigm shift is significant. Your team needs training on:

  • Incident vs. service request classification — concepts that do not exist in LiveAgent.
  • Problem management — linking recurring incidents to root-cause problems.
  • Change management workflows — approval processes, CAB reviews.
  • CMDB usage — linking incidents to configuration items.
  • Self-service portal — different from LiveAgent's customer portal.

Post-Migration Monitoring

Plan a 2-week hypercare window after go-live. Run daily audits for the first week, then weekly for the first month:

  • Check for orphaned incidents (no requester linked).
  • Verify comment ordering on high-volume tickets.
  • Confirm that attachment links are accessible.
  • Monitor for misrouted incidents and bad requester mapping.
  • Watch SWSD error logs for API-related issues.
  • Spot-check 10 incidents per day against LiveAgent source to verify content integrity.

Best Practices

  1. Back up the source first. Export a full LiveAgent database dump before starting. Besides the CSV export option, LiveAgent offers the ability to request a database dump from their administration team. If you need raw historical extraction, request it early — allow 3–5 business days for LiveAgent to process the request.
  2. Run a sandbox migration first. Always execute a full test run into a SWSD sandbox environment. Never push directly to production on the first attempt.
  3. Pre-create reference data. Create SWSD sites, departments, categories, custom fields, and custom states before loading any records. These cannot be created inline during import.
  4. Freeze the schema. Do not allow admins to add new custom fields in LiveAgent or SWSD while the migration scripts are being written and tested.
  5. Incremental validation. Validate after each batch (users, then incidents, then comments) rather than waiting until the end.
  6. Automate ID mapping. Maintain a persistent lookup table mapping LiveAgent IDs to SWSD IDs. This is essential for relationship rebuilding, deduplication on retry, and debugging.
  7. Plan for the delta. Extract the bulk of your history over a week. On cutover weekend, only extract tickets modified since the initial extraction date.
  8. Turn off notifications during bulk loads. SWSD will send emails for every incident creation unless you disable notifications first. Re-enable immediately after loading completes.
  9. Document field mapping decisions. Every mapping choice (especially lossy ones like tag → category) should be documented for audit purposes.
  10. Preserve source IDs. Store the original LiveAgent ticket ID in a SWSD custom field. This is essential for idempotent re-runs and post-migration debugging.

Sample Data Mapping Table

LiveAgent Field SWSD Field Type Transformation
ticket.id custom field source_liveagent_id String Store for idempotent re-runs
ticket.code custom field source_ticket_code String Preserve user-facing reference
ticket.subject incident.name String Direct copy; normalize empty subjects to "No Subject"
ticket.status incident.state Enum Map: New→New, Open→Assigned, Answered→Awaiting Input, Resolved→Resolved, Postponed→On Hold (custom)
ticket.priority incident.priority Enum Map: 1-star→Low, 2-star→Medium, 3-star→High
ticket.date_created incident.created_at DateTime ISO 8601 format; test backdating in sandbox first
ticket.department_id incident.assignee.group Reference Map via group lookup table
ticket.tags [] incident.category.name String→Enum Primary tag → category; overflow → custom field
ticket.channel custom field source_channel String Preserve original channel (email, chat, phone, social)
contact.email user.email String Lowercase, trim, deduplicate
contact.firstname + lastname user.name String Concatenate with space
contact.phone user.phone String Direct copy
contact.company_id user.department or custom field Reference Map per company→dept/site/custom decision tree
contact.custom_fields user.custom_fields Various Create field definitions in SWSD admin panel first
message.body comment.body HTML Sanitize HTML, preserve formatting, rewrite image links
message.type (internal) comment.is_private Boolean Internal notes → private comments
message.channel Comment body prefix String Prepend [CHAT], [EMAIL], [CALL] if channel attribution needed
message.attachments [] incident.attachments [] File Download from LA, upload to SWSD if ≤ 25 MB; externalize otherwise
kb_article.title solution.name String Direct copy
kb_article.body solution.content HTML Rewrite internal links pointing to LiveAgent URLs
kb_article.visibility solution.visibility Enum Map: public→public, internal→internal

Automation Script Outline

High-level structure for a Python-based migration pipeline:

import requests
import time
import json
import logging
from datetime import datetime, timedelta
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
 
 
class LiveAgentExtractor:
    """Handles paginated extraction from LiveAgent API v3."""
    def __init__(self, base_url, api_key):
        self.base_url = base_url
        self.headers = {"apikey": api_key}
        self.request_count = 0
 
    def _throttled_get(self, url, params=None):
        """GET with rate limiting (180 req/min)."""
        self.request_count += 1
        resp = requests.get(url, headers=self.headers, params=params)
        if resp.status_code == 429:
            logger.warning("Rate limited, sleeping 60s")
            time.sleep(60)
            return self._throttled_get(url, params)
        resp.raise_for_status()
        time.sleep(0.35)
        return resp.json()
 
    def extract_all_contacts(self) -> list:
        contacts, page = [], 1
        while True:
            data = self._throttled_get(
                f"{self.base_url}/customers",
                params={"_perPage": 100, "_page": page}
            )
            if not data:
                break
            contacts.extend(data)
            if len(data) < 100:
                break
            page += 1
        logger.info(f"Extracted {len(contacts)} contacts")
        return contacts
 
    def extract_tickets(self, date_from, date_to) -> list:
        """Window by date to stay under 10K filter cap."""
        all_tickets = []
        current = datetime.strptime(date_from, "%Y-%m-%d")
        end = datetime.strptime(date_to, "%Y-%m-%d")
        while current < end:
            window_end = min(current + timedelta(days=30), end)
            page = 1
            while True:
                params = {
                    "_perPage": 100,
                    "_page": page,
                    "_filters": json.dumps([
                        ["date_created", "D>", current.strftime("%Y-%m-%d 00:00:00")],
                        ["date_created", "D<", window_end.strftime("%Y-%m-%d 23:59:59")]
                    ])
                }
                data = self._throttled_get(f"{self.base_url}/tickets", params=params)
                if not data:
                    break
                all_tickets.extend(data)
                if len(data) < 100:
                    break
                page += 1
            logger.info(f"Window {current.date()}{window_end.date()}: {len(all_tickets)} total")
            current = window_end
        return all_tickets
 
    def extract_messages(self, ticket_id) -> list:
        return self._throttled_get(f"{self.base_url}/tickets/{ticket_id}/messages")
 
    def download_attachment(self, url) -> bytes:
        resp = requests.get(url, headers=self.headers)
        resp.raise_for_status()
        return resp.content
 
 
class DataTransformer:
    """Maps LiveAgent objects to SWSD schema."""
    STATUS_MAP = {"N": "New", "T": "Assigned", "A": "Awaiting Input", "R": "Resolved", "P": "On Hold"}
    PRIORITY_MAP = {"1": "Low", "2": "Medium", "3": "High"}
 
    def __init__(self, category_map):
        self.category_map = category_map
 
    def deduplicate_contacts(self, contacts) -> list:
        by_email = {}
        for c in contacts:
            email = (c.get("email") or "").lower().strip()
            if not email:
                continue
            if email not in by_email or (c.get("date_changed", "") > by_email[email].get("date_changed", "")):
                by_email[email] = c
        logger.info(f"Deduplicated {len(contacts)} contacts to {len(by_email)}")
        return list(by_email.values())
 
    def transform_contact_to_user(self, contact) -> dict:
        return {
            "user": {
                "name": contact.get("name") or "Unknown",
                "email": contact["email"].lower().strip(),
                "phone": contact.get("phone", ""),
                "role": {"name": "End User"}
            },
            "_source_id": contact.get("id")
        }
 
    def transform_ticket_to_incident(self, ticket, contact_email_fallback) -> dict:
        email = (ticket.get("contact_email") or "").lower().strip()
        if not email:
            email = contact_email_fallback
        messages = ticket.get("messages", [])
        primary_tag = (ticket.get("tags") or ["General"])[0]
        return {
            "incident": {
                "name": ticket.get("subject") or "No Subject",
                "description": messages[0]["body"] if messages else "",
                "requester": {"email": email},
                "state": self.STATUS_MAP.get(ticket.get("status"), "New"),
                "priority": self.PRIORITY_MAP.get(str(ticket.get("priority", "1")), "Low"),
                "category": {"name": self.category_map.get(primary_tag, "General")},
                "created_at": ticket.get("date_created"),
            },
            "_source_id": ticket.get("id"),
            "_messages": messages[1:] if len(messages) > 1 else []
        }
 
    def transform_message_to_comment(self, message, channel_prefix="") -> dict:
        body = message["body"]
        if channel_prefix:
            body = f"[{channel_prefix}] {body}"
        return {
            "comment": {
                "body": body,
                "is_private": not message.get("is_public", True)
            }
        }
 
 
class SWSDLoader:
    """Handles creation of records in SolarWinds Service Desk."""
    MAX_RETRIES = 5
 
    def __init__(self, base_url, token):
        self.base_url = base_url
        self.headers = {
            "X-Samanage-Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "Accept": "application/vnd.samanage.v2.1+json"
        }
        self.id_map = {}
 
    def _post_with_retry(self, url, payload):
        delay = 1
        for attempt in range(self.MAX_RETRIES):
            resp = requests.post(url, headers=self.headers, json=payload)
            if resp.status_code == 429:
                wait = int(resp.headers.get("Retry-After", delay))
                logger.warning(f"SWSD rate limit, waiting {wait}s (attempt {attempt+1})")
                time.sleep(wait)
                delay = min(delay * 2, 60)
                continue
            if resp.status_code >= 500:
                logger.warning(f"SWSD 5xx, retrying in {delay}s")
                time.sleep(delay)
                delay = min(delay * 2, 60)
                continue
            return resp
        raise Exception(f"Max retries exceeded for {url}")
 
    def create_user(self, user_data) -> str:
        email = user_data["user"]["email"]
        if email in self.id_map:
            return self.id_map[email]
        resp = self._post_with_retry(f"{self.base_url}/users.json", user_data)
        if resp.status_code == 200:
            uid = resp.json()["id"]
            self.id_map[email] = uid
            return uid
        raise Exception(f"User create failed: {resp.status_code} {resp.text}")
 
    def create_incident(self, incident_data) -> str:
        resp = self._post_with_retry(f"{self.base_url}/incidents.json", incident_data)
        if resp.status_code == 200:
            return resp.json()["id"]
        raise Exception(f"Incident create failed: {resp.status_code} {resp.text}")
 
    def add_comment(self, incident_id, comment_data):
        resp = self._post_with_retry(
            f"{self.base_url}/incidents/{incident_id}/comments.json", comment_data
        )
        if resp.status_code not in (200, 201):
            raise Exception(f"Comment failed: {resp.status_code}")
 
    def upload_attachment(self, incident_id, file_bytes, filename):
        if len(file_bytes) > 25 * 1024 * 1024:
            logger.warning(f"Skipping {filename} ({len(file_bytes)} bytes) — exceeds 25 MB limit")
            return None
        # Use multipart upload — headers differ for file upload
        headers = {k: v for k, v in self.headers.items() if k != "Content-Type"}
        resp = requests.post(
            f"{self.base_url}/incidents/{incident_id}/attachments.json",
            headers=headers,
            files={"file": (filename, file_bytes)}
        )
        return resp
 
 
class MigrationOrchestrator:
    """Coordinates the full ETL pipeline with error handling."""
    FALLBACK_EMAIL = "legacy-migration-user@yourdomain.com"
 
    def __init__(self, extractor, transformer, loader):
        self.extractor = extractor
        self.transformer = transformer
        self.loader = loader
        self.error_log = []
 
    def run(self, date_from="2020-01-01", date_to="2025-12-31"):
        # Phase 1: Extract
        logger.info("Phase 1: Extracting contacts")
        raw_contacts = self.extractor.extract_all_contacts()
 
        logger.info("Phase 2: Deduplicating and transforming contacts")
        contacts = self.transformer.deduplicate_contacts(raw_contacts)
        users = [self.transformer.transform_contact_to_user(c) for c in contacts]
 
        # Phase 3: Load users (must complete before incidents)
        logger.info(f"Phase 3: Loading {len(users)} users")
        for user in users:
            try:
                self.loader.create_user(user)
            except Exception as e:
                self.error_log.append({"type": "user", "source_id": user.get("_source_id"), "error": str(e)})
 
        # Phase 4: Extract and load tickets
        logger.info("Phase 4: Extracting tickets")
        tickets = self.extractor.extract_tickets(date_from, date_to)
        logger.info(f"Extracted {len(tickets)} tickets, loading incidents")
 
        for ticket in tickets:
            try:
                # Fetch messages
                messages = self.extractor.extract_messages(ticket["id"])
                ticket["messages"] = messages
 
                # Transform
                incident_data = self.transformer.transform_ticket_to_incident(ticket, self.FALLBACK_EMAIL)
                remaining_messages = incident_data.pop("_messages")
                source_id = incident_data.pop("_source_id")
 
                # Create incident
                swsd_id = self.loader.create_incident(incident_data)
                logger.info(f"Ticket {source_id} → Incident {swsd_id}")
 
                # Add comments
                for msg in remaining_messages:
                    channel = msg.get("channel", "")
                    comment = self.transformer.transform_message_to_comment(msg, channel)
                    self.loader.add_comment(swsd_id, comment)
 
                    # Attachments
                    for att in msg.get("attachments", []):
                        file_bytes = self.extractor.download_attachment(att["url"])
                        self.loader.upload_attachment(swsd_id, file_bytes, att["name"])
 
            except Exception as e:
                self.error_log.append({"type": "ticket", "id": ticket.get("id"), "error": str(e)})
                logger.error(f"Failed ticket {ticket.get('id')}: {e}")
 
        # Phase 5: Report
        logger.info(f"Migration complete. Errors: {len(self.error_log)}")
        with open("errors.json", "w") as f:
            json.dump(self.error_log, f, indent=2)

For full auditability, keep raw source payloads, transformed payloads, and the source-to-target crosswalk in separate database tables or JSON files.

What to Expect

This migration is medium-to-high complexity. Expect 2–3 weeks for environments under 50,000 records with a dedicated engineer. Larger datasets (100K+) or environments with deep custom field usage and multi-channel ticket

Frequently Asked Questions

Can I migrate LiveAgent tickets to SolarWinds Service Desk using CSV only?
Partially. LiveAgent supports CSV export for ticket metadata, and SWSD supports CSV import for incidents — but CSV only brings in text, not attachments, screenshots, or individual message threads. LiveAgent CSV export captures metadata but not message bodies. For full data fidelity, use the LiveAgent API v3 for extraction and the SWSD REST API for loading.
What are the API rate limits for this migration?
LiveAgent's API rate limit is 180 requests per minute per API key for cloud-hosted accounts. The GET tickets endpoint returns a maximum of 1,000 rows per call and caps at 10,000 records per filter — use date-range windowing for large exports. SWSD rate limits vary by plan tier; monitor the response headers and implement exponential backoff on 429 responses. Self-hosted LiveAgent instances can override rate limits via the qu_g_settings database table.
How do LiveAgent objects map to SolarWinds Service Desk?
Tickets map to Incidents or Service Requests. Contacts become Users with the Requester role. Companies map to Departments, Sites, or custom fields (SWSD has no Company object). Tags map to hierarchical Categories (not 1:1 — requires planning). Knowledge Base articles become Solutions. Chat transcripts are flattened into incident comments, losing real-time metadata.
How long does a LiveAgent to SolarWinds Service Desk migration take?
For environments under 50,000 records with a dedicated engineer, expect 2–3 weeks including extraction, transformation, loading, and validation. Larger datasets (100K+ records) with complex custom fields and multi-channel histories typically take 4–8 weeks. A managed migration service can compress this timeline.
What data is lost when migrating from LiveAgent to SolarWinds Service Desk?
Chat visitor metadata (pages viewed, browser info), SLA timing data, ticket merge history, forum/feedback board content, chat widget configurations, and automation rules cannot be migrated. Call recordings transfer as file attachments but lose call-specific metadata. Channel indicators (WhatsApp, Facebook, etc.) are lost unless stored in a custom field. All automations must be rebuilt manually in SWSD.

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
Zero-Downtime Help Desk Data Migration: How to Keep Support Running During the Move
Help Desk

Zero-Downtime Help Desk Data Migration: How to Keep Support Running During the Move

This guide details the 3-stage technical process for a zero-downtime help desk migration. Learn how to use an initial bulk data transfer, a continuous delta migration (Change Data Capture), and a seamless final cutover to move platforms without any service interruption. Discover how an engineer-led approach can guarantee a 100% accurate, 50x faster migration.

Raaj Raaj · · 7 min read