Skip to content

Puzzel Case Management to Kayako Migration: Technical Guide

A technical guide covering API extraction, data model mapping, and validation for migrating from Puzzel Case Management to Kayako — with architecture decisions and edge cases.

Wahab Wahab · · 33 min read
Puzzel Case Management 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

Puzzel Case Management to Kayako Migration: Technical Guide

Info

TL;DR: Puzzel Case Management → Kayako Migration

A Puzzel Case Management to Kayako migration is a data-model translation, not a CSV copy job. Puzzel's CSV export captures ticket metadata but strips full message threads, notes, and attachments — you must extract through the Puzzel REST API (Swagger-documented per tenant as of September 2025). On the Kayako side, cases are created through the /api/v1/cases.json endpoint with Basic Auth and per-minute rate limits. Puzzel's Teams, Categories, Forms, and Form Fields do not map 1:1 to Kayako's Teams, Tags, Custom Fields, and Organizations — each requires structural mapping decisions. Smart Rules, SLAs, and automated workflows cannot be migrated programmatically. A typical mid-size migration (10,000–50,000 tickets) takes 2–4 weeks, roughly broken into: 3–4 days for planning and mapping, 3–5 days for extraction, 3–5 days for transformation and test loads, 2–3 days for production loading and delta sync, and 2–3 days for validation and cutover.

What Is a Puzzel Case Management to Kayako Migration?

A Puzzel Case Management to Kayako migration moves tickets, contacts, organisations, categories, tags, notes, attachments, and conversation history from Puzzel Case Management into Kayako while preserving message threads, timestamps, and relational integrity.

Puzzel Case Management is a Case/Ticket management tool that provides your contact centre or helpdesk with the ability to manage written interactions in a secure and efficient way. Each email in the queue is converted into a support ticket and assigned a unique case/ticket ID which is then filtered, categorised and distributed to the right team or agent. It sits inside the broader Puzzel CX ecosystem — designed for contact centres that need skill-based routing, SLA tracking, and multi-channel queuing across email, SMS, and API integrations.

