Freshchat to Kayako Migration: A Technical Guide
Technical guide to migrating from Freshchat to Kayako — covering API extraction, bulk import with timestamp preservation, data model mapping, and zero-downtime cutover.
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
Freshchat to Kayako Migration: A Technical Guide
TL;DR: No native migration path exists between Freshchat and Kayako. You must extract via the Freshchat REST API (v2), transform conversations into case payloads, and load via Kayako's REST API (v1) — specifically the bulk import endpoint POST /api/v1/bulk/cases. Kayako's bulk endpoint accepts historical created_at and updated_at timestamps on both cases and posts, which is rare among helpdesk platforms. The bottleneck is extraction: Freshchat's Messages API caps at 50 messages per page per conversation, forcing an N+1 query pattern that dominates total runtime. For a 15,000-conversation dataset averaging 8 messages per conversation, sustained extraction at ~100 req/min yields approximately 75–90 hours of API call time alone — budget 5–9 days of total pipeline runtime. Last verified against Freshchat REST API v2 and Kayako Helpdesk REST API v1 (not Kayako Classic).
What This Migration Actually Involves
Freshchat is Freshworks' conversation-first messaging platform. It handles real-time conversations across web chat, WhatsApp, Facebook Messenger, Instagram, Apple Business Chat, and email. Its data model is conversation-centric: every interaction is a Conversation containing a stream of Messages, each with its own sender, timestamp, and message type (normal, private, system). Conversations have no subject lines. Statuses are limited to four values: new, assigned, waiting, and resolved.
Kayako (this guide applies to Kayako Helpdesk, not Kayako Classic) is a case-based helpdesk, and a Freshchat to Kayako migration has to build those cases from scratch. Interactions are stored as Cases, which require a subject, department, status, priority, and type. Individual replies within a case are stored as Posts. Kayako supports channels including MAIL, HELPCENTER, TWITTER, MESSENGER, FACEBOOK, and NOTE. The bulk import endpoint only supports MAIL and NOTE — other channel types must be stored as metadata.
The structural mismatch between these two models—a common challenge when translating a conversation-centric platform into a ticket-centric one—drives four concrete design decisions:
- One Freshchat conversation maps to one Kayako case. The
conversation_idis the most stable thread key across both the messages API and raw transcript exports. - You must synthesize subject lines. Freshchat has no concept of a ticket subject. The standard approach is to truncate the first text content in
message_partsto roughly 50 characters. - Rich message parts need flattening. Freshchat messages contain a
message_partsarray that can include text, images, buttons, quick-reply options, and collection cards. Kayako posts accept plain text or HTML. Bot-interaction UI elements (carousels, input controls) lose their interactive structure and must be serialized as readable fallbacks. - Actor types are broader in Freshchat. Freshchat message actors can be
user,agent,bot, orsystem. Kayako posts require acreator_idreferencing a pre-existing Kayako user. Bot and system messages must be mapped to a designated placeholder user account.
Why Teams Move from Freshchat to Kayako
- Ticket-centric workflow needs. Teams that need traditional ticket management with SLA tracking, case types, priorities, and formal resolution workflows find Kayako's model a better fit than Freshchat's conversation-first approach.
- Self-hosted requirements. Kayako Classic offers self-hosted deployment. Freshchat is cloud-only.
- Help center integration. Kayako includes a built-in help center tightly coupled to the case system. Freshchat relies on Freshdesk or external knowledge bases.
- Channel consolidation. Teams already using Kayako for email and ticket support who want to fold Freshchat messaging history into a single system of record.
Freshchat API: Extraction Constraints
The Freshchat REST API v2 is your primary extraction path. There are two practical extraction patterns, and most successful projects use both.
Base URL: https://{subdomain}.freshchat.com/v2
Regional endpoints: Freshchat instances in the EU region use api.eu.freshchat.com instead of {subdomain}.freshchat.com. Confirm your base URL before starting by checking Admin → Settings → API Tokens, which displays the active region.
Authentication: Bearer token via API key generated in Freshchat Admin → Settings → API Tokens. Pass as Authorization: Bearer <API_KEY> header.
Key Extraction Endpoints
| Endpoint | Purpose | Pagination | Max per page |
|---|---|---|---|
GET /v2/conversations |
List conversations | page + items_per_page |
100 |
GET /v2/conversations/{id}/messages |
Messages per conversation | page + items_per_page |
50 |
GET /v2/agents |
Agent roster | page + items_per_page |
100 |
GET /v2/contacts |
Customer contacts | page + items_per_page |
100 |
POST /v2/reports/raw/ |
Raw report export | Date-windowed | N/A |
Rate Limits
Freshchat rate limits are enforced but not fully documented per-plan. The API returns HTTP 429 when limits are exceeded. The dashboard API has a documented limit of 100 requests per minute. Response headers include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset — build your extractor to read these headers dynamically instead of hard-coding a fixed request cadence. Implement exponential backoff starting at 2 seconds, doubling on each consecutive 429.
The N+1 Query Problem
The conversations list endpoint (GET /v2/conversations) returns metadata — channel_id, status, user_id — but not the actual message content. For every conversation, you must make at least one separate call to fetch its messages, paginated at 50 per page.
Concrete arithmetic for a 15,000-conversation dataset:
- 15,000 conversations ÷ 100 per page = 150 list API calls
- 15,000 conversations × average 8 messages ÷ 50 per page = ~2,400 message-fetch calls
- Total calls: ~2,550 minimum (more for longer conversations)
- At 100 req/min with a conservative 600ms average call duration: 2,550 calls ÷ 100 req/min = ~25.5 minutes of pure call time under ideal conditions
- In practice,
429backoff events, network latency, and sequential (non-async) execution multiply this 3–6× — yielding 75–150 hours for a naively synchronous implementation
Run message fetches asynchronously using a worker queue or Python's aiohttp with a semaphore controlling concurrency. Respect X-RateLimit-Remaining on every response. Do not hard-code sleep intervals.
Raw Reports: A Faster Path for Historical Backfill
Freshchat's raw reports API (POST /v2/reports/raw/) emits pre-built CSV datasets designed for reporting and downstream processing. This is faster than per-conversation fetches for large historical ranges, but has strict windowing constraints:
startcannot be earlier than 15 months from the current dateendcannot be more than one month afterstart- The Chat-Transcript report is limited to 24-hour windows per request
For transcript reconstruction, the most useful fields are: conversation_id, message_id, customer_id, message_type, detailed_message_type, message_parts, and created_time. A multi-day conversation must be stitched together by conversation_id across many daily extracts. Companion datasets (Conversation-Created, Conversation-Resolved, Conversation-Activity) are useful for count reconciliation, reopen detection, and QA validation.
Hybrid extraction wins. Use Chat-Transcript and companion reports for the bulk historical backfill, then use the per-conversation messages API (GET /v2/conversations/{id}/messages with from_time) for open-thread deltas during cutover. This minimizes total API call volume without losing control over in-flight conversations.
Contact/Agent Split
Freshchat maintains separate object types for contacts (customers) and agents. GET /v2/contacts does not return agents, and GET /v2/agents does not return contacts. Your user-mapping script must pull both endpoints and merge them into a unified freshchat_id → type → kayako_id lookup table before transformation begins.
Handling Multi-Channel Contacts
A single Freshchat contact may have simultaneous conversations across multiple channels — web chat, WhatsApp, and email — resulting in multiple distinct conversation_id values for the same requester. These are not duplicates; they are separate threads. Do not deduplicate by user_id alone. Each conversation should map to its own Kayako case. If you need to link them, store the shared user_id (mapped to the Kayako requester) as a common field — the case-level relationship is established through the shared requester, not through merging threads.
Handling Attachments
Freshchat attachments are returned as signed URLs within the message payload. These URLs are time-limited and will expire — the exact TTL is not publicly documented but is typically hours to days. Download all attachments during the extraction phase, not during transformation. Store them locally or in object storage (S3, GCS) keyed by message_id + filename. Attempting to pass a Freshchat URL directly to Kayako will fail silently for any attachment where the URL has already expired.
Multilingual and Encoding Considerations
Freshchat deployments in Arabic, Japanese, or CJK character sets can produce encoding issues when flattening message_parts to HTML or plain text. Always use ensure_ascii=False when serializing JSON message content in Python. For HTML output, set charset=utf-8 in your content-type headers when uploading to Kayako. Test with a multilingual conversation sample before running the full pipeline.
Extraction Script Pattern
import requests
import time
FRESHCHAT_BASE = "https://{subdomain}.freshchat.com/v2"
HEADERS = {
"Authorization": "Bearer <API_KEY>",
"Accept": "application/json"
}
def fetch_all_conversations():
conversations = []
page = 1
while True:
resp = requests.get(
f"{FRESHCHAT_BASE}/conversations",
headers=HEADERS,
params={"page": page, "items_per_page": 100,
"sort_by": "created_time", "sort_order": "asc"}
)
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 10))
time.sleep(wait)
continue
data = resp.json()
convos = data.get("conversations", [])
if not convos:
break
conversations.extend(convos)
if not data.get("pagination", {}).get("next_link"):
break
page += 1
return conversations
def fetch_messages(conversation_id):
messages = []
page = 1
while True:
resp = requests.get(
f"{FRESHCHAT_BASE}/conversations/{conversation_id}/messages",
headers=HEADERS,
params={"page": page, "items_per_page": 50}
)
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 10))
time.sleep(wait)
continue
data = resp.json()
msgs = data.get("messages", [])
if not msgs:
break
messages.extend(msgs)
page += 1
return messagesFor a detailed breakdown of all Freshchat export methods, see How to Export Data from Freshchat: Methods, API Limits & Formats.
Kayako API: Import Constraints
Product version scope: This section applies to Kayako Helpdesk (cloud, current product line) using REST API v1. It does not apply to Kayako Classic (self-hosted, legacy) or Kayako Engage. Endpoint paths, authentication, and bulk import behavior differ across product generations. Verify your Kayako product line before proceeding.
Base URL: https://{subdomain}.kayako.com/api/v1
Authentication: HTTP Basic Auth with admin username and password.
Key Import Endpoints
| Endpoint | Purpose | Batch size | Timestamp support |
|---|---|---|---|
POST /api/v1/bulk/cases |
Bulk case creation | 200 cases max | ✅ created_at, updated_at |
POST /api/v1/cases |
Single case creation | 1 | ❌ |
POST /api/v1/cases/:id/reply |
Add post to case | 1 | ❌ |
POST /api/v1/bulk/users |
Bulk user creation | 200 | ✅ |
Critical Import Details
- Bulk import preserves timestamps.
POST /api/v1/bulk/casesacceptscreated_atandupdated_atin ISO-8601 format (YYYY-MM-DDThh:mm:ssTZD) on both cases and inline posts. The single-casePOST /api/v1/casesendpoint and thePOST /api/v1/cases/:id/replyendpoint do not support timestamp overrides — they stamp the current server time. Use the bulk endpoint for all historical data. Always verify timestamp preservation behavior in your specific Kayako sandbox instance before full-scale import. - 200 cases per batch, async execution. Each bulk request can contain up to 200 cases with inline
posts. The endpoint returns202 Acceptedwith a job ID. You must pollGET /api/v1/jobs/{id}until the job status returnsCOMPLETEDorFAILEDbefore submitting the next dependent batch. partial_importpost-level behavior. Thepartial_import=trueparameter causes the endpoint to insert valid cases and return invalid ones in the error payload. However, whether a case with one invalid post is inserted as a truncated case (without the failing post) or dropped entirely depends on where the validation failure occurs. In practice: a case-level validation failure (missing required field) drops the entire case; a post-level validation failure (malformed content) may drop only the post. Verify this behavior in your sandbox with a deliberately malformed test payload before relying on it for production. Log both inserted counts and error payloads on every batch.legacy_idfor cross-referencing and idempotency. The bulk import accepts alegacy_idstring field per case. Store the Freshchatconversation_idhere. This is the cleanest anchor for idempotency checks — before re-running any batch, queryGET /api/v1/cases?legacy_id={conversation_id}to check for existing records. Note: Kayako does not enforcelegacy_iduniqueness at the API level, so duplicate imports are possible if you re-run without cleanup.- Bulk channel types are limited. The bulk import endpoint only supports
MAILandNOTEas channel types. Preserve the original Freshchat channel identity (web chat, WhatsApp, etc.) in a custom case field or tag. - Rate limits are not publicly documented. Kayako returns HTTP
429with aRetry-Afterheader. Start at 2–3 bulk requests per second and back off on every429. Aggressive pacing against the async bulk endpoint can queue jobs faster than Kayako processes them, causing delayedFAILEDresponses. - Default page size is 10. When reading back imported data for validation, set
limit=100explicitly. Archived cases are excluded from default queries — appendarchived=1to include them. - Webhook/callback support for bulk jobs. Kayako does not provide webhook callbacks on bulk job completion as of this writing. Polling
GET /api/v1/jobs/{id}at 5-second intervals is the supported pattern.
Verify timestamp behavior in your sandbox. Always test created_at and updated_at preservation on your specific Kayako Helpdesk instance using a small batch before running production imports. Behavior can vary by instance configuration.
Kayako Status IDs Are Instance-Specific
Kayako status IDs are assigned per instance and vary between deployments. Always call GET /api/v1/cases/statuses on your target Kayako instance to retrieve the actual ID values before building your transformation. The response schema is:
{
"data": [
{"id": 1, "label": "NEW", "type": "NEW"},
{"id": 2, "label": "OPEN", "type": "OPEN"},
...
]
}Do not hardcode status IDs. The table below shows the Freshchat-to-Kayako status concept mapping only — the numeric IDs must be resolved from your instance at runtime:
| Freshchat Status | Kayako Status Type | Query to resolve ID |
|---|---|---|
new |
NEW |
GET /api/v1/cases/statuses |
assigned |
OPEN |
GET /api/v1/cases/statuses |
waiting |
PENDING |
GET /api/v1/cases/statuses |
resolved |
COMPLETED |
GET /api/v1/cases/statuses |
Data Model Mapping: Freshchat → Kayako
Field-Level Mapping
| Freshchat Field | Kayako Field | Notes |
|---|---|---|
conversation_id |
legacy_id |
Store for cross-reference and idempotency checks |
First message created_at |
created_at |
Bulk import only; verify in sandbox |
Last message created_at |
updated_at |
Bulk import only; verify in sandbox |
Conversation status |
status_id |
Query GET /api/v1/cases/statuses at runtime |
assigned_agent_id |
assigned_agent_id |
Requires user pre-migration to complete |
assigned_group_id |
assigned_team_id |
Requires team pre-creation in Kayako |
| Conversation properties | field_values |
Map to pre-created custom case fields |
| Messages array | posts array |
Flatten message_parts to HTML or plain text |
Message created_at |
Post created_at |
Per-post timestamps (bulk endpoint only) |
Message actor_type = agent |
Post creator_id = Kayako agent user ID |
|
Message actor_type = user |
Post creator_id = Kayako requester user ID |
|
Message actor_type = bot or system |
Post creator_id = placeholder Kayako user |
Create a "Bot/System" user in Kayako before import |
| Message attachments | Post attachment_file_ids |
Upload to Kayako first; reference returned file IDs |
| Tags | tags |
Comma-separated string |
| CSAT rating | Custom field, tag, or internal note | No native CSAT import field in Kayako |
interaction_id |
Custom field or migration note | Changes on conversation reopen; preserve all distinct values if reopen history matters for audit |
Generating Case Subjects
Freshchat conversations have no subject field. Generate one programmatically from the first message:
def generate_subject(messages):
if not messages:
return "Empty Chat Transcript"
first_text = ""
for part in messages[0].get("message_parts", []):
if "text" in part:
first_text = part["text"].get("content", "")
break
if not first_text:
return "Chat Transcript"
if len(first_text) > 50:
return first_text[:47] + "..."
return first_textFlattening Rich Messages
Freshchat messages can include structured reply_parts, collections, quick replies, and template content. Kayako posts are text or HTML. Use ensure_ascii=False when serializing non-ASCII content:
import json
def flatten_message(msg):
parts = []
for part in msg.get("message_parts", []):
if "text" in part:
parts.append(part["text"]["content"])
if "file" in part:
parts.append("[attachment] " + part["file"]["name"])
if "image" in part:
parts.append("[image] " + part["image"]["url"])
if msg.get("reply_parts"):
parts.append("[Bot UI elements — original JSON preserved below]")
parts.append(json.dumps(msg["reply_parts"], ensure_ascii=False))
header = "[" + msg.get("actor_type", "unknown") + "]"
return "\n".join([header] + parts)A plain-text serialization of bot elements is more useful to agents than a partially preserved widget structure that Kayako cannot render. Explicitly label discarded interactive elements with [Button: "Talk to agent"] or similar so agents understand what was there.
Step-by-Step Migration Pipeline
Step 1: Pre-Migration Setup
- Generate API credentials for both platforms. Freshchat: Admin → Settings → API Tokens. Kayako: use admin username/password for Basic Auth, or Admin → Apps → API Keys if your instance supports token auth.
- Inventory your Freshchat data. Collect: total conversation count, total message count (estimate from raw reports), total contacts and agents, approximate attachment volume in GB, and oldest conversation date. If data older than 15 months exists, the raw reports API cannot cover it — you must use the per-conversation messages API for that date range.
- Provision a Kayako sandbox. Request a sandbox instance from Kayako support or use a staging subdomain. Run all timestamp,
partial_import, and channel-type tests against the sandbox before touching production. A realistic test dataset should include: one conversation with attachments, one bot-only conversation, one conversation reopened multiple times, and one conversation with non-ASCII characters. - Pre-create Kayako infrastructure. Create teams, custom case fields, case types, priorities, and status values in Kayako before import. Query
GET /api/v1/cases/statuses,GET /api/v1/departments,GET /api/v1/teams, andGET /api/v1/cases/fieldsand record all IDs in a local config file. - Build a user mapping table. Extract all Freshchat agents (
GET /v2/agents) and contacts (GET /v2/contacts) as separate lists. Create corresponding Kayako users viaPOST /api/v1/bulk/users. Store the mapping as{freshchat_id: {type: "agent"|"contact", kayako_id: ...}}. Map departed agents to a "Legacy Agent" placeholder account to avoid orphaned records. Create a "Bot/System" placeholder user foractor_type=botandsystemmessages.
Step 2: Extract Freshchat Data
- Pull conversations using paginated
GET /v2/conversationscalls (100 per page, sorted ascending bycreated_time). - For each conversation, pull all messages via
GET /v2/conversations/{id}/messages(50 per page max). Run message fetches asynchronously. This is the bottleneck step — see runtime estimates below. - Download all attachments immediately. Do not defer to the transformation phase. Store locally keyed by
{message_id}_{filename}. - Export raw reports for CSAT, resolution times, and agent assignments via
POST /v2/reports/raw/in 24-hour windows, covering the full 15-month range available. - Store everything in a staging database. PostgreSQL works well. Create tables for conversations, messages, contacts, agents, and attachments with the raw JSON preserved alongside parsed fields. This allows re-running transformation without re-extraction.
Step 3: Transform Data
- Flatten
message_partsinto HTML for each message. Replace interactive elements with human-readable text:[Button: "Talk to agent"],[Quick reply: "Yes"]. - Generate subject lines from the first message of each conversation using the function above.
- Map users using the lookup table from Step 1. Flag any unmapped
actor_idvalues — these indicate contacts or agents missing from your extraction. - Map statuses, priorities, and types to Kayako IDs resolved from your instance API.
- Upload attachments to Kayako via the file upload endpoint. Receive back
file_idvalues. Associate eachfile_idwith the corresponding message record in your staging database. - Build Kayako bulk payloads. Each payload contains up to 200 cases with inline
posts, timestamps,legacy_ids, andattachment_file_ids. Validate payload structure against the Kayako API schema before submission.
Step 4: Load into Kayako
- Submit bulk case batches of up to 200 via
POST /api/v1/bulk/cases?partial_import=true. - Poll job status via
GET /api/v1/jobs/{id}at 5-second intervals until each batch reportsCOMPLETEDorFAILED. - Log both successful inserts and error payloads. Record the returned Kayako case IDs alongside the source
legacy_id(Freshchatconversation_id) in your staging database. - Reprocess failures individually, inspecting the error payload to determine whether failure was at case level or post level.
- Pace your requests. Start at 1 batch per 5 seconds. Back off on every
429.
import requests
import json
import time
KAYAKO_BASE = "https://{subdomain}.kayako.com/api/v1"
AUTH = ("admin@example.com", "password")
def submit_batch(cases_batch):
resp = requests.post(
f"{KAYAKO_BASE}/bulk/cases?partial_import=true",
auth=AUTH,
headers={"Content-Type": "application/json"},
data=json.dumps({"cases": cases_batch})
)
if resp.status_code == 429:
wait = int(resp.headers.get("Retry-After", 60))
time.sleep(wait)
return submit_batch(cases_batch)
if resp.status_code == 202:
job_url = resp.json()["data"]["resource_url"]
return poll_job(job_url)
raise Exception(f"Unexpected status {resp.status_code}: {resp.text}")
def poll_job(job_url):
while True:
resp = requests.get(job_url, auth=AUTH)
data = resp.json()["data"]
if data["status"] == "COMPLETED":
return data
if data["status"] == "FAILED":
raise Exception(f"Job failed: {data}")
time.sleep(5)Step 5: Delta Sync and Cutover
Freezing support operations for 5–9 days is not feasible. Use a delta sync strategy:
- Initial load: Run the migration on all historical data up to a specific cutover timestamp (e.g., all conversations with
created_timebefore Friday 00:00 UTC). Your team continues working in Freshchat. - Delta sync: On cutover day, query only conversations
created_timeorupdated_timeafter that timestamp. The smaller volume finishes in hours. - Cutover: Update DNS records, website widgets, and routing to direct new traffic to Kayako. Team logs out of Freshchat and into Kayako.
- Final catch-up: Run the delta script one more time to capture conversations updated during the switchover window.
- Read-only period: Keep Freshchat active in read-only mode for at least 14 days before decommissioning. This preserves access for audit and any discovered migration gaps.
Step 6: Validate
API calls returning success codes does not confirm data integrity. Run all of the following:
- Count reconciliation: Compare total conversations in Freshchat against total cases in Kayako. Compare total messages in your staging database against total posts in Kayako. A >1% discrepancy warrants investigation before cutover.
- Timestamp spot-checks: Sample 20+ cases and verify that
created_atin Kayako matches the original Freshchat conversationcreated_time, not the date your migration script ran. This is the most common silent failure. - Attachment verification: Sample 5–10% of conversations with attachments. Open each in Kayako and confirm files are accessible and correctly associated with the right post.
- Thread usability checks: Open a sample of bot-heavy conversations, reopened conversations, and multilingual conversations. Verify the flattened content is readable and correctly attributed by
actor_type. - Multi-channel contact check: For contacts with conversations across multiple Freshchat channels, verify that all conversations appear as separate Kayako cases linked to the same requester.
- Archived case access: Use
GET /api/v1/cases/{id}/posts?archived=1for any resolved cases that Kayako has archived. Standard queries exclude archived cases.
What Cannot Be Migrated
Some data and configuration will not transfer programmatically:
- Freddy bot flows and configurations — Must be rebuilt manually in Kayako or a third-party bot tool. As we note in our Freshchat to Ada migration guide, bot migration is always a redesign, never a direct conversion.
- IntelliAssign routing rules — Kayako uses its own assignment automation. Rebuild using Kayako Triggers and Monitors.
- CSAT survey configuration — Raw CSAT scores can be preserved as custom field values, tags, or internal notes (e.g.,
[Historical Data: Customer rated this interaction 5/5]). Survey logic must be rebuilt natively. - Canned responses / macros — Must be manually recreated in Kayako's macro engine.
- Widget customizations — Kayako Messenger uses its own configuration interface.
- Marketplace app integrations — Freshchat marketplace apps must be replaced with Kayako equivalents or custom integrations.
- Agent performance metrics and historical reports — Export from Freshchat raw reports before decommissioning. This data does not transfer to Kayako reporting.
- Rich interactive message elements — Carousels, quick-reply buttons, and input controls lose interactivity. They are preserved as labeled text fallbacks only.
Edge Cases and Failure Modes
- Conversations with no messages. Some conversations are created by automation or bots but contain zero messages. The Kayako bulk import requires at least one post per case. Filter these out during transformation or inject a placeholder post:
[No messages — automated conversation]. - User creation race condition. Bulk user import jobs run asynchronously. If your user import has not finished when you submit the first case batch,
requester_idandcreator_idreferences will fail validation. Always poll user import jobs toCOMPLETEDstatus before starting case import. partial_importand post-level failures. A post with malformed content (oversized payload, invalid encoding, nullcreator_id) may be dropped while the parent case is inserted. This produces a case in Kayako with fewer posts than in Freshchat — silent data loss. Log the full error payload from every job and reconcile post counts per case, not just case counts.legacy_idis not enforced as unique. Kayako's API does not prevent duplicatelegacy_idvalues. Re-running a migration batch without cleanup creates duplicate cases. Build idempotency into your script: before submitting a batch, queryGET /api/v1/cases?legacy_id={conversation_id}for a sample of records to detect prior runs.- Attachment URL expiration. Freshchat signed attachment URLs expire. Download during extraction. If you discover expired URLs during transformation, you cannot recover those attachments without re-running extraction against the source (which may itself fail if the Freshchat conversation is old).
- Conversation property schema access. Freshchat's conversation property definition API is documented as available only for accounts that are part of Freshsales Suite. Even there, default properties (
group,agent,status) are not returned. Capture the custom property schema manually from the Freshchat admin interface early in the project. - Freshchat
interaction_idchanges on reopen. Each time a resolved conversation is reopened, Freshchat assigns a newinteraction_idwhile keeping the sameconversation_id. If reopen history matters for compliance or audit, store all distinctinteraction_idvalues encountered in a custom field or migration note. Do not discard this data. - Multi-channel contact deduplication. A single Freshchat contact with simultaneous or sequential conversations across web chat, WhatsApp, and email will appear as multiple distinct
conversation_idrecords, all sharing the sameuser_id. These should map to separate Kayako cases under the same requester. Do not merge these threads — they represent different channel interactions. Verify that your user mapping correctly resolves the shareduser_idto a single Kayako requester.
Runtime Estimates
Calculation basis for 15,000 conversations, ~8 messages average, 5% with attachments:
| Phase | Calculation | Estimated time |
|---|---|---|
| Conversation list extraction | 150 API calls at 100 req/min | ~2 minutes |
| Message extraction (synchronous) | 15,000 conversations × avg 0.16 pages × 600ms/call + 429 backoff | 2–4 days |
| Message extraction (async, 10 workers) | Same call volume, parallelized within rate limit | 6–12 hours |
| Attachment download (750 conversations) | Depends on file sizes and bandwidth | 2–8 hours |
| Transformation + staging | Message flattening, user mapping, payload build | 4–8 hours |
| Loading (Kayako bulk, 75 batches × 200 cases) | Job submission + polling | 2–4 hours |
| Validation + delta sync | Spot checks + catch-up run | 4–8 hours |
| Total (synchronous extraction) | 5–9 days | |
| Total (async extraction) | 2–4 days |
Larger instances (50K+ conversations) or attachment-heavy datasets scale approximately linearly with conversation count. Extraction is always the bottleneck. Moving from synchronous to async message fetching is the single highest-impact optimization available.
DIY vs. Managed Migration
Handle it in-house if:
- Fewer than 5,000 conversations
- No attachments or minimal attachment volume
- No custom fields requiring complex mapping
- You have a Python or Node.js developer with 2+ weeks available and comfortable with API debugging
- You have a staging environment to test against
Use a managed service if:
- 10,000+ conversations
- Attachment preservation is required
- You need the migration completed in under a week
- Historical timestamps must be exactly preserved and verified
- You cannot afford to discover silent data loss (missing posts, expired attachment URLs, encoding failures) after cutover
Making the Right Call
A Freshchat to Kayako migration is a moderately complex API-to-API data pipeline. The structural advantages: Kayako's bulk import endpoint accepts historical timestamps and supports batch operations with inline posts — this is meaningfully better than target platforms that force current-date stamping or require separate post-creation calls. The structural disadvantages: Freshchat's per-conversation message extraction is rate-limited and sequential by default, rich message content requires careful flattening, and neither platform's rate limit behavior is fully documented.
The most operationally dangerous gaps are post-level silent failures under partial_import=true, expired attachment URLs discovered late in the pipeline, and legacy_id duplicate inserts from re-runs without idempotency checks.
Plan for extraction to be 50%+ of your total timeline. Always use async extraction. Use legacy_id on every imported case. Set partial_import=true on every batch and log every error payload. Reconcile post counts per case, not just total case counts. Keep Freshchat in read-only mode for at least 14 days after cutover.
For related migration guides, see our technical walkthroughs on Freshchat to HappyFox migration and Kayako to Freshdesk migration. If you are evaluating Kayako as a target from other platforms, the Zammad to Kayako guide covers a similar load pattern.
Frequently Asked Questions
- Is there a native migration tool from Freshchat to Kayako?
- No. There is no built-in migration path, import wizard, or connector between Freshchat and Kayako. You must use the Freshchat REST API v2 for extraction and Kayako's REST API v1 bulk import endpoint for loading.
- Can I preserve original timestamps when migrating to Kayako?
- Yes. Kayako's POST /api/v1/bulk/cases endpoint accepts created_at and updated_at fields in ISO-8601 format on both cases and posts. The single-case POST endpoint does not support timestamp overrides — only the bulk endpoint does. Verify behavior in your sandbox before running the full migration.
- How long does a Freshchat to Kayako migration take?
- For a mid-size instance (15,000 conversations), expect 5–9 days total: 2–4 days for extraction (bottlenecked by per-conversation message fetching at 50 per page), 1–2 days for transformation, 1–2 days for loading, and 1 day for validation. Larger datasets scale linearly.
- What data cannot be migrated from Freshchat to Kayako?
- Freddy bot flows, IntelliAssign rules, CSAT survey configurations, canned responses, widget customizations, marketplace app integrations, and agent performance analytics cannot be migrated programmatically. These must be rebuilt manually or exported separately from Freshchat raw reports.
- Can I export Freshchat data as a CSV and import it into Kayako?
- Not for a complete migration. Freshchat allows CSV exports via raw reports for reporting purposes, but these files do not contain full message payloads, attachments, or relational data needed for a proper import. A full migration requires using the REST APIs of both platforms.