Skip to content

SolarWinds Service Desk to Kayako Migration: Technical Guide

Technical guide to migrating from SolarWinds Service Desk to Kayako — covering API extraction, data model mapping, timestamp preservation, and cutover strategy.

Nachi Nachi · · 20 min read
SolarWinds Service Desk to Kayako 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

SolarWinds Service Desk to Kayako Migration: Technical Guide

Info

TL;DR — SolarWinds Service Desk to Kayako Migration

Migrating from SolarWinds Service Desk (SWSD) to Kayako compresses an ITIL-centric service management platform into a conversation-first helpdesk. Only SWSD incidents and their comments map naturally to Kayako conversations. Problems, changes, releases, CMDB records, assets, and service catalog items have no Kayako equivalent and must be archived or left behind. The SWSD REST API (api.samanage.com) uses offset pagination with a max page size of 200 and undocumented rate-limit thresholds. Kayako's single-case create endpoint does not accept a writable created_at — use the bulk cases endpoint (POST /api/v1/bulk/cases.json) for history-preserving imports. Kayako uses PUT-only updates with no PATCH, so partial updates can silently overwrite fields. Migration scope and timeline scale with record count, attachment volume, and custom field complexity: a 30,000-incident tenant with clean fields and few attachments can complete extraction, transformation, and import in 5–7 days of engineering time; a 100,000-incident tenant with inline images, multi-team routing, and heavy attachment volume realistically requires 3–5 weeks.

Targets SWSD API v2.1 and Kayako API v1.

A SolarWinds Service Desk to Kayako migration extracts incidents, users, comments, attachments, and custom fields from SWSD and loads them into Kayako's conversation-based data model. The goal is to preserve support interaction history so agents in Kayako can reference prior exchanges without losing context.

This is not a like-for-like swap. You are flattening an internal IT Service Management framework built around hardware assets, CMDB records, and ITIL processes into a platform designed for external customer support and omnichannel conversations. There is no plug-and-play connector. You extract via the SWSD REST API, transform the data to fit Kayako's schema, and load it via the Kayako API.

Every technical decision in this migration stems from that structural mismatch.

Structural Mismatch: What This Migration Actually Is

SWSD is built around the ITIL process model: incidents link to problems, problems link to changes, changes belong to releases, and everything relates back to configuration items in a CMDB. Kayako has none of this hierarchy — it is a flat conversation store with case types, tags, and custom fields as the primary organizational primitives.

The practical consequence: approximately 60–70% of the entities that exist in a typical SWSD tenant (problems, changes, releases, CMDB records, service catalog items, SLA policies, automation rules) cannot be migrated to Kayako in any functionally equivalent way. If your team uses these entities as active working data — not just historical reference — evaluate whether Kayako is the correct target before investing in migration engineering.

SWSD-to-Kayako Data Model Mapping

This is where the migration succeeds or quietly drops data.

SWSD Entity Kayako Equivalent Notes
Incident Case/Conversation Direct mapping. Store the SWSD incident ID in legacy_id and a visible custom field.
Public Comment Public Case Post Use MAIL channel for customer-visible history in bulk imports.
Private Comment NOTE Post Do not flatten internal comments into the public thread.
Service Request Case (archived) Catalog workflow, task chains, and approvals cannot be recreated natively.
Problem Case (type: Problem) Kayako supports a "Problem" case type, but it is a flat conversation — no linked-incident graph, no root cause analysis fields.
Change ❌ None No equivalent. Archive or store in a notes field.
Release ❌ None No equivalent.
Asset / CMDB ❌ None Kayako has no asset management module.
Service Catalog ❌ None No request-fulfillment workflow.
Solution (KB Article) Help Center Article Separate migration via /api/v1/helpcenter/articles.
User (Requester) User Map email-to-email. Precreate users for stable requester IDs.
User (Technician/Agent) Agent Must be pre-provisioned in Kayako before import.
Category / Subcategory Tag or Custom Field SWSD categories are hierarchical; Kayako tags are flat. Hierarchy is lost.
Custom Fields Custom Fields Must be pre-created in Kayako with matching API field keys before import.
SLA Policy SLA Definition Must be manually rebuilt — no programmatic migration path.
Attachment Attachment Binary transfer required. SWSD allows 25 MB per file; Kayako documents 20 MB per post.
Warning

