---
title: "SolarWinds Service Desk to Kayako Migration: Technical Guide"
slug: solarwinds-service-desk-to-kayako-migration-technical-guide
date: 2026-08-12
author: Nachi
categories: [SolarWinds Service Desk, Migration Guide, Help Desk]
excerpt: "Technical guide to migrating from SolarWinds Service Desk to Kayako — covering API extraction, data model mapping, timestamp preservation, and cutover strategy."
tldr: "Only SWSD incidents map to Kayako conversations. Use the bulk cases endpoint for timestamp preservation. Archive ITSM-only objects instead of forcing a fake 1:1 mapping."
canonical: https://clonepartner.com/blog/solarwinds-service-desk-to-kayako-migration-technical-guide/
---

# SolarWinds Service Desk to Kayako Migration: Technical Guide


# SolarWinds Service Desk to Kayako Migration: Technical Guide

> [!NOTE]
> **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](https://clonepartner.com/blog/blog/solarwinds-service-desk-to-enchant-migration-technical-guide/) built around hardware assets, CMDB records, and ITIL processes into a platform designed for external customer support and [omnichannel conversations](https://clonepartner.com/blog/blog/solarwinds-service-desk-to-dixa-migration-technical-guide/). 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.

```python
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.

```python
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](https://clonepartner.com/blog/blog/how-to-export-data-from-solarwinds-service-desk-methods-limits/).

## 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`.

```python
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](https://clonepartner.com/blog/blog/how-to-export-data-from-kayako-api-limits-methods-portability/).

## 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 []`.

```yaml
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:**
```json
// 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:**
```json
// 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: false` → `channel: MAIL` (public case post). Map `is_private: true` → `channel: 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:

```python
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.

```python
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.

```python
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.

> [!CAUTION]
> **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.
>
> ```python
> 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](https://clonepartner.com/blog/blog/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](https://clonepartner.com/blog/blog/how-to-migrate-from-solarwinds-service-desk-to-servicenow/) 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](https://clonepartner.com/blog/blog/your-ultimate-guide-to-data-mapping-for-a-flawless-help-desk-data-migration-with-csv-templates/).

> ClonePartner handles SWSD-to-Kayako migrations end-to-end — extraction, field mapping, timestamp preservation, and zero-downtime cutover. If you'd rather ship this in days instead of weeks, book a 30-minute call with our engineering team.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

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