Kayako is a unified customer service platform that organises support around conversations (referred to as "cases" in its API — this guide uses "case" and "conversation" interchangeably to match Kayako's own documentation). The Kayako API is a proprietary REST API implementation, based on resources — an object that the API endpoint fetches and/or manipulates. Kayako supports custom fields on conversations, users, and organizations, trigger-based automations, and a Help Center.

Teams move from Puzzel to Kayako for several concrete reasons:

  • Cost simplification: Kayako consolidates ticketing without requiring a full contact centre stack, reducing per-seat costs for teams that don't use Puzzel's voice/omnichannel routing.
  • Conversation-centric UX: Kayako's interface organises support around threaded conversations rather than Puzzel's queue-and-ticket paradigm.
  • Journey tracking: Kayako natively tracks external customer events (website visits, purchases) directly in the conversation timeline — Puzzel requires external integration for this.
  • Organisational alignment: Mergers or platform consolidations where Kayako is the target standard.

If the real requirement is marketing automation or CRM pipeline management, neither platform's documented core model is a leads-and-opportunities system. Those records should stay in a CRM or be represented only as lightweight context in Kayako custom fields.

Core Differences in Data Model

Before writing a single migration script, you need to understand where the two platforms diverge architecturally. These differences drive every mapping decision.

Concept Puzzel Case Management Kayako
Primary record Ticket (with unique case/ticket ID) Case / Conversation
Customer grouping Organisation Organization
Agent grouping Teams (mapped to queues in Puzzel CC) Teams
Classification Categories (multi-tier, required-to-resolve) + Forms + Form Fields Tags + Custom Fields (conversation, user, org) + Types (via forms)
Structured data Form Fields (dropdown, text, date, nested dropdown) Custom Fields (dropdown, text, numeric, date, checkbox, radio, yes/no)
Notes Private/Public notes on tickets Notes (pinned/unpinned) on cases, users, and organizations
Routing Skill-based routing, inbound rules, event rules Triggers and monitors (event-based automations)
SLAs Multi-tier SLA targets (response, resolution) SLA policies (configurable per priority/type)
Automation Smart Rules, Inbound Rules, Event Rules, Response Mappings Triggers, Monitors
Channels Email, SMS, API ticket channels Email, Messenger, Social, API

The most significant structural difference: Puzzel Categories are selected from ticket attributes, with multiple categories per ticket and a configurable number required for resolution. Kayako has no direct equivalent of this multi-tier category system. You'll need to flatten categories into Kayako tags, custom fields, or a combination — and that's a design decision, not a simple field copy. This is detailed in the Field-Level Mapping section below.

Migration Approaches

There are five viable paths. Each carries distinct trade-offs on control, speed, and engineering cost.

1. Native CSV Export / Import

How it works: In Puzzel, go to the Tickets page, use the Ticket Attributes widget to filter the Tickets List for only those tickets you wish to export — for example, filter by "Team: Customer Service" and "Time Period: Last Week". Select tickets and export to CSV. Kayako has no native CSV import UI, so you'd still need to parse CSVs and feed them into the Kayako API.

When to use it: Quick one-time snapshots of metadata for small datasets (<1,000 tickets). Not suitable for full conversation history.

Pros: No API credentials needed for extraction. Fast for small subsets. Useful as reconciliation checkpoints even if the main migration is API-led.

Cons: CSV exports from Puzzel include ticket metadata (status, category, team, priority) but do not include full message threads, notes, or attachments. Timestamps are often overridden by the import date. Kayako still requires API scripting on the load side.

Scalability: Small datasets only. No pagination control — you select tickets page by page.

Complexity: Low (extraction) / Medium (loading).

2. API-Based Migration (Puzzel API → Kayako API)

How it works: Extract tickets, contacts, organisations, categories, and full message threads from Puzzel's REST API. Transform data in-memory or via an intermediate data store. Load into Kayako using POST /api/v1/cases.json, POST /api/v1/users.json, and POST /api/v1/organizations.json.

On the Puzzel side, API ticket channels require selecting an authentication level — Global (access to all tickets) or Organisation-specific — with Basic Token or OAuth Token authentication.

On the Kayako side, to retrieve cases via an API call, you need an agent, admin, or collaborator role user account. Use Basic Auth as the authentication type. By default, API calls return 10 results per page. You can retrieve up to 100 conversations by setting a limit of 100.

When to use it: Any migration where you need full conversation history, attachments, and relational integrity. This is the recommended approach for production migrations.

Pros: Full control. Preserves message threads, notes, timestamps. Handles attachments. Supports incremental/delta loads.

Cons: Requires significant development effort. Kayako API rate limits are defined per minute — you need throttling logic. Exact rate limits are not publicly documented and vary by plan; you must benchmark against your own Kayako tenant during UAT. Puzzel API documentation must be accessed from your tenant's Swagger UI. Script errors can result in duplicated data if not properly handled with idempotency checks.

Scalability: Enterprise-grade with proper batching and retry logic.

Complexity: High.

3. Third-Party Migration Tools

No major migration tool (Help Desk Migration, Import2, etc.) currently offers a verified direct Puzzel Case Management → Kayako connector. Both platforms are niche enough that third-party coverage is limited. Kayako's own support documentation points customers toward Help Desk Migration for some migration scenarios and notes the service can ingest from API, CSV, XML, or JSON when needed.

If a tool claims support, verify it handles full thread history, not just metadata. Confirm edge-case support before committing — complex Puzzel Forms and deep relational mappings are where generic tools break down.

Complexity: Low–Medium if available, but availability is the constraint.

4. Custom ETL Pipeline

How it works: Build an extract-transform-load pipeline using Python, Node.js, or a data engineering framework (Airflow, Prefect, AWS Glue, Azure Data Factory). Extract from Puzzel API, stage in a relational database or flat files, run transformation scripts, and load into Kayako API.

When to use it: Large-scale migrations (50,000+ tickets), complex transformations (category flattening, field splitting, multi-system merges), or when you need an auditable intermediate layer for compliance.

Pros: Full auditability. Replayable. Handles complex transformations cleanly. Intermediate staging enables validation before loading.

Cons: Highest engineering and infrastructure investment. Overkill for standard helpdesk migrations under 50,000 tickets.

Scalability: Best for enterprise volumes.

Complexity: High.

5. Middleware / Integration Platforms (Zapier, Make)

How it works: Use Zapier, Make, or Workato to connect Puzzel and Kayako via their APIs. Trigger-based flows extract records from one and push to the other.

When to use it: Ongoing sync of new tickets during a phased cutover, or very small one-time transfers. Not suitable for bulk historical migration.

Pros: Low code. Fast to set up for small volumes.

Cons: No native Puzzel Case Management connector on most platforms — you'd use generic HTTP/webhook modules. No bulk extraction. Rate limit and timeout risks. Extremely slow for historical data — one record at a time. High cost per task at volume.

Scalability: Small datasets only (<500 records for one-time; low-volume for ongoing sync).

Complexity: Low–Medium.

Comparison Table

Method Best Fit Full History Attachments Scalability Eng Effort Complexity
CSV Export/Import Contacts & metadata only Low Low–Medium Low
API-to-API Most production migrations High High High
Third-Party Tools Standard setups (if available) Varies Varies Medium Low Low–Medium
Custom ETL Enterprise + compliance High Very High High
Middleware (Zapier/Make) Phased cutovers, ongoing sync ❌ (new only) Limited Low Low Low–Medium

Recommendations by Scenario

  • Small business (<5,000 tickets), limited engineering: CSV export for metadata plus manual backfill of critical threads, or engage a managed migration service.
  • Mid-market (5,000–50,000 tickets), one-time migration: API-to-API with a Python or Node.js script. Budget 2–4 weeks.
  • Enterprise (50,000+ tickets), dedicated dev team: Custom ETL with staging database, automated validation, and delta sync.
  • Ongoing sync (running both platforms in parallel): API-to-API with a scheduled job. Middleware is too brittle for this at scale.

Pre-Migration Planning

A successful migration is 80% planning and 20% execution. Start with a data audit, not a script.

Data Audit Checklist

Inventory what you have in Puzzel and what you need in Kayako:

Data Object Audit Questions
Tickets Total count? Status distribution (open, pending, on hold, resolved, closed)? Date range?
Organisations How many? Custom attributes? Inactive ones to archive?
Contacts/Customers Total count? Duplicates by email? Associated to organisations?
Categories How many tiers? How many categories per tier? Required-to-resolve rules?
Forms & Form Fields How many forms? What field types (dropdown, text, date, nested)? Unused fields?
Tags Total unique tags?
Notes Private vs public notes? Volume per ticket?
Attachments Total storage size? File types? Per-ticket volume? Max individual file size?
Templates How many? Actively used?
Smart Rules / Event Rules How many? What triggers and actions?
SLAs How many policies? Per-team or per-category?

Identify What Not to Migrate

Not everything needs to move. Common candidates for exclusion:

  • Resolved/closed tickets older than 2–3 years (archive, don't migrate)
  • Test tickets and internal-only records
  • Unused categories or forms
  • Disabled users and orphaned contacts
  • Spam tickets

Remove dead data early, then define scope: full history, last 24 months, or active-only plus archive.

Migration Strategy

  • Big bang: Migrate everything in a single cutover window. Best for small datasets. Risk: if something fails, you're stuck between two systems.
  • Phased: Migrate by team or category. Each phase is independently validated. Lower risk, longer timeline.
  • Incremental: Migrate historical data first, then run a delta sync to capture new tickets created during the migration window. Recommended for live support operations.

For most helpdesks, Big Bang paired with a final delta sync is the safest way to avoid split-brain data scenarios. The pattern is: pilot → full test → delta sync → UAT → cutover.

Risk Mitigation

Before you start:

  • Name migration owners (technical lead + business stakeholder)
  • Define schema freeze windows (at least two weeks before migration — no new custom fields, categories, or forms in Puzzel)
  • Pre-approve fallback routing (where do tickets go if Kayako is down during cutover?)
  • Decide what gets archived instead of migrated
  • Document rollback criteria: what constitutes a failed cutover, who can revert routing, which batches are safe to rerun

Data Model and Object Mapping

This is where the migration lives or dies. Get the mapping wrong, and everything downstream breaks.

Warning

No clean 1:1 mapping exists from Puzzel categories, forms, and form fields to Kayako forms and custom fields. Decide early what becomes a form, what becomes a typed custom field, what becomes a tag, and what stays in an external archive.

Object-Level Mapping

Puzzel Case Management Kayako Notes
Organisation Organization Near 1:1. Map custom attributes to Kayako org custom fields. Enable domain matching in Kayako to auto-associate future users.
Contact / Customer User (customer role) Map email, name, phone. Puzzel contact groups have no Kayako equivalent — use org or tags. Retain primary email as unique identifier.
Agent / User User (agent/admin role) Map team membership.
Team Team Near 1:1.
Ticket Case / Conversation Core record. Map status, priority, assignee, channel. Store Puzzel Case ID in a Kayako Custom Field (e.g., legacy_puzzel_id) for auditability.
Ticket Message (reply) Post (case message) Preserve sender, timestamp, HTML body.
Note (private) Note Kayako notes are agent-internal by default. Direct mapping.
Note (public) Post Puzzel supports flagging notes as public or private — private notes are not exposed via API channels. Public Puzzel notes should be migrated as case posts in Kayako, not notes, to preserve customer-visible context.
Category Tag or Custom Field Puzzel categories are structured (multi-tier with dropdowns). Flatten to tags for simple use; map to custom dropdown fields for structured data. See detailed options below.
Form Form (conversation type) Kayako forms control which fields appear per conversation type. Rebuild manually.
Form Field Custom Field (conversation) Map field types: dropdown → dropdown, text → text, date → date. Nested dropdowns have no Kayako equivalent — flatten to separate fields.
Tag Tag Direct 1:1 mapping.
Attachment Attachment Binary transfer via API. Upload via POST /api/v1/cases/{id}/attachments (see Attachments section).
Smart Rule / Inbound Rule Trigger Cannot be migrated programmatically. Must be rebuilt manually.
SLA Policy SLA Policy Rebuild manually.

Field-Level Mapping Decisions

Ticket Status: Puzzel uses configurable statuses (typically: New, Open, Pending, On Hold, Resolved, Closed). Kayako uses: New, Open, Pending, Completed, Closed.

Puzzel Status Kayako Status Notes
New New Direct map
Open Open Direct map
Pending Pending Direct map
On Hold Pending Kayako has no native "On Hold" — map to Pending and add a tag on-hold or create a custom field hold_reason to preserve context
Resolved Completed Kayako's "Completed" is the semantic equivalent
Closed Closed Direct map

Priority: Both platforms support priority levels, but naming differs. Map explicitly:

Puzzel Priority Kayako Priority
Low Low
Medium Normal
High High
Critical Urgent

Categories → Tags/Custom Fields: This is the hardest mapping decision. If Puzzel uses a two-tier category like Query Type: Complaint > Billing, you have three options:

  1. Flatten to a single tag: complaint-billing — simplest, but loses the hierarchy and makes filtering less precise.
  2. Map to two separate custom dropdown fields on the Kayako conversation (e.g., category_tier_1: Complaint, category_tier_2: Billing) — preserves the most structure but requires creating custom fields in Kayako before loading. Best for teams that use categories for reporting.
  3. Use only the leaf value as a tag or custom field (billing) — loses parent context, only viable if leaf values are globally unique.

The right answer depends on whether agents actively filter and report by category hierarchy. Run a usage audit in Puzzel: if agents rarely filter by the top tier, option 3 suffices.

Leads / Opportunities: Neither vendor publicly documents a native lead object in the helpdesk layer. If you track leads via Puzzel Forms, map these to Kayako Cases with a custom Type field set to "Lead" — or keep them in a CRM and use Kayako custom fields for read-only context.

Migration Architecture

The reliable pattern is Extract → Stage → Transform → Load.

Data Flow

Puzzel API (Extract) → Staging DB (Raw JSON) → Transform Layer → Kayako API (Load)

Extraction from Puzzel

Puzzel's API documentation is now accessible via Swagger UI from the Help menu within your tenant. This is your primary reference for available endpoints. Puzzel does not publish a public API reference for Case Management — all endpoint paths and payload structures must be confirmed from your tenant's Swagger documentation.

The API supports: creating, reading, modifying, and deleting tickets and all associated metadata; listing tickets with search criteria; filtering by date range; listing categories and forms associated to tickets; organisation list; and tag list.

Key extraction endpoints (accessed via your tenant's Swagger):

  • GET /tickets — paginated ticket list with filters (including modified_after for delta sync — see Delta Sync section)
  • GET /tickets/{id} — full ticket detail including messages, notes, and attachment metadata
  • GET /organisations — organisation list
  • GET /tags — tag list
  • GET /categories — category definitions
  • GET /forms — form definitions with fields

Authentication headers: For Basic Token, include Authorization: Basic {base64(username:token)} in your request headers. For OAuth Token, include Authorization: Bearer {access_token}. The specific token type depends on how your API ticket channel is configured in Puzzel.

Pagination: Puzzel's ticket list endpoint supports offset/limit pagination. The default page size and maximum vary by tenant configuration — start with ?limit=50&offset=0 and increment. If you have very large classification trees, Puzzel's June 2025 release added category-choice pagination with support for fetching up to 1,000 choices per request using ?per_page=1000.

Store raw JSON payloads in an intermediate staging database (PostgreSQL or SQLite for small migrations, MongoDB for complex nested structures). This prevents re-querying the Puzzel API if a transformation step fails and gives you an auditable snapshot of the source data.

Loading into Kayako

For writing data:

  • POST /api/v1/users.json — create users
  • POST /api/v1/organizations.json — create organizations
  • POST /api/v1/cases.json — create cases
  • POST /api/v1/cases/{id}/notes.json — add notes
  • POST /api/v1/cases/{id}/posts.json — add messages/replies
  • POST /api/v1/cases/{id}/attachments — upload attachments (multipart form data)

Kayako API rate limits are defined per minute. If you exceed the rate limit, you get HTTP 429 with a Retry-After header holding the number of seconds before you can retry. Exact limits are not publicly documented and vary by plan. During UAT, run a burst test against your Kayako tenant to determine your actual ceiling — a common approach is to send sequential requests and record when 429s first appear.

User deduplication behavior: When you POST /api/v1/users.json with an email that already exists, Kayako returns a 409 Conflict error rather than creating a duplicate. Your script must handle this: catch the 409, extract the existing user's ID from the error response or a follow-up GET /api/v1/users.json?email={email} query, and use that ID for subsequent operations. This is critical for retry safety — if a migration batch fails partway through and you re-run it, users created in the first pass won't be duplicated.

Warning

Kayako Classic vs Kayako (New): If you're migrating to Kayako Classic (the self-hosted version), note that there's no API rate limit implemented in Kayako Classic. The API surface also differs significantly — Classic uses XML-based endpoints like /Tickets/Ticket. Confirm which Kayako version you're targeting before writing any integration code. Everything in this guide assumes Kayako (New) unless explicitly stated otherwise.

Authentication

Puzzel: Select your Token Type — Basic Token or OAuth Token — when creating an API ticket channel.

Kayako: When sending an API call with Postman, add your credentials (Kayako username and password) to the Authorization tab. Use Basic Auth as the authentication type. The header format is Authorization: Basic {base64(email:password)}.

Handling Large Datasets

  • Puzzel: Paginate through ticket lists using offset/limit parameters via the Swagger-documented endpoints. Extract in batches of 50–100. Store each batch's raw JSON immediately before processing.
  • Kayako: By default, API calls return 10 results per page. You can increase to 100 per page using the limit argument. For writes, there is no bulk create endpoint — cases, users, and organizations must be created one at a time. Implement a single-threaded write loop with exponential backoff and jitter.
  • CSV reconciliation: Use Puzzel CSV exports as audit checkpoints even if the main migration is API-led. Compare ticket counts and status distributions between CSV and API extractions to catch extraction gaps.

Delta Sync Implementation

Delta sync captures tickets created or modified in Puzzel during the migration window. The implementation depends on Puzzel's API filtering capabilities:

  1. Timestamp-based polling (preferred): Use GET /tickets?modified_after={iso_datetime} to fetch only tickets modified since your last extraction. Store the timestamp of your last successful extraction run. After the historical load completes, run delta sync repeatedly (e.g., every 30 minutes) until the cutover moment.

  2. Full re-extraction with diff: If Puzzel's API doesn't support modified_after filtering on your tenant, extract the full ticket list and compare IDs and updated_at timestamps against your staging database. Process only new or changed records.

  3. Cutover sequence:

    • T-24h: Final delta sync before cutover
    • T-0: Freeze Puzzel (stop accepting new tickets by disabling inbound channels)
    • T+15min: Run final delta sync
    • T+30min: Validate delta sync results
    • T+45min: Switch DNS/email routing to Kayako
    • T+1h: Confirm new tickets arriving in Kayako

The goal is to minimize the window where tickets might be created in Puzzel but not yet synced to Kayako.

Step-by-Step Migration Process

Step 1: Extract Data from Puzzel

Extract in dependency order — this is critical because downstream records reference upstream IDs:

  1. Organisations — must exist before tickets reference them
  2. Contacts / Customers — linked to organisations
  3. Categories, Forms, Form Fields — structural metadata
  4. Tags — flat list
  5. Tickets — with full message threads, notes, and attachments

For each ticket, extract:

  • Ticket metadata (status, priority, team, assignee, created/updated timestamps)
  • All messages in thread order (sender, body HTML, timestamp)
  • Notes (with public/private flag)
  • Category selections (all tiers)
  • Form field values
  • Attachments (binary content + metadata: filename, MIME type, size)

Step 2: Transform Data

  • Map Puzzel status values to Kayako status values (including On Hold → Pending + tag)
  • Flatten multi-tier categories to tags or custom field values
  • Convert Puzzel Form Fields to Kayako custom field payloads
  • Normalise email addresses (lowercase, trim whitespace)
  • Deduplicate contacts by email (merge records, keep the most complete profile)
  • Sanitize HTML content and replace inline image CID references with hosted URLs (see CID Mapping section)
  • Convert attachment references to multipart form data for Kayako upload
  • Generate the ID mapping table skeleton

Step 3: Pre-Create Structure in Kayako

Before loading data, manually or programmatically create:

  • Teams (matching Puzzel team names)
  • Custom fields (conversation, user, organization) — including legacy_puzzel_id (text), original_created_at (date), and any fields mapped from Puzzel Forms
  • Forms (conversation types) — matching Puzzel Forms
  • SLA policies — rebuilt from Puzzel's SLA configuration
  • Tags (can be auto-created on first use in Kayako)

Kayako supports organization fields for tracking data about supported organizations, and user fields for adding detail to both staff and customer profiles — with options for customer visibility and editability.

Step 4: Load Data into Kayako

Load in strict dependency order:

  1. Organizations → capture Kayako IDs, store in mapping table
  2. Users (customers and agents) → associate with organizations, handle 409 Conflict for existing emails
  3. Cases → create with subject, status, priority, assignee, requester, type, custom fields (including legacy_puzzel_id and original_created_at)
  4. Posts (messages) → add to each case in chronological order
  5. Notes → add to each case (private notes only; public notes become posts)
  6. Attachments → attach to relevant posts or cases

If you create a Case before the User exists, Kayako will reject the payload or assign it to a default admin — so ordering is non-negotiable.

Step 5: Handle Timestamp Preservation

Kayako's API behavior on created_at is version-dependent. Test this explicitly against your Kayako tenant before the production run:

  1. Create a test case via POST /api/v1/cases.json with a created_at field set to a past date (e.g., "created_at": "2023-01-15T10:30:00Z").
  2. Retrieve the case via GET /api/v1/cases/{id}.json and check whether created_at reflects your specified date or the API call time.

If the API accepts created_at overrides: Include the original Puzzel creation timestamp in the case creation payload.

If the API ignores created_at (most common with Kayako New): Store the original Puzzel creation date in a custom field (original_created_at, type: date). This preserves timeline accuracy for reporting and agent reference. The Kayako created_at will reflect the migration date, which is acceptable as long as agents know to reference the custom field for the true ticket age.

Step 6: Rebuild Relationships

Maintain an ID mapping table (puzzel_id → kayako_id) for every object type. Use this to:

  • Link users to organizations
  • Link cases to requesters and assignees
  • Link posts to the correct case
  • Associate attachments to the right post

Store puzzel_id, kayako_id, object_type, migration_timestamp, and migration_batch_id for every record. This mapping table is critical for the delta sync, rollback, selective deletion, and post-migration troubleshooting.

Step 7: Validate and Delta Sync

Run a delta sync to capture tickets created during the migration window (see Delta Sync Implementation above). Validate before cutover (see the Validation section below).

Code Example: Creating a Case and Adding Posts

import requests
import time
import logging
 
logger = logging.getLogger(__name__)
 
KAYAKO_DOMAIN = "yourcompany.kayako.com"
KAYAKO_EMAIL = "admin@yourcompany.com"
KAYAKO_PASSWORD = "your-password"
BASE_URL = f"https://{KAYAKO_DOMAIN}/api/v1"
AUTH = (KAYAKO_EMAIL, KAYAKO_PASSWORD)
 
def create_case(subject, requester_id, contents, assignee_id=None,
                tags=None, custom_fields=None, max_retries=3):
    url = f"{BASE_URL}/cases.json"
    payload = {
        "subject": subject,
        "requester_id": requester_id,
        "channel": "MAIL",
        "contents": contents,
    }
    if assignee_id:
        payload["assigned_agent_id"] = assignee_id
    if tags:
        payload["tags"] = tags
    if custom_fields:
        payload["custom_fields"] = custom_fields
 
    for attempt in range(max_retries):
        response = requests.post(url, json=payload, auth=AUTH)
 
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            logger.warning(f"Rate limited. Retrying after {retry_after}s (attempt {attempt+1})")
            time.sleep(retry_after)
            continue
 
        if response.status_code >= 500 and attempt < max_retries - 1:
            wait = (2 ** attempt) + (time.time() % 1)  # exponential backoff + jitter
            logger.warning(f"Server error {response.status_code}. Retrying after {wait:.1f}s")
            time.sleep(wait)
            continue
 
        if response.status_code >= 400:
            logger.error(f"Failed to create case: {response.status_code} {response.text}")
            return None  # Add to dead-letter queue
 
        response.raise_for_status()
        return response.json()["data"]
 
    logger.error(f"Exhausted retries for case: {subject}")
    return None
 
def add_post_to_case(case_id, contents, creator_id, max_retries=3):
    url = f"{BASE_URL}/cases/{case_id}/posts.json"
    payload = {
        "contents": contents,
        "creator_id": creator_id,
        "source": "MAIL"
    }
    for attempt in range(max_retries):
        response = requests.post(url, json=payload, auth=AUTH)
 
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
 
        if response.status_code >= 500 and attempt < max_retries - 1:
            time.sleep((2 ** attempt) + (time.time() % 1))
            continue
 
        if response.status_code >= 400:
            logger.error(f"Failed to add post to case {case_id}: {response.status_code} {response.text}")
            return None
 
        response.raise_for_status()
        return response.json()["data"]
 
    return None
 
def upload_attachment(case_id, filepath, filename, mime_type, max_retries=3):
    """Upload attachment to a case via multipart form data."""
    url = f"{BASE_URL}/cases/{case_id}/attachments"
    for attempt in range(max_retries):
        with open(filepath, 'rb') as f:
            files = {'file': (filename, f, mime_type)}
            response = requests.post(url, files=files, auth=AUTH)
 
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            continue
 
        if response.status_code >= 500 and attempt < max_retries - 1:
            time.sleep((2 ** attempt) + (time.time() % 1))
            continue
 
        if response.status_code >= 400:
            logger.error(f"Failed to upload attachment to case {case_id}: "
                        f"{response.status_code} {response.text}")
            return None
 
        response.raise_for_status()
        return response.json()["data"]
 
    return None
 
def find_or_create_user(email, full_name, org_id=None):
    """Create user, handling 409 Conflict for existing emails."""
    url = f"{BASE_URL}/users.json"
    payload = {"email": email, "full_name": full_name}
    if org_id:
        payload["organization_id"] = org_id
 
    response = requests.post(url, json=payload, auth=AUTH)
 
    if response.status_code == 409:
        # User exists — look up by email
        search_url = f"{BASE_URL}/users.json?email={email}"
        search_resp = requests.get(search_url, auth=AUTH)
        search_resp.raise_for_status()
        users = search_resp.json().get("data", [])
        if users:
            return users[0]
        logger.error(f"409 on user create but email lookup failed: {email}")
        return None
 
    if response.status_code >= 400:
        logger.error(f"Failed to create user {email}: {response.status_code} {response.text}")
        return None
 
    response.raise_for_status()
    return response.json()["data"]
 
# Example usage
user = find_or_create_user("customer@example.com", "Jane Smith", org_id=42)
if user:
    case = create_case(
        subject="Login Issue (Migrated)",
        requester_id=user["id"],
        contents="User cannot log into the portal.",
        custom_fields={
            "legacy_puzzel_id": "CASE-9921",
            "original_created_at": "2024-03-15T09:22:00Z"
        }
    )
Tip

Error handling: Always log the full response body on failures — Kayako returns structured error objects with code and message fields. Store failed records in a dead-letter queue (a JSON file or database table) rather than stopping the entire migration. Review and retry failed records after the main batch completes.

Full Migration Script Outline

A production migration script typically has these modules:

# migration.py — Puzzel Case Management to Kayako Migration
import logging
import json
import sqlite3
from datetime import datetime
 
logging.basicConfig(level=logging.INFO, filename="migration.log")
logger = logging.getLogger(__name__)
 
# --- Configuration ---
PUZZEL_BASE_URL = "https://your-tenant.puzzel-ticketing.com/api"
PUZZEL_TOKEN = "your-puzzel-api-token"  # Base64(username:token) for Basic
KAYAKO_DOMAIN = "yourcompany.kayako.com"
KAYAKO_AUTH = ("admin@yourcompany.com", "password")
 
# --- ID Mapping Store (SQLite for durability) ---
db = sqlite3.connect("migration_map.db")
db.execute("""CREATE TABLE IF NOT EXISTS id_map (
    object_type TEXT, puzzel_id TEXT, kayako_id TEXT,
    batch_id TEXT, migrated_at TEXT,
    PRIMARY KEY (object_type, puzzel_id))""")
db.execute("""CREATE TABLE IF NOT EXISTS dead_letter (
    object_type TEXT, puzzel_id TEXT, payload TEXT,
    error TEXT, attempted_at TEXT)""")
 
def store_mapping(object_type, puzzel_id, kayako_id, batch_id):
    db.execute("INSERT OR REPLACE INTO id_map VALUES (?,?,?,?,?)",
               (object_type, str(puzzel_id), str(kayako_id),
                batch_id, datetime.utcnow().isoformat()))
    db.commit()
 
def get_kayako_id(object_type, puzzel_id):
    row = db.execute("SELECT kayako_id FROM id_map WHERE object_type=? AND puzzel_id=?",
                     (object_type, str(puzzel_id))).fetchone()
    return row[0] if row else None
 
# --- Step 1: Extract from Puzzel ---
def extract_organisations():
    """Paginate through Puzzel organisations endpoint.
    Headers: Authorization: Basic {PUZZEL_TOKEN}
    Endpoint: GET {PUZZEL_BASE_URL}/organisations?limit=50&offset=0
    """
    pass
 
def extract_contacts():
    """Paginate through Puzzel contacts."""
    pass
 
def extract_tickets(batch_size=50, modified_after=None):
    """Paginate through tickets. For each, fetch messages, notes, attachments.
    For delta sync, pass modified_after as ISO datetime string.
    Endpoint: GET {PUZZEL_BASE_URL}/tickets?limit={batch_size}&offset=0
              &modified_after={modified_after}
    """
    pass
 
# --- Step 2: Transform ---
def map_status(puzzel_status):
    STATUS_MAP = {
        "New": "NEW", "Open": "OPEN", "Pending": "PENDING",
        "On Hold": "PENDING",  # Add on-hold tag separately
        "Resolved": "COMPLETED", "Closed": "CLOSED"
    }
    return STATUS_MAP.get(puzzel_status, "OPEN")
 
def map_priority(puzzel_priority):
    PRIORITY_MAP = {
        "Low": "LOW", "Medium": "NORMAL",
        "High": "HIGH", "Critical": "URGENT"
    }
    return PRIORITY_MAP.get(puzzel_priority, "NORMAL")
 
def flatten_categories(categories):
    """Convert multi-tier categories to tags or custom field values."""
    return ["-".join(cat["path"]).lower().replace(" ", "-") for cat in categories]
 
def needs_on_hold_tag(puzzel_status):
    return puzzel_status == "On Hold"
 
# --- Step 3: Load into Kayako ---
def create_kayako_org(name, custom_fields=None):
    """POST /api/v1/organizations.json with rate limit handling."""
    pass
 
def create_kayako_user(email, full_name, org_id=None):
    """POST /api/v1/users.json — handle 409 Conflict for existing emails."""
    pass
 
def create_kayako_case(subject, requester_id, contents, status,
                        priority, tags, custom_fields):
    """POST /api/v1/cases.json with retry logic."""
    pass
 
def add_kayako_post(case_id, contents, creator_id, created_at):
    """POST /api/v1/cases/{id}/posts.json — add message to case."""
    pass
 
def add_kayako_note(case_id, body, creator_id):
    """POST /api/v1/cases/{id}/notes.json — add internal note."""
    pass
 
def upload_kayako_attachment(case_id, filepath, filename, mime_type):
    """POST /api/v1/cases/{id}/attachments — multipart upload."""
    pass
 
# --- Step 4: Orchestrate ---
def run_migration(batch_id="batch_001", modified_after=None):
    logger.info(f"Starting migration batch {batch_id}...")
 
    # Phase 1: Organisations
    for org in extract_organisations():
        existing = get_kayako_id("org", org["id"])
        if existing:
            continue  # Idempotent — skip if already migrated
        kayako_org = create_kayako_org(org["name"], org.get("custom_attributes"))
        if kayako_org:
            store_mapping("org", org["id"], kayako_org["id"], batch_id)
 
    # Phase 2: Users
    for contact in extract_contacts():
        existing = get_kayako_id("user", contact["id"])
        if existing:
            continue
        org_id = get_kayako_id("org", contact.get("organisation_id"))
        kayako_user = create_kayako_user(contact["email"], contact["name"], org_id)
        if kayako_user:
            store_mapping("user", contact["id"], kayako_user["id"], batch_id)
 
    # Phase 3: Tickets → Cases
    for ticket in extract_tickets(modified_after=modified_after):
        existing = get_kayako_id("ticket", ticket["id"])
        if existing:
            continue
        requester_id = get_kayako_id("user", ticket["customer_id"])
        if not requester_id:
            logger.error(f"No Kayako user for Puzzel customer {ticket['customer_id']}")
            continue
 
        tags = flatten_categories(ticket.get("categories", []))
        tags.extend(ticket.get("tags", []))
        if needs_on_hold_tag(ticket["status"]):
            tags.append("on-hold")
 
        custom_fields = ticket.get("form_fields", {})
        custom_fields["legacy_puzzel_id"] = str(ticket["id"])
        custom_fields["original_created_at"] = ticket.get("created_at")
 
        case = create_kayako_case(
            subject=ticket["subject"],
            requester_id=requester_id,
            contents=ticket["messages"][0]["body"],
            status=map_status(ticket["status"]),
            priority=map_priority(ticket["priority"]),
            tags=tags,
            custom_fields=custom_fields
        )
        if not case:
            continue
        store_mapping("ticket", ticket["id"], case["id"], batch_id)
 
        # Phase 4: Messages (skip first — already used as case contents)
        for msg in ticket["messages"][1:]:
            creator_id = get_kayako_id("user", msg["sender_id"])
            add_kayako_post(case["id"], msg["body"], creator_id, msg["created_at"])
 
        # Phase 5: Notes
        for note in ticket.get("notes", []):
            creator_id = get_kayako_id("user", note["user_id"])
            if note["is_private"]:
                add_kayako_note(case["id"], note["body"], creator_id)
            else:
                add_kayako_post(case["id"], note["body"], creator_id, note["created_at"])
 
        # Phase 6: Attachments
        for attachment in ticket.get("attachments", []):
            upload_kayako_attachment(
                case["id"], attachment["local_path"],
                attachment["filename"], attachment["mime_type"]
            )
 
    logger.info(f"Migration batch {batch_id} complete.")
 
if __name__ == "__main__":
    run_migration()

Edge Cases and Challenges

Migrations are rarely straightforward. Anticipate these specific hurdles:

Inline Images (CID Mapping)

Puzzel emails often contain inline images referenced by Content-ID (cid:image001.png). When migrating to Kayako:

  1. Download the inline image from Puzzel's API (the message response includes attachment/inline-image URLs)
  2. Upload the image to Kayako as an attachment, capturing the returned URL
  3. Run a regex replace on the HTML body to swap cid: references with the new Kayako-hosted URL
import re
 
def replace_cid_references(html_body, cid_url_map):
    """Replace cid: references with Kayako-hosted URLs.
    cid_url_map: {"image001.png": "https://yourcompany.kayako.com/attachments/abc123"}
    """
    for cid, url in cid_url_map.items():
        html_body = re.sub(
            rf'cid:{re.escape(cid)}',
            url,
            html_body,
            flags=re.IGNORECASE
        )
    return html_body

Use Puzzel's API-exposed inline-image links rather than trying to parse raw MIME email.

Duplicate Contacts

Puzzel may have duplicate contacts (same person with different email addresses, or different contact records with the same email). Deduplicate in your staging database before loading:

  • Same email, different names: merge to a single record, keeping the most complete profile
  • Same person, different emails (e.g., personal and work): decide whether to merge (losing one email as primary) or create two Kayako users

Kayako will return 409 Conflict on duplicate email creation — your find_or_create_user function must handle this gracefully.

Multi-Tier Categories

Puzzel supports categories like Query Type > Billing > Refund Request. Kayako has no hierarchical tag or field system. The three mapping options were detailed above (flatten to tag, split to multiple custom dropdowns, or use leaf only). Additional considerations:

  • If Puzzel enforces more than 3 category tiers, the number of required Kayako custom dropdown fields may become unwieldy — consider flattening the deepest tiers
  • Run a distinct count on category paths before deciding: if there are 200+ unique leaf values, tags may be more manageable than a dropdown with 200 options

Timestamp Preservation

As detailed in Step 5, Kayako may not accept created_at overrides on case creation. Test this early. The fallback — storing original timestamps in a custom field — is reliable but means Kayako's built-in "sort by created date" won't reflect the original timeline. Document this limitation for agents.

Public vs Private Notes

When adding a note to a Puzzel ticket via API, it can be flagged as public or private — private notes are not exposed via API channels. In Kayako, notes are agent-internal by default. Public notes from Puzzel must be migrated as case posts (messages) in Kayako, not notes — otherwise customer-visible context is lost. Tag migrated public notes with a consistent prefix (e.g., [Migrated Note]) so agents can distinguish them from original messages.

Attachment Handling

Kayako enforces attachment size limits per API request. The exact limit depends on your Kayako plan and server configuration — verify against your tenant's documentation or by testing with progressively larger files. Puzzel itself warns that outbound emails over 40MB can fail into ERROR status, so most Puzzel attachments will be under 40MB.

For files exceeding Kayako's limit:

  1. Host them externally (e.g., AWS S3 bucket with time-limited presigned URLs)
  2. Append a secure link to the Kayako case as a note: "Large attachment: [filename] — [link]"
  3. Log all oversized files for post-migration review

Merged or Linked Tickets

If your Puzzel instance uses ticket linking (parent/child relationships), Kayako doesn't have a native ticket linking feature. Preserve these relationships as notes on the target case: "Linked to original Puzzel ticket #12345 (Kayako case #{kayako_id})." Use the ID mapping table to include the Kayako case ID if the linked ticket has already been migrated.

API Failures and Retries

If you exceed Kayako's rate limit, you get HTTP 429 with a Retry-After header. Implement exponential backoff with jitter — never retry immediately:

def backoff_delay(attempt, base=2, max_delay=120):
    """Exponential backoff with jitter. Returns seconds to wait."""
    import random
    delay = min(base ** attempt + random.uniform(0, 1), max_delay)
    return delay

Handle 500 errors with retry logic (up to 3 retries per request). Keep a dead-letter queue (database table or JSON file) for records that fail after all retries — these need human review.

Limitations and Constraints

Be explicit with stakeholders about what cannot be migrated programmatically.

What Kayako Cannot Replicate from Puzzel

  • Multi-tier category system: Kayako has flat tags and simple custom fields. No hierarchical classification. Best approximation: multiple custom dropdown fields, one per tier.
  • Skill-based routing with contact centre integration: Puzzel's deep integration with Puzzel Contact Centre for voice/omnichannel routing has no Kayako equivalent. Kayako uses trigger-based assignment, which is less granular.
  • Form-per-team enforcement: Puzzel enforces "Force Category Choice to Resolve" at the team level, requiring agents to select categories before resolving. Kayako has no direct equivalent — you can approximate this with required custom fields on conversation forms, but enforcement isn't as strict.
  • Event Rules and Response Mappings: Puzzel's API Response Mapping enables automated enrichment of ticket attributes from webhook responses. Kayako has no equivalent feature. You'd need to build external webhook handlers that update Kayako cases via API.
  • Automations and Macros: Puzzel routing rules, SLAs, and macros cannot be exported via API. These must be manually rebuilt in Kayako's trigger and monitor engine.
  • Reporting History: Puzzel analytics and agent performance metrics do not migrate. Puzzel tracks metrics including SLA analysis, team performance, ticket performance per team, per user, and per channel. Export historical reports to CSV for your data warehouse before decommissioning Puzzel.

Kayako API Limitations

  • No bulk create: Cases, users, and organizations must be created one at a time via individual POST requests.
  • Rate limits per minute: Exact limits are not publicly documented; they vary by plan. Monitor 429 responses and benchmark your tenant during UAT.
  • Pagination cap: 100 results per page maximum for GET requests.
  • There is no direct way to export all conversations from Kayako's UI — extraction requires API usage. This matters for validation and rollback.
  • No created_at override (Kayako New): Most Kayako New tenants do not accept created_at in POST payloads — the API sets it to the request time. Confirm against your tenant.

Validation and Testing

Do not assume a 200 OK response means the data is correct. Implement a rigorous validation phase.

Record Count Comparison

After loading, compare counts for every object type:

Object Puzzel Count Kayako Count Match? Notes
Organizations X X ✅/❌
Users (customers) X X ✅/❌ May differ if duplicates were merged
Users (agents) X X ✅/❌
Cases (tickets) X X ✅/❌ Account for intentionally skipped records
Posts (messages) X X ✅/❌
Notes X X ✅/❌ Public notes become posts — adjust counts
Attachments X X ✅/❌ Oversized files logged separately
Tags X X ✅/❌

Account for intentionally skipped records (spam, archived tickets, merged duplicates) when counts differ. Document the reason for every discrepancy.

Field-Level Validation

For a random sample of 50–100 tickets:

  • Verify subject, status, priority, and assignee match
  • Confirm all messages are present in correct chronological order
  • Check custom field values (especially legacy_puzzel_id and category-mapped fields)
  • Verify attachments are downloadable and not corrupted (compare file sizes)
  • Confirm organisation association
  • Verify inline images render correctly (CID references replaced)

Sampling Strategy

Don't validate only recent tickets. Sample across:

  • Different teams (at least 5–10 tickets per team)
  • Different date ranges (oldest, newest, and mid-range)
  • Tickets with many messages (>10 replies)
  • Tickets with large or multiple attachments
  • Tickets with all category tiers populated
  • Tickets with internal notes
  • Tickets that were "On Hold" in Puzzel
  • Tickets with inline images

UAT Process

Have 2–3 agents from each team review their migrated tickets in Kayako. They should verify:

  • Can they find specific tickets by searching the legacy_puzzel_id custom field?
  • Is the conversation history complete and readable?
  • Are custom fields populated correctly?
  • Do organisation profiles look right?
  • Can they open and download attachments?

Rollback Planning

Before the production load:

  • Document the exact Kayako state (empty or pre-existing data)
  • If Kayako was empty, rollback = delete all imported records via API (use the migration_batch_id to identify migrated records)
  • If Kayako had existing data, tag all migrated records with a migration_batch_id custom field so you can selectively delete
  • You can request a data dump or backup for your Kayako instance, which contains all instance data in a MySQL dump file format. Take a backup before migration.

Write down rollback criteria before production: who can trigger a rollback, which batches are safe to rerun, and what constitutes a failed cutover (e.g., >5% record count mismatch, any data corruption in sampled tickets).

Post-Migration Tasks

Once the delta sync is complete and data is validated:

Update DNS and Email Forwarding

Redirect your support email addresses from Puzzel to Kayako. Keep the Puzzel instance read-only until the final delta sync passes validation. Verify email delivery by sending test messages to each support address and confirming they arrive in Kayako.

Rebuild Automations

Puzzel Smart Rules, Inbound Rules, and Event Rules do not transfer. Manually rebuild in Kayako using triggers and monitors. Document each Puzzel rule and its Kayako equivalent before cutover:

Puzzel Rule Kayako Equivalent Implementation Notes
Inbound Rule (route by subject) Trigger (on case create) Match subject with regex, assign team
Event Rule (escalate on SLA breach) Monitor (time-based) Configure SLA policy + escalation trigger
Smart Rule (auto-categorise) Trigger (on case create/update) Set tags or custom fields based on conditions
Response Mapping (webhook enrichment) External webhook + API callback Build external service; no native equivalent

Rebuild SLA Policies

Recreate SLA targets in Kayako. Map Puzzel's per-team and per-category SLA policies to Kayako's SLA policy structure. Kayako SLAs are configurable per priority and type — match your Puzzel configuration as closely as possible.

Activate Automations

Turn on Kayako triggers, SLAs, and routing rules. Keep these disabled during the API load to prevent sending notifications to customers for migrated tickets. Enable them only after the production load is complete and validated.

Agent Training

Kayako's UI differs substantially from Puzzel's agent dashboard. Plan for:

  • A walkthrough of the Kayako conversation view vs Puzzel ticket view
  • Training on Kayako's trigger system (replaces Puzzel's rules engine)
  • How to search for legacy Puzzel ticket IDs using the legacy_puzzel_id custom field
  • How to interpret original_created_at vs Kayako's system created_at
  • Updated internal documentation for workflows, escalation paths, and SLA handling

Monitor for Data Inconsistencies

For 1–2 weeks after cutover:

  • Monitor support queue for agents reporting missing data
  • Run daily count comparisons between the ID mapping table and Kayako
  • Check for orphaned records (cases without requesters, users without organisations)
  • Verify no duplicate tickets were created during the delta sync window

Best Practices

  1. Backup everything. Export a full snapshot from Puzzel and take a Kayako backup (MySQL dump) before production loading.
  2. Never test in production. Execute at least two full dry runs against a Kayako sandbox before the production load. Compare results between runs for consistency.
  3. Migrate in dependency order. Always: organisations → users → cases → posts → notes → attachments. Violating this order creates orphaned records.
  4. Maintain a durable ID mapping table. Use SQLite or PostgreSQL, not an in-memory dict. Store puzzel_id, kayako_id, object_type, migration_batch_id, and migration_timestamp for every record.
  5. Freeze the Puzzel schema. Impose a change freeze on Puzzel custom fields and categories at least two weeks before migration. Any structural changes after the mapping is finalised will break the migration.
  6. Validate incrementally. Don't wait until the full load is done. Validate after each object type — check org counts before loading users, check user counts before loading cases.
  7. Log everything. Every API call, every response code, every transformation decision. Write to a file, not just stdout. You'll need this for debugging and post-migration auditing.
  8. Handle rate limits gracefully. Implement exponential backoff with jitter. Never retry immediately. Respect the Retry-After header value.
  9. Communicate with agents. Give your support team a migration timeline, a testing window, and a clear cutover date. Unexpected UI changes without context cause support quality drops.
  10. Don't force-fit data. If the only honest mapping for a Puzzel object is a note, tag, or archive link in Kayako, do that rather than creating a broken abstraction that misleads agents.

Sample Data Mapping Table

Puzzel Field Kayako Field Type Transformation
Ticket ID legacy_puzzel_id (custom field) Text Preserve for reference and auditability
Subject Subject Text Direct copy
Status Status Enum New→New, Open→Open, Pending→Pending, On Hold→Pending (+tag), Resolved→Completed, Closed→Closed
Priority Priority Enum Low→Low, Medium→Normal, High→High, Critical→Urgent
Team Team Reference Map by name or create lookup table
Assigned Agent Assigned Agent Reference Map by email address
Customer Email Requester (user) Reference Lookup or create user by email; handle 409 Conflict
Organisation Organization Reference Lookup or create by name; load before users
Category: Query Type Custom Field or Tag Dropdown/Tag Flatten or map to custom dropdown (see options above)
Form Field: Contract ID Custom Field (conversation) Text Create matching custom field in Kayako
Tags Tags Array Direct copy
Message (HTML body) Post contents HTML Sanitize HTML, replace CID image references with Kayako URLs
Note (private) Note Text Direct copy
Note (public) Post Text Convert to case post; prefix with [Migrated Note]
Created Date original_created_at (custom field) DateTime Store in custom field (Kayako may not accept created_at override)
Attachment Attachment Binary Download from Puzzel, upload to Kayako via multipart POST

Making the Right Call

A Puzzel Case Management to Kayako migration is a structural data translation — Puzzel's contact-centre-oriented categories, forms, and team-queue mappings don't have direct equivalents in Kayako's conversation-centric model. The migration is achievable with the approach outlined here, but the mapping decisions (especially multi-tier categories, timestamp handling, and public note conversion) are where the real engineering work lives.

The critical path items to resolve before writing migration code:

  1. Test whether your Kayako tenant accepts created_at overrides
  2. Decide on category flattening strategy (tags vs. custom dropdowns vs. leaf-only)
  3. Benchmark your Kayako API rate limits
  4. Confirm Puzzel API endpoint paths from your tenant's Swagger documentation
  5. Map all Puzzel statuses including On Hold

If your team has the engineering bandwidth and API experience, this guide gives you a complete path. If you'd rather keep your engineers focused on product work, ClonePartner specialises in helpdesk migrations — including complex source platforms like Puzzel Case Management where API documentation isn't publicly indexed and extraction requires tenant-level access. Talk to us about your migration.

For validation best practices, see our post-migration QA checklist.

Frequently Asked Questions

Can I migrate full ticket history from Puzzel Case Management to Kayako?
Yes, but not via CSV. Puzzel's CSV export captures ticket metadata but strips full message threads, notes, and attachments. You must use Puzzel's REST API (accessed via your tenant's Swagger UI) to extract complete conversation history, then load into Kayako via the /api/v1/cases.json endpoint.
How long does a Puzzel Case Management to Kayako migration take?
A typical mid-size migration (10,000–50,000 tickets) takes 2–4 weeks total: 3–4 days for planning and mapping, 3–5 days for extraction, 3–5 days for transformation and test loads, 2–3 days for production loading and delta sync, and 2–3 days for validation and cutover.
How do I map Puzzel categories to Kayako?
Puzzel uses multi-tier categories (e.g., Query Type > Billing > Refund). Kayako has no hierarchical category system. You can flatten categories into tags, map them to custom dropdown fields, or concatenate into text fields. Custom dropdown fields preserve the most structure but require pre-creation in Kayako.
Does Kayako have API rate limits that affect migration?
Yes. Kayako API rate limits are defined per minute. If exceeded, the API returns HTTP 429 with a Retry-After header. Exact limits vary by plan and aren't publicly documented. Kayako Classic (self-hosted) has no rate limits. Implement exponential backoff in your migration scripts.
Can Puzzel Case Management automations be migrated to Kayako automatically?
No. Puzzel Smart Rules, Inbound Rules, Event Rules, and SLA policies cannot be migrated programmatically. They must be manually rebuilt in Kayako using triggers, monitors, and SLA policies. Document every Puzzel rule before starting the migration.

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