What you lose. SWSD's linked-incident-to-problem relationships, change approval chains, CMDB dependency maps, service catalog request workflows, SLA policies, automation rules, and dashboard configurations have zero programmatic transfer path to Kayako. If your team actively depends on these as live working data, Kayako is the wrong target. Audit your SWSD usage before committing.

Handling Problems and Changes

SWSD Problems can be imported as Kayako conversations with the type set to "Problem" (Kayako case type ID 3, alongside Question=1, Incident=2, Task=4). A Kayako "Problem" is a flat conversation with a label — no linked incidents, no root cause analysis fields, no problem-incident relationship graph. The relationship data is permanently lost.

Changes have no Kayako equivalent at all. Your options:

  • Archive to CSV/JSON. Export all change records and store them as reference files. Link to them from a Kayako Help Center article or internal wiki.
  • Import as tagged conversations. Create change records as Kayako conversations tagged migrated-change-record and immediately close them. Searchable, but clutters the conversation list.
  • Leave in SWSD. Keep SWSD in read-only mode for historical change records. Many teams maintain read-only access to the old system for 6–12 months post-migration.

If your SWSD tenant uses service requests heavily, decide up front whether they migrate as searchable history or stay in an archive. SolarWinds treats service requests as catalog-based workflows; Kayako can store the record as a conversation, but cannot recreate the original request catalog, task chain, or approvals as native workflow objects.

Extracting Data from the SolarWinds Service Desk API

Authentication and Regional Endpoints

SWSD uses regional API base URLs: US (https://api.samanage.com), EU (https://apieu.samanage.com), and APAC (https://apiau.samanage.com). To get your API token, log in as an administrator, navigate to Setup > Account > API Token, and generate or copy the existing token. Regenerating the token invalidates previous integrations — do not let the token owner reset it mid-project.

All requests require the Accept header set to application/vnd.samanage.v2.1+json and the X-Samanage-Authorization: Bearer <token> header.

import requests
import time
 
SWSD_BASE = "https://api.samanage.com"  # Adjust for EU/APAC
SWSD_TOKEN = "your-api-token"
 
headers = {
    "X-Samanage-Authorization": f"Bearer {SWSD_TOKEN}",
    "Accept": "application/vnd.samanage.v2.1+json",
}
 
def get_incidents(page=1, per_page=200):
    url = f"{SWSD_BASE}/incidents.json?page={page}&per_page={per_page}"
    resp = requests.get(url, headers=headers)
    if resp.status_code == 429:
        time.sleep(10)
        return get_incidents(page, per_page)
    total = int(resp.headers.get("X-Total-Count", 0))
    return resp.json(), total

Pagination

SWSD pagination uses offset-based paging with a default and max page size of 200, using the page parameter. The total record count is returned in the X-Total-Count response header.

Loop through pages until page * per_page >= total_count. At high page offsets (page 1,000+), the API slows noticeably because offset pagination requires scanning the full preceding dataset on each request. For 100,000 incidents at 200 per page, this means 500 API calls. At 2 req/sec (a safe sustained pace based on observed behavior across multiple tenants), extraction takes roughly 4–5 minutes for the index alone — plus additional requests for comments and attachments per incident.

def extract_all_incidents():
    all_incidents = []
    page = 1
    while True:
        batch, total = get_incidents(page=page)
        all_incidents.extend(batch)
        if page * 200 >= total:
            break
        page += 1
    return all_incidents

Rate Limits

SolarWinds Service Desk enforces rate limiting on API requests, but does not publish specific numeric thresholds or rate-limit response headers in its public documentation. In practice, sustained extraction at 2 requests per second produces no 429 responses against tenants with up to 200,000 incidents. Bursting above 5 req/sec intermittently triggers 429s. Implement exponential backoff starting at 10 seconds on any 429 response — the retry-after behavior is not consistent across SWSD API versions.

Extracting Comments and Attachments

SWSD comments are nested under each incident. Fetch them per-incident:

GET /incidents/{incident_id}/comments.json?per_page=200

Attachments are embedded within comment or incident payloads as download URLs. These URLs are authenticated — you cannot pass them to Kayako and expect it to download them. You must download each binary file and re-upload it.

For a full breakdown of SWSD export options and limitations, see How to Export Data from SolarWinds Service Desk.

Loading Data into the Kayako API

Authentication and Endpoints

Kayako Cloud uses Basic Auth with agent/admin credentials. All API calls go to https://YOUR-DOMAIN.kayako.com/api/v1/.

One Kayako-specific naming gotcha: the docs use both conversations and cases. The conversations endpoints work for front-end conversation creation, but the import features you need — legacy_id, bulk insert, posts [], historical timestamps — are on the cases endpoints. Build against cases, not conversations.

import requests
from requests.auth import HTTPBasicAuth
 
KAYAKO_DOMAIN = "yourcompany.kayako.com"
KAYAKO_AUTH = HTTPBasicAuth("admin@yourcompany.com", "your-password")
 
def create_case(subject, requester_email, content, channel="MAIL"):
    url = f"https://{KAYAKO_DOMAIN}/api/v1/cases.json"
    payload = {
        "subject": subject,
        "channel": channel,
        "requester": {"email": requester_email},
        "contents": content,
        "type": {"id": 4}
    }
    resp = requests.post(url, json=payload, auth=KAYAKO_AUTH)
    return resp.json()

PUT-Only Updates and Silent Field Overwrites

Kayako does not expose a PATCH endpoint. All updates use PUT and require the complete object. If you need to update a conversation after creation — to add custom fields, for example — you must send the full object. Omitting a field from the PUT payload can silently null it out.

Concrete failure scenario: You create a case, then want to add a custom field value via a subsequent PUT. If your PUT payload includes the subject, status, and custom field but omits requester_id, the API may clear the requester. The fix: always fetch the current object before updating and merge your changes into the retrieved payload before PUTting.

Pagination and Rate Limits

By default, Kayako API calls return 10 results per page. You can increase this to 100 using ?limit=100.

Kayako API rate limits are defined per minute. When exceeded, the API returns HTTP 429 with a Retry-After header holding the number of seconds until you can retry. The exact per-minute ceiling is not published in Kayako's API documentation. In migration write workloads across several production imports, 30–40 POST requests per minute against the bulk endpoint produces no sustained 429s. Bursting above 60 req/min triggers throttling within 2–3 minutes. Honor the Retry-After header on every 429 — it is accurate.

For target-side API details, see How to Export Data from Kayako.

Timestamp Preservation: The Hard Problem

This is the single most technically challenging aspect of this migration.

The problem: When you POST /api/v1/cases.json to create a single case, the resulting created_at timestamp is set to the current server time. The single-case endpoint does not expose created_at as a writable field. A ticket originally created on 2023-01-15 shows as created today. This affects every date-ordered view, SLA calculations, and any reporting that depends on original ticket age.

The solution: Use Kayako's bulk cases endpoint. POST /api/v1/bulk/cases.json accepts created_at and updated_at as writable fields for both cases and posts, supports up to 200 cases per request, and accepts legacy_id and posts [].

cases:
  - subject: VPN access fails after password reset
    legacy_id: swsd-incident-104233
    requester_id: 4812
    channel: MAIL
    channel_id: 1
    priority_id: 3
    status_id: 2
    created_at: 2026-05-02T14:33:10Z
    updated_at: 2026-05-02T16:01:44Z
    posts:
      - contents: "<p>User cannot connect from home.</p>"
        channel: MAIL
        channel_id: 1
        created_at: 2026-05-02T14:33:10Z
      - contents: "Reset VPN profile; waiting for retest."
        channel: NOTE
        created_at: 2026-05-02T14:48:03Z

Notice the visibility split: public history uses MAIL (customer-visible), internal commentary uses NOTE (agent-only). In Kayako, channel on a post serves dual duty — it signals both the ingestion source (email, API, etc.) and the visibility scope. NOTE is not a channel in the routing sense; it is a visibility flag that renders the post in the staff-only note pane regardless of the case's primary channel.

Tip

One subtle edge case: the bulk-case request example in Kayako's docs includes creator_id, but the parameter table does not document it. If preserving original authorship matters, test creator_id in a sandbox before depending on it in production. In testing, creator_id is accepted and stored but may not display in all UI views.

Fallback workarounds if the bulk endpoint doesn't meet your needs:

  1. Custom field. Pre-create an original_created_at custom field in Kayako (type: DATE) and populate it during import. This makes the original timestamp searchable and filterable — the most reliable self-service approach.
  2. Metadata header. Prepend a standardized text block to the first message:
    [Migrated from SolarWinds Service Desk]
    Original Created: 2023-01-15T09:30:00Z
    Original ID: INC-00412
    Original Status: Resolved
    
  3. Database-level import. You can request a data dump or backup for your Kayako instance containing all instance data in a MySQL dump file format. Importing at the database level requires coordination with Kayako's support team and is not self-service.

Step-by-Step Migration Pipeline

Step 1: Audit SWSD Data and Define Scope

Before writing a line of extraction code, run counts against your SWSD tenant:

GET /incidents.json?per_page=1  → X-Total-Count header = total incidents
GET /problems.json?per_page=1   → total problems
GET /changes.json?per_page=1    → total changes

Record: total incidents, service requests, comments, attachments, custom field definitions, active agents, and every SWSD module that will be archived instead of migrated. Sample attachment sizes across 100–200 records — anything over 20 MB needs alternate handling on the Kayako side. Identify whether agents reference problems, changes, releases, or CMDB dependencies during daily triage. Those records are the first things teams discover they miss after go-live if they weren't archived.

Step 2: Pre-Create Kayako Users, Organizations, Teams, and Fields

Load prerequisites before cases. Kayako supports bulk user and organization inserts up to 200 records per request. Use legacy_id wherever possible — users, organizations, and cases all support it. A reliable pattern: use legacy_id for immutable source IDs plus a searchable custom field like source_incident_number for human reconciliation during validation.

When mapping SWSD users to Kayako:

  • SWSD Administrators and Service Desk Agents → Kayako Agents (with appropriate team_ids and role settings)
  • SWSD Requesters → Kayako Users
Warning

Kayako uses a strict identity system. A user record must have an associated identity — usually an email address. If an SWSD user lacks an email (common for system accounts or directory-synced service accounts), the Kayako API will reject the creation payload with a validation error. Generate placeholder emails (e.g., system-user-123@local.migration) for these edge cases and maintain a log of placeholders for post-migration cleanup.

For custom fields, Kayako writes custom values by API field key, not by display label. Pre-create every target field in Kayako's admin panel (Admin > Customizations > Custom Fields) and record the API field key for each.

Silent fail example — wrong key:

// This silently does nothing if "Customer Type" is the display label, not the API key
{
  "custom_fields": {
    "Customer Type": "Enterprise"
  }
}

Correct request using API field key:

// API field key is set in the admin panel, typically snake_cased
{
  "custom_fields": {
    "customer_type": "Enterprise"
  }
}

To confirm a write succeeded, retrieve the case and check the custom_fields object in the response. A missing or null value means the key was wrong — the API returns HTTP 200 either way.

Use the include=* parameter on GET requests to view full custom field option sets:

GET /api/v1/cases/{id}.json?include=*

Watch for type mismatches when pre-creating fields:

SWSD Field Type Kayako Equivalent Watch Out
Text TEXT Direct map
Dropdown SELECT Option values must match exactly — mismatched options silently fail
Checkbox CHECKBOX Boolean mapping
Date DATE ISO 8601 on both sides
Multi-select SELECT (multi) Verify Kayako supports multi-select for the specific field type

Step 3: Map Statuses and Priorities

SWSD Status Kayako Status Notes
New New Direct map
Assigned Open Kayako's "Open" covers assigned cases
In Progress Open No separate in-progress status by default
On Hold Pending Closest match
Awaiting Input Pending Same
Resolved Completed Or a custom status
Closed Closed Direct map

Kayako's default status types are NEW, OPEN, PENDING, COMPLETED, and CLOSED. Create custom statuses in Kayako's admin panel if you need finer granularity. Retrieve Kayako status IDs before import:

GET /api/v1/statuses.json

For priorities: SWSD uses Critical, High, Medium, Low. Kayako uses numbered priorities (1–4 by default). Map them 1:1 and retrieve priority IDs before import:

GET /api/v1/priorities.json

Step 4: Handle Public vs. Private Comment Visibility

SWSD comments carry an is_private boolean field. Public comments are visible to agents and end users; private comments are visible only to agents.

Map is_private: falsechannel: MAIL (public case post). Map is_private: truechannel: NOTE (staff-only note).

This is the easiest place to leak internal troubleshooting data to customers. A single visibility mapping bug — for example, defaulting all posts to MAIL when the is_private field is missing from the payload — will expose internal SWSD comments to end users. Build an explicit check into your transformation function:

def map_comment_channel(swsd_comment):
    return "NOTE" if swsd_comment.get("is_private", False) else "MAIL"

After every test batch, pull 20–30 cases in the Kayako UI using a non-admin account and confirm that notes are not visible. Do not skip this check.

Step 5: Transfer Attachments and Inline Images

Attachments are the most common failure point in helpdesk migrations.

Size limits: SolarWinds documents a 25 MB per-file attachment limit. Kayako's support documentation describes a 20 MB per-post limit. Build an exception report for files over 20 MB before starting — identify count and total size. Options for oversized files: skip and note in the case metadata, compress where format allows, or upload to external storage (S3, Google Drive) and include a link in the case post. Test your largest attachments in a sandbox before the full run.

Transfer process:

  1. Download from SWSD using an authenticated GET request against the attachment URL in the incident or comment payload.
  2. Stage locally on disk with original filenames and MIME types. Do not store in memory for large volumes — disk staging allows resume on failure.
  3. Upload to Kayako via multipart/form-data during message creation.
  4. Verify the returned response includes the file reference before marking the attachment as transferred in your ledger.
def add_message_with_attachment(conversation_id, content, file_path, filename):
    url = f"https://{KAYAKO_DOMAIN}/api/v1/cases/{conversation_id}/messages.json"
    files = {
        "files[]": (filename, open(file_path, "rb")),
    }
    data = {
        "contents": content,
    }
    resp = requests.post(url, data=data, files=files, auth=KAYAKO_AUTH)
    return resp.json()

Inline images — the S3 expiry problem: SWSD allows agents to paste images directly into the rich text editor. These are stored as <img> tags in the HTML body pointing to time-limited Amazon S3 presigned URLs. If you push this HTML directly to Kayako, the images break within hours — typically within 6–24 hours — when the S3 token expires. This is not a Kayako bug; it is a consequence of how SWSD stores inline content.

Fix: parse the HTML before transformation, find all <img src="..."> attributes, download each image, upload it to Kayako, and rewrite the src to point to the new Kayako file URL before creating the case post.

from bs4 import BeautifulSoup
 
def rewrite_inline_images(html_content, kayako_domain, auth):
    soup = BeautifulSoup(html_content, "html.parser")
    for img in soup.find_all("img"):
        src = img.get("src", "")
        if "s3.amazonaws.com" in src or "amazonaws.com" in src:
            # Download from S3
            img_resp = requests.get(src)
            if img_resp.status_code == 200:
                # Upload to Kayako files endpoint
                upload_resp = requests.post(
                    f"https://{kayako_domain}/api/v1/files.json",
                    files={"file": ("image.png", img_resp.content, "image/png")},
                    auth=auth
                )
                if upload_resp.status_code == 201:
                    new_url = upload_resp.json()["data"]["url"]
                    img["src"] = new_url
    return str(soup)

Run this transformation on every case body and comment body before creating posts in Kayako. Skipping it means a percentage of your imported cases will have broken images with no error in the import log.

Step 6: Import Cases and Posts

With users mapped, fields pre-created, and attachments staged, load historical tickets through POST /api/v1/bulk/cases.json with up to 200 cases per request. Include legacy_id for each case to maintain a source-to-target mapping.

Include comments in the posts [] array of the bulk payload in chronological order. The order of entries in posts [] determines conversation order in the Kayako UI — sort by created_at ascending before building the payload.

Danger

Idempotency. Kayako's POST endpoints have no built-in idempotency key. If your script retries a failed request that actually succeeded at the server, you get duplicate conversations. Build a local ledger (SQLite or a JSON map) tracking swsd_incident_id → kayako_conversation_id before every create call. Check the ledger before every request. Write to the ledger immediately after a successful response, not in a batch at the end of a loop.

import sqlite3
 
def init_ledger(db_path="migration_ledger.db"):
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS id_map (
            swsd_id TEXT PRIMARY KEY,
            kayako_id TEXT,
            migrated_at TEXT
        )
    """)
    conn.commit()
    return conn
 
def already_migrated(conn, swsd_id):
    row = conn.execute("SELECT kayako_id FROM id_map WHERE swsd_id=?", (swsd_id,)).fetchone()
    return row[0] if row else None
 
def record_migration(conn, swsd_id, kayako_id):
    conn.execute(
        "INSERT OR REPLACE INTO id_map VALUES (?, ?, datetime('now'))",
        (swsd_id, kayako_id)
    )
    conn.commit()

Step 7: Reconcile, Delta Sync, and Cut Over

A migration of 100,000 tickets takes days to run. During this time, your live SWSD instance continues receiving new and updated tickets.

Delta sync strategy:

  1. Initial sync: Extract and load all data up to a specific timestamp (e.g., Friday at midnight). This takes the bulk of the time.
  2. Delta extraction: Query SWSD for incidents updated after that timestamp: GET /incidents.json?updated_since=2023-10-12T00:00:00Z.
  3. Upsert logic: Check your mapping ledger. If the ticket exists in Kayako (found in ledger), append new comments and update status. If it does not exist, create it as a new case.
  4. Final delta sync: Run one more delta immediately before cutover. At this stage it should process only hours of updates — typically under 30 minutes.
  5. Cutover: Switch email forwarding and portal routing to Kayako.

Validation before cutover:

  • Compare total record counts between SWSD and Kayako (allow ≤0.1% variance for explicitly skipped records)
  • Spot-check 25–50 records selected randomly plus the 10 most-commented tickets for content accuracy
  • Verify oldest and newest timestamps match originals in the bulk-imported data
  • Confirm attachment presence on a random sample of 20 cases with known attachments
  • Confirm that inline images load in the Kayako UI without authentication
  • Check that private notes are not visible when viewing cases as a non-agent user account
  • Verify assignee and status mappings on a stratified sample across all status values
  • Confirm custom field values on 10–15 cases with populated custom fields

For a deeper look at zero-downtime strategies, see Zero-Downtime Help Desk Data Migration.

Edge Cases and Failure Modes

Each failure mode below includes a detection method and minimum-viable fix.

CCs and collaborators

  • Trigger: SWSD tickets with CC'd users where those users don't exist in Kayako.
  • Detection: Cross-reference SWSD CC email addresses against Kayako's user list before import. GET /api/v1/users.json?email=<address> returns the user if they exist.
  • Fix: Pre-create CC'd users as Kayako Users before the case import run. Then add them as collaborators via POST /api/v1/cases/{id}/collaborators.json after the case is created.

Rich text stripping

  • Trigger: SWSD allows complex HTML including nested tables, <div> layouts, and custom styling in incident descriptions. Kayako's renderer may strip unsupported tags or collapse nested structures.
  • Detection: During sandbox runs, export the raw HTML from Kayako for 20 imported cases and diff against SWSD source. Look for missing table cells, lost line breaks, stripped formatting.
  • Fix: Sanitize HTML to a safe subset (bold, italic, lists, links, images, simple tables) using a library like bleach before import. Test the sanitized output in the Kayako UI before running at scale.

Archived/closed tickets excluded from API

  • Trigger: Some SWSD tenants have tickets in a soft-deleted or deeply archived state that are excluded from GET /incidents.json even without a status filter.
  • Detection: Compare the total count from X-Total-Count against any known total from SWSD admin reporting. A gap of >1% warrants investigation.
  • Fix: Explicitly query closed and resolved statuses: add &state []=resolved&state []=closed to extraction queries and verify they are included in the default response.

Silent custom field writes

  • Trigger: Using a display label instead of the Kayako API field key in the custom_fields object.
  • Detection: After creating a test case with custom fields, retrieve it with GET /api/v1/cases/{id}.json?include=* and check that custom_fields in the response contains the expected values. HTTP 200 is returned regardless of whether the write succeeded.
  • Fix: Use the API field key (visible in Admin > Customizations > Custom Fields, typically snake_cased). Maintain a mapping table of SWSD field name → Kayako API field key established before the import run begins.

Token breakage

  • Trigger: The SWSD API token owner resets their token during the migration project.
  • Detection: Immediate HTTP 401 on all SWSD API calls.
  • Fix: Generate a dedicated service account in SWSD for the migration. Do not use a personal admin account as the token source.

Duplicate records from retries

  • Trigger: Network timeout after the server successfully created a case, causing the client to retry without checking the ledger.
  • Detection: Count Kayako cases after a failed/retried run and compare to the expected count from your ledger.
  • Fix: Check the ledger before every POST. On network errors (not HTTP 4xx/5xx), wait and then GET /api/v1/cases.json?legacy_id=<swsd_id> to check if the case was created before retrying.

Private note leakage

  • Trigger: Defaulting to channel: MAIL when is_private is missing or null in SWSD payload.
  • Detection: After each test batch, log in as a non-agent user and view 10 randomly selected cases. Confirm no internal notes appear.
  • Fix: Default to NOTE when is_private is ambiguous. The cost of an internal note being slightly over-restricted is lower than the cost of internal commentary being customer-visible.

Migration Complexity and Timeline Model

Use this model to estimate engineering effort before committing to a timeline:

Factor Low Complexity High Complexity
Incident volume <10,000 >75,000
Attachments Few, small (<5 MB) Many, large (>15 MB), inline images
Custom fields <10 fields, text/select >25 fields, multi-select, complex types
Comment volume per ticket <5 avg >20 avg, mixed public/private
SWSD modules in active use Incidents only Problems, changes, service requests
Agent count <20 >100, multiple teams
Zero-downtime requirement No Yes

Rough engineering hour estimates (custom pipeline, not tool-assisted):

  • Extraction script + pagination + rate limiting: 8–16 hours
  • Transformation and field mapping logic: 12–24 hours
  • Attachment handling + inline image rewriting: 8–20 hours
  • Bulk import with timestamp preservation: 8–12 hours
  • Idempotency ledger + retry logic: 4–8 hours
  • Delta sync logic: 8–12 hours
  • Validation and reconciliation tooling: 6–10 hours
  • Test runs + debugging: 16–40 hours

Total range: 70–140 engineering hours for a clean, production-grade pipeline. This is the foundation for the 2–4 week calendar timeline, which includes coordination, test cycles, stakeholder review, and a controlled cutover window.

When This Migration Is the Wrong Call

Don't move from SWSD to Kayako if:

  • Your team depends on change management workflows with approval boards
  • You actively use CMDB dependency mapping for root cause analysis
  • You need ITIL-compliant problem management with linked incidents
  • Your compliance requirements mandate asset tracking within your helpdesk
  • You have more than 5 active integrations pulling from SWSD's CMDB or change APIs

In these cases, ServiceNow or Freshservice is a better fit. See our SWSD to ServiceNow migration guide for that path.

Migration Complexity Assessment

Writing a script to move 500 tickets is a weekend project. A production-grade migration pipeline that handles inline image rewriting, public/private comment separation, attachment buffering, rate-limit backoffs, timestamp preservation, idempotency, and delta syncs for 100,000+ tickets requires 70–140 engineering hours for the pipeline alone, plus test cycles and coordination time.

Self-service is realistic if: ticket volume is under 10,000, attachments are few and small, custom fields are under 10, agents number under 20, and the team has no active dependency on problems or changes.

The project becomes infrastructure work if: volume exceeds 50,000 tickets, inline images exist in comments, multi-team routing needs verification, or a zero-downtime cutover is required. The four highest-risk failure modes in that scenario: timestamp loss on imported conversations, duplicate records from retry logic, private note leakage, and silent data overwrites from Kayako's PUT-only update model.

For more guidance on mapping data between helpdesk platforms, see our data mapping guide with CSV templates.

Frequently Asked Questions

Can I preserve original timestamps when migrating from SolarWinds Service Desk to Kayako?
Yes, but only through Kayako's bulk cases endpoint (POST /api/v1/bulk/cases.json), which supports writable created_at and updated_at for both cases and posts. The single-case create endpoint stamps the current server time and does not accept historical timestamps.
What SolarWinds Service Desk data cannot be migrated to Kayako?
CMDB/asset records, change records, release records, service catalog items, approval chains, linked incident-to-problem relationships, SLA policies, automations, and dashboard configurations have no equivalent in Kayako and cannot be migrated programmatically.
How should private comments migrate from SolarWinds Service Desk to Kayako?
Map SWSD public comments to public case posts (channel: MAIL) and SWSD private comments to Kayako NOTE posts or staff-only notes. A visibility mapping mistake is the easiest way to expose internal troubleshooting to customers.
What are the API rate limits for SolarWinds Service Desk and Kayako?
Neither platform publicly documents specific rate-limit thresholds. In practice, pace SWSD extraction at 2-3 requests per second and Kayako write operations at 30-40 requests per minute. Both APIs return HTTP 429 when limits are exceeded — Kayako includes a Retry-After header.
How long does a SolarWinds Service Desk to Kayako migration take?
For 30,000–100,000 incidents including mapping, test runs, and cutover, expect 2–4 weeks DIY. A managed migration service can typically compress the timeline to 5–7 business days.

More from our Blog