Skip to content

Kustomer to Crisp Migration: A Technical Guide

A technical guide to migrating from Kustomer to Crisp: API extraction, data model mapping, crisp-import-conversations, rate limits, and edge cases.

Nachi Nachi · · 23 min read
Kustomer to Crisp Migration: A 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

Kustomer to Crisp Migration: A Technical Guide

Migrating from Kustomer to Crisp is a custom API project. There is no native import path between the two platforms — no Kustomer CSV export that Crisp can ingest directly, and no Kustomer adapter in Crisp's official crisp-import-conversations tool. Every customer, conversation, message, note, and attachment must be extracted from the Kustomer REST API, structurally transformed from Kustomer's CRM-timeline model to Crisp's chat-session model, and loaded into Crisp while managing rate limits and daily quotas on both sides.

This guide covers data model differences, API constraints, object mapping, the extraction and loading pipeline, edge cases, validation, and cutover strategy. It is written for engineering teams and ops leads who need to move from Kustomer to Crisp without losing historical data.

All API references target Kustomer REST API V1 and Crisp REST API V1. Verify endpoints against current documentation before implementation — both platforms update their APIs periodically.

Info

Quick summary: Kustomer structures data as Customers containing Conversations containing Messages, with custom objects (KObjects) attached to the timeline. Crisp structures data as Contacts with Conversations containing Messages. No native Kustomer adapter exists in Crisp's import tool — supported adapters are limited to Gorgias, Groove, Help Scout, Tidio, WHMCS, and Zendesk. You must extract data via the Kustomer API, write a custom transformation layer to produce Crisp's expected JSON format, and load it using crisp-import-conversations or direct API calls — all while managing Kustomer's per-minute rate caps and Crisp's daily plugin quotas.

If you're evaluating whether Crisp is the right target, our posts on Kustomer to Zendesk, Kustomer to Freshdesk, Kustomer to Intercom, and Kustomer to Missive cover alternative paths. This guide assumes the decision is made and focuses on execution.

Why Teams Move from Kustomer to Crisp

Kustomer is a CRM-first customer service platform built around a unified customer timeline. It was acquired by Meta for roughly $1 billion in 2020, then spun out in 2023 as an independent venture-backed company. Crisp is a chat-first customer messaging platform popular with startups and SMBs, bundling live chat, a basic CRM, knowledge base, chatbot builder, and multi-channel inbox into a lightweight package.

The most common triggers for this migration:

  • Cost reduction: Kustomer's per-agent pricing on Enterprise and Ultimate plans can be steep for smaller teams. Crisp's flat-rate plans (Essentials at $95/month for the workspace, not per seat) eliminate per-agent scaling costs.
  • Simplicity over CRM depth: Kustomer's strength — deep CRM with custom objects, timeline views, and attribute-level permissions — becomes overhead for teams that just need a fast shared inbox with live chat.
  • Chat-first workflows: Teams that primarily use live chat and chatbot automation often find Crisp's widget architecture and bot builder more intuitive than Kustomer's conversation-plus-workflow model.
  • No need for KObjects: Kustomer's custom objects (KObjects) let you model orders, reservations, or any business entity on the customer timeline. If your team doesn't use them, you're paying for infrastructure you don't need.
Warning

What you lose in the move: Kustomer's deep CRM capabilities — multi-channel conversation threading across a unified customer timeline, custom object schemas, SLA tracking, queue-based routing, and sentiment analysis — have no direct equivalent in Crisp. If your support operation relies heavily on these, validate the trade-off before committing.

Kustomer vs. Crisp: Data Model Differences

Understanding the structural mismatch between these two platforms is the most important step before writing any migration code.

Concept Kustomer Crisp
Contact record Customer (standalone object with email, phone, custom attributes, linked to Company) Contact / People (profile with email, phone, data segments, company as metadata field)
Conversation container Conversation (belongs to Customer, has channel, status, tags, assigned team/agent, SLA, priority) Conversation / Session (belongs to Contact, has state, tags, segments, assigned operator)
Messages Message (belongs to Conversation; direction: in/out/note; includes channel, subject) Message (belongs to Conversation; types: text, note, file, animation, audio, picker, field, event; from: user/operator; origin: chat/email)
Custom objects KObjects (fully custom schema per Klass, related to Customer or Conversation) No equivalent — use contact data segments or conversation metadata
Company Company (standalone object, linked to Customers) Company field on contact profile (flat, not a standalone entity)
Agent/operator User (with roles, teams, permissions) Operator (with inbox access, assignment)
Tags Conversation tags, customer tags Conversation segments, contact segments
Routing Queues, work items, SLA policies, business rules Inbox routing rules, assignment rules (simpler model)
Conversation status open, snoozed, done, deleted pending, unresolved, resolved

The key structural mismatch: Kustomer is CRM-first — everything revolves around the Customer object and its timeline. Conversations, messages, custom objects, and events all nest under a single customer record. Crisp is conversation-first — the inbox and chat session are the primary interface, with contact profiles serving as metadata attached to sessions. (While this simplicity is a feature, teams with complex multi-contact B2B accounts can outgrow it — a limitation we explore in our Crisp to Intercom migration guide). (help.kustomer.com)

The primary engineering challenge is flattening Kustomer's multi-channel, object-rich timeline into Crisp's linear conversation model without losing critical context.

API Constraints: Kustomer (Source)

Kustomer's REST API is the only reliable way to extract complete conversation and message data at scale.

Rate Limits

  • Enterprise plan: 1,000 requests per minute (org-wide)
  • Ultimate plan: 2,000 requests per minute (org-wide)
  • Object update limit: A single object (customer, conversation, message) can be updated up to 100 times per 10-minute window
  • Message creation: 120 messages per minute per customer (messages with importedAt in the request body are exempt from this limit — use importedAt in your extraction payloads to avoid hitting this cap during high-volume replays)

All API keys in the organization share the same rate ceiling. If you have other integrations running during the migration, budget their consumption into your throughput calculations.

At Enterprise limits (1,000 req/min), extracting 50,000 conversations with an average of 10 messages each requires approximately 550,000 API calls (1 per conversation list page + 1 per conversation messages page at 100 per page). At sustained throughput that is roughly 9–10 hours of extraction time, assuming no retries. Real-world migrations typically run 1.5–2× longer due to retries, pagination overhead, and attachment downloads.

Pagination

Kustomer uses cursor-based pagination with a default page size of 100 records. For large datasets, use updated_at date-range filters on the search endpoint to partition your extraction into manageable windows. This avoids hitting pagination depth limits and lets you resume interrupted extractions.

Kustomer documents a hard 100-page limit per query on the standard Search API. For conversation enumeration, use POST /v1/customers/search with queryContext set to conversation and conversation_-prefixed fields. (help.kustomer.com)

Key Extraction Endpoints

GET /v1/customers                          # List all customers
GET /v1/customers/{id}/conversations       # Conversations for a customer
GET /v1/conversations/{id}/messages        # Messages in a conversation
GET /v1/conversations/{id}/attachments     # Attachments on a conversation
POST /v1/customers/search                  # Search with filters and queryContext

Export Alternatives

Kustomer offers several non-API export methods, but each has hard limits:

  • Export Buddy app: Exports users, teams, snippets, shortcuts, conversation tags, and KB articles as CSV. Does not export customer data or conversation history.
  • Reporting CSV exports: Conversation, customer, message, and note objects — useful for one-off analysis but not structured for import. Full message bodies are not included.
  • Saved search exports: Returns data updated within the past 2 years only, capped at 50,000 rows (calculated as search results × selected attributes).
  • Support-assisted conversation export: Capped at the 60,000 most recent conversations.
  • Archive Search: For data older than 2 years that the standard Search API misses, Kustomer documents an Archive Search endpoint. If you need full historical coverage, include this in your extraction plan.

(help.kustomer.com)

For a complete migration, the API is your only option. Keep inventory enumeration and transcript extraction as two separate phases — let search tell you which conversations and customers exist, then pull full message bodies from the Messages API. That split makes reruns cheap and avoids the mistake of assuming standard search sees all historical records.

Danger

Kustomer has no free plan. If you cancel your subscription before completing the export, you lose access to your data. Export everything before initiating any account changes.

API Constraints: Crisp (Target)

Crisp's write-side constraints work differently from Kustomer's — instead of per-minute rate limits, Crisp primarily uses daily quotas on plugin tokens.

Rate Limits and Quotas

  • Plugin tokens are exempt from per-route and global rate limits, but are subject to a daily quota that resets every 24 hours. If your integration hits the quota, you'll receive 429 Too Many Requests until the next reset.
  • Website tokens can work if your total API call needs are below ~10,000 or the import can be split across multiple days. They don't require Marketplace approval.
  • Development tokens have lower quotas and are useful for testing only.

To calculate the quota you need, Crisp's official formula is:

Required quota = (n × 5) + (n × m)

Where n = number of conversations and m = average number of messages per conversation. For 10,000 conversations averaging 8 messages each: (10000 × 5) + (10000 × 8) = 130,000 API calls. Request a production-tier plugin token with a quota at or above this number. (github.com)

For a workspace with 20,000 conversations averaging 12 messages each, you need roughly 340,000 API calls — which may span multiple days of quota resets. Plan your migration timeline accordingly.

Token Acquisition Process

To obtain a production plugin token: (1) create a Marketplace account at marketplace.crisp.chat, (2) register a new plugin and set the required OAuth scopes, (3) submit for approval. Crisp's standard approval turnaround is 2–5 business days for production tokens. Request the token before your planned migration start date — blocking on approval is a common project delay. You can test with a development token while waiting for production approval, but development token quotas are too low for full migration runs.

Conversation and Plan Limits

  • Each conversation can hold approximately 10,000 messages. Exceeding this returns a 409 "too_many_messages" error. If any Kustomer conversation exceeds this threshold, split it into multiple Crisp conversations and add a note indicating continuation.
  • Free plan: No note-type messages, max 1 extra participant per conversation.
  • Mini plan: Max 3 extra participants.
  • Essentials and Plus plans: Max 10 extra participants.

If your Kustomer inbox relies on internal notes or large CC chains, validate the target Crisp plan before you finalize your field mapping.

Crisp API Error Reference

Error Trigger Handling
409 too_many_messages Conversation exceeds ~10,000 messages Split conversation; add continuation note
429 Too Many Requests Daily quota exhausted Wait for 24-hour reset; use resume: true to continue
400 Bad Request Malformed payload (e.g., missing from, wrong timestamp unit) Log full request body; fix transform layer
404 Not Found Invalid website_id, session_id, or operator ID Verify IDs against current workspace state
403 Forbidden Missing OAuth scope Re-check token scopes against required list

Critical Pre-Import Step

Before starting any import, contact Crisp support to temporarily block outgoing emails for your workspace. Without this, Crisp will send email notifications for every imported conversation — flooding your customers' inboxes with historical messages. (github.com)

Using crisp-import-conversations

Crisp maintains an official open-source import tool: crisp-import-conversations. It supports adapters for Gorgias, Groove, Help Scout, Tidio, WHMCS, and Zendesk — but not Kustomer.

Danger

npm malware warning: GitHub's advisory database flags the npm package crisp-import-conversations as malware with no patched version (advisory GHSA-fgqv-qmj9-cxvq). Do not npm install by package name from the public registry. Clone the official crisp-im/crisp-import-conversations repository directly, or vendor your own importer code. (github.com)

You have two options:

  1. Write a custom Kustomer adapter for crisp-import-conversations (recommended for most teams)
  2. Build a standalone ETL script that calls Crisp's REST API directly

Decision guide: Use option 1 if your migration is straightforward (email + chat channels, no complex KObjects, <50,000 conversations). The tool already handles conversation creation, message sequencing, participant setup, resume logic, and plan-aware limits — you just transform Kustomer's JSON into Crisp's expected format. Use option 2 if you need fine-grained error handling per message, custom retry logic, or deep KObjects transformation that doesn't fit the tool's conversation-oriented data model.

Required Auth Scopes

Crisp's importer docs specify these production scopes: website:conversation:initiate, website:conversation:sessions, website:conversation:messages, website:conversation:states, and website:conversation:participants. Create a Marketplace account, register a plugin, and request a production token with these scopes before testing at scale.

Crisp's Expected Conversation Format

The import tool expects each conversation as a JSON object:

{
  "user": {
    "name": "Jane Smith",
    "email": "jane.smith@example.com",
    "country": "US"
  },
  "messages": [
    {
      "text": "I need help with my order",
      "date": 1700000000000,
      "from": "user"
    },
    {
      "text": "Let me look into that for you.",
      "date": 1700000060000,
      "from": "operator"
    },
    {
      "note": "Customer is a VIP — prioritize this.",
      "date": 1700000120000,
      "from": "operator"
    }
  ]
}

Key rules:

  • from must be "user" or "operator" — there is no concept of system-generated messages
  • Private notes use the note field instead of text
  • Dates are Unix timestamps in milliseconds (not seconds — using seconds places all messages in January 1970)
  • The user object maps to the Crisp contact profile
  • Setting resume: true lets you restart interrupted imports without creating duplicates

Step-by-Step Migration Pipeline

Step 1: Extract Customers from Kustomer

Paginate through all customers, storing the Kustomer id alongside the extracted email. You need this ID to fetch each customer's conversations.

import requests
import time
from datetime import datetime, timezone
 
KUSTOMER_API = "https://api.kustomerapp.com/v1"
HEADERS = {
    "Authorization": "Bearer YOUR_API_KEY",
    "Content-Type": "application/json"
}
 
def extract_customers(after_cursor=None):
    """
    Returns (customers_list, next_cursor).
    Checks x-ratelimit-remaining and sleeps if headroom is low.
    """
    url = f"{KUSTOMER_API}/customers"
    params = {"pageSize": 100}
    if after_cursor:
        params["page"] = after_cursor
 
    resp = requests.get(url, headers=HEADERS, params=params)
    resp.raise_for_status()
    data = resp.json()
 
    remaining = int(resp.headers.get("x-ratelimit-remaining", 100))
    if remaining < 50:
        time.sleep(5)  # Back off before hitting the org-wide cap
 
    customers = data.get("data", [])
    next_cursor = data.get("links", {}).get("next")
    return customers, next_cursor
 
def parse_iso_to_ms(iso_str):
    """
    Converts Kustomer ISO 8601 UTC timestamp to Unix milliseconds (Crisp format).
    Using seconds instead of milliseconds places all messages in January 1970.
    """
    dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
    return int(dt.timestamp() * 1000)

For each customer, extract their conversations and each conversation's messages. This is an N+1 pattern: 1 call for the customer list page, then 1 call per customer for conversations, then 1+ calls per conversation for messages.

Step 2: Extract Conversations and Messages

def extract_conversations(customer_id):
    url = f"{KUSTOMER_API}/customers/{customer_id}/conversations"
    resp = requests.get(url, headers=HEADERS)
    resp.raise_for_status()
    return resp.json().get("data", [])
 
def extract_messages(conversation_id):
    """
    Paginates through all messages in a conversation.
    Uses importedAt-aware extraction to avoid the 120 msg/min/customer cap.
    """
    url = f"{KUSTOMER_API}/conversations/{conversation_id}/messages"
    all_messages = []
    params = {"pageSize": 100}
 
    while True:
        resp = requests.get(url, headers=HEADERS, params=params)
        resp.raise_for_status()
        data = resp.json()
        all_messages.extend(data.get("data", []))
 
        next_cursor = data.get("links", {}).get("next")
        if not next_cursor:
            break
        params["page"] = next_cursor
        time.sleep(0.1)  # ~600 req/min — within Enterprise limit with headroom
 
    return all_messages
 
def extract_merged_customer_ids(customer):
    """
    Kustomer merged customers retain a mergedCustomerIds array on the primary record.
    The merged (secondary) records return 404 on direct lookup but their conversation
    history is accessible under the primary customer ID post-merge.
    Returns list of merged source IDs for audit logging.
    """
    return customer.get("attributes", {}).get("mergedCustomerIds", [])

Step 3: Transform to Crisp Format

This is where the structural mismatch matters. Kustomer messages have a direction field (in for customer, out for agent) and a separate concept for notes. Map these to Crisp's from (user/operator) and message type (text/note).

Do not transform straight from source JSON into Crisp API calls. A durable staging layer gives you deterministic reruns, makes validation simpler, and lets you fix mapping bugs without pulling history from Kustomer again. Store the raw Kustomer JSON and the transformed Crisp JSON separately, keyed by kustomer_conversation_id.

Conversation State Mapping

Kustomer has four conversation states. Map them to Crisp as follows:

Kustomer Status Crisp State Notes
open unresolved Active conversation
snoozed unresolved Crisp has no snooze concept; import as unresolved and add a note with the original snooze timestamp
done resolved Closed conversation
deleted Skip Do not import deleted conversations unless required for compliance audit

For snoozed conversations, prepend a system note: " [Kustomer: snoozed until YYYY-MM-DD HH:MM UTC]" as the first note-type message in the Crisp thread.

import re
 
KUSTOMER_TO_CRISP_STATUS = {
    "open": "unresolved",
    "snoozed": "unresolved",  # Add note with original snooze time
    "done": "resolved",
    "deleted": None           # Skip
}
 
def strip_html(html_str):
    """
    Strips HTML tags and normalizes whitespace for Crisp text messages.
    For production use, replace with html-to-text (Node) or bleach (Python)
    to handle entities, tables, and nested tags correctly.
    """
    text = re.sub(r'<[^>]+>', '', html_str or '')
    text = re.sub(r'\s+', ' ', text).strip()
    return text
 
def transform_conversation(kustomer_customer, kustomer_conversation, kustomer_messages):
    customer_attrs = kustomer_customer["attributes"]
    conv_attrs = kustomer_conversation["attributes"]
 
    # Map conversation status
    kustomer_status = conv_attrs.get("status", "open")
    crisp_state = KUSTOMER_TO_CRISP_STATUS.get(kustomer_status)
    if crisp_state is None:
        return None  # Signal caller to skip deleted conversations
 
    crisp_messages = []
 
    # Inject snooze note if applicable
    if kustomer_status == "snoozed":
        snooze_until = conv_attrs.get("snoozedUntil", "unknown")
        crisp_messages.append({
            "date": parse_iso_to_ms(conv_attrs["createdAt"]),
            "from": "operator",
            "note": f"[Kustomer: snoozed until {snooze_until}]"
        })
 
    for msg in sorted(kustomer_messages, key=lambda m: m["attributes"]["createdAt"]):
        attrs = msg["attributes"]
 
        sender = "user" if attrs.get("direction") == "in" else "operator"
        is_note = attrs.get("channel") == "note" or attrs.get("isNote", False)
        timestamp_ms = parse_iso_to_ms(attrs["createdAt"])
 
        # importedAt exempts messages from the 120 msg/min/customer cap
        message_obj = {
            "date": timestamp_ms,
            "from": sender,
            "importedAt": timestamp_ms
        }
 
        if is_note:
            message_obj["note"] = attrs.get("preview", "")
        else:
            raw_body = attrs.get("preview", attrs.get("body", ""))
            message_obj["text"] = strip_html(raw_body)
 
        crisp_messages.append(message_obj)
 
    # First message must be from "user" for conversation to appear in Crisp Inbox
    if crisp_messages and crisp_messages[0].get("from") != "user":
        crisp_messages.insert(0, {
            "date": parse_iso_to_ms(conv_attrs["createdAt"]) - 1,
            "from": "user",
            "text": "[Conversation started by agent]"
        })
 
    return {
        "user": {
            "name": customer_attrs.get("displayName", ""),
            "email": customer_attrs.get("emails", [{}])[0].get("email", ""),
            "country": (customer_attrs.get("locale") or "")[:2].upper() or None
        },
        "state": crisp_state,
        "messages": crisp_messages
    }
Info

Agent attribution: If a message was sent by a Kustomer agent, you must map the Kustomer user ID to the corresponding Crisp operator ID and set from: "operator". If the original agent no longer exists in Crisp, fall back to a generic "Legacy Agent" operator to preserve the agent/customer distinction. Build the operator ID mapping table before you begin transformation — it's needed for every out-direction message.

Step 4: Handling KObjects (Custom Objects)

KObjects are Kustomer's most migration-hostile data structure. They have fully custom schemas per Klass (e.g., order, reservation, subscription) and can be related to either a Customer or a Conversation. Crisp has no equivalent relational object model.

Practical mapping strategy:

def flatten_kobject_to_crisp_data(kobject):
    """
    Flattens a Kustomer KObject into a flat key-value dict
    suitable for Crisp's contact data segments.
 
    Before (Kustomer KObject - order Klass):
    {
      "id": "kobj_abc123",
      "attributes": {
        "orderId": "ORD-9001",
        "status": "shipped",
        "total": 149.99,
        "items": [{"sku": "SKU-1", "qty": 2}]
      }
    }
 
    After (Crisp contact data segment):
    {
      "kustomer_order_orderId": "ORD-9001",
      "kustomer_order_status": "shipped",
      "kustomer_order_total": "149.99",
      "kustomer_order_items": "[{\"sku\": \"SKU-1\", \"qty\": 2}]"
    }
    """
    import json
    klass = kobject.get("type", "object").replace("klass-", "")
    attrs = kobject.get("attributes", {})
    flat = {}
    for key, value in attrs.items():
        segment_key = f"kustomer_{klass}_{key}"[:64]  # Crisp key length limit
        if isinstance(value, (dict, list)):
            flat[segment_key] = json.dumps(value)
        else:
            flat[segment_key] = str(value)
    return flat

What this loses: relational links between KObjects and conversations, multi-value arrays stored as structured data, and any schema-level validation. Document your KObject Klasses before migration, decide which fields matter for agent context, and store only those. Complex relational data that agents actively reference (e.g., real-time order status) should be rebuilt as Crisp Plugins that fetch live data from your backend — not migrated as static strings.

Step 5: Download and Re-host Attachments

Kustomer attachment URLs are authenticated. Once your Kustomer account is deactivated, those URLs stop resolving. During extraction, download every attachment to local or cloud storage.

To load attachments into Crisp:

  1. Identify attachments in the Kustomer message payload (check meta.attachments array on message objects).
  2. Download the file from Kustomer's S3 URL during extraction — do not wait until after cutover.
  3. Request an upload URL from Crisp via POST /v1/website/{website_id}/bucket/url/generate.
  4. PUT the file to the generated Crisp URL.
  5. Store the resulting Crisp file URL and inject it into the final message payload as a file-type message.

Step 6: Load into Crisp

Using crisp-import-conversations (cloned from the official repository):

// Node.js — run from the cloned crisp-im/crisp-import-conversations repo
// Do NOT npm install from the public registry (see malware warning above)
 
var CrispImport = require("./lib/import");
 
var Import = new CrispImport(
  {
    websiteId: "YOUR_WEBSITE_ID",
    websitePlan: "essentials",   // "free", "mini", "essentials", "plus"
    tier: "plugin",
    identifier: "YOUR_TOKEN_IDENTIFIER",
    key: "YOUR_TOKEN_KEY",
    urn: "YOUR_PLUGIN_URN",
    name: "Kustomer Migration"
  },
  {
    resume: true  // Resume interrupted imports without creating duplicates
  }
);
 
Import.importFromFile("./res/conversations.json")
  .then((result) => {
    console.log(`Done. ${result.count} conversations imported.`);
  })
  .catch((error) => {
    // Log full error including response body for 400/409 diagnosis
    console.error("Import failed:", JSON.stringify(error, null, 2));
    process.exit(1);
  });

If daily quota is exhausted, the import resumes the next day without creating duplicates because resume: true checks the local state file before creating each conversation.

Warning

Conversation visibility: A newly created conversation is not visible in the Crisp Inbox until a message is sent with from: "user". Start each imported thread with the first customer message — do not begin with an internal note or operator reply. The transform step above handles this automatically by inserting a placeholder first message when needed. (docs.crisp.chat)

Object Mapping: What Transfers and What Doesn't

Kustomer Object Crisp Equivalent Migration Notes
Customer profile Contact / People Email, name, phone map directly. Custom attributes go into Crisp's data object as key-value segments.
Conversation Conversation Status maps: openunresolved, doneresolved, snoozedunresolved + note, deleted→skip. Tags map to segments.
Messages (in/out) Messages (user/operator) direction: infrom: user; direction: outfrom: operator. HTML email bodies need stripping.
Internal notes Note messages Crisp Free plan does not support notes. Upgrade to Mini or higher before import.
Attachments File messages URLs must be re-hosted. Crisp file messages use their own storage bucket. Download during extraction, not after cutover.
KObjects Contact data segments (flat) Relational structure lost. Flatten key fields to string segments. Active integrations must be rebuilt as Crisp Plugins.
Company Contact company field Kustomer Company is a standalone entity with attributes. In Crisp, company is a flat string field. Multi-contact company relationships are lost.
SLA policies No equivalent Crisp does not have SLA tracking. Document SLA data externally if needed for compliance.
Queue routing Inbox routing rules Manual re-creation required. No programmatic import path.
Business rules/workflows No equivalent Kustomer explicitly states business rules, workflows, saved searches, and Custom Klasses schemas are not exportable. Rebuild manually in Crisp. (help.kustomer.com)
Snippets/shortcuts Message shortcuts Crisp supports CSV import for shortcuts. Export from Kustomer via Export Buddy, reformat, and import.
Knowledge base Crisp Helpdesk No automated migration path. Rebuild articles manually or use the Crisp REST API.
Warning

KObjects warning: If your support workflows rely on rendering custom objects (like Shopify orders or proprietary backend data) natively in the Kustomer timeline, you will need to rebuild these integrations in Crisp using Crisp Plugins or Webhooks. Historical custom object data can only be stored as static string/numeric attributes in Crisp. (E-commerce teams heavily reliant on Shopify data often require deeper integrations — a limitation we discuss in our Crisp to Gorgias migration guide).

Edge Cases and Failure Modes

Kustomer Merged Customer Records

When two Kustomer customers are merged, the secondary (merged-in) record becomes inaccessible by direct ID lookup — GET /v1/customers/{merged_id} returns 404. The primary record retains a mergedCustomerIds array in its attributes listing all absorbed IDs. All conversation history from the secondary record is accessible under the primary customer ID.

Handling: During extraction, log mergedCustomerIds for every customer. If your ID tracking table references a secondary ID, resolve it to the primary before fetching conversations. Do not attempt to extract conversations from merged secondary IDs directly.

In Crisp, contacts are de-duplicated by email. If your Kustomer data had two separate customer records for the same email that were later merged, the migration will correctly land all their conversations under one Crisp contact. If they had different emails and were merged in Kustomer for non-email reasons, the Crisp contact will only carry the primary email — document the secondary email in a contact data segment.

Multi-Channel Conversations

Kustomer conversations can span multiple channels — a customer might start on chat, continue via email, and follow up on WhatsApp, all within the same conversation thread. Crisp conversations have an origin field (chat, email) but are structurally simpler. When migrating multi-channel conversations, choose the origin of the first message in the thread as the Crisp conversation origin. Write a source-to-target channel mapping table before you start loading data.

HTML Email Bodies

Kustomer stores email message bodies as HTML. Crisp's text message type expects plain text. Pushing raw HTML into a standard Crisp text message renders it as raw code. Two options:

  1. Strip HTML to plain text — fast but loses formatting. Use html-to-text (Node.js) or bleach + html2text (Python) rather than a regex substitution, which fails on nested tags, HTML entities, and encoded characters.
  2. Use Crisp's original message payload — preserves the raw email content but requires deeper API integration outside the standard import tool (an approach we detail in our Zammad to Crisp migration guide).

For most migrations, option 1 is sufficient.

Email Subject Lines

Crisp's chat interface does not natively display email subject lines the way Kustomer does. When migrating email conversations, prepend the subject line to the first message: Subject: [Original Subject]\n\n [Message Body].

Data Older Than Two Years

Kustomer's standard Search API only returns records updated within the last two years, and has a hard 100-page limit per query. If you need older data, use Archive Search or another extraction path that does not depend on standard search freshness. Do not assume a standard paginated export covers your full history. (help.kustomer.com)

Attachment Re-hosting

Kustomer attachment URLs are authenticated and will stop resolving when your account is deactivated. Download every attachment during the extraction phase, not after cutover. Budget storage: a typical support operation with 50,000 conversations and moderate attachment use can generate 5–50 GB of files.

Duplicate Contacts

Kustomer allows multiple customer records with the same email (merged records aside). Crisp de-duplicates contacts by email. If your Kustomer data has duplicate emails, the import will merge conversations under a single Crisp contact — which may be desirable, but verify the results. Run a pre-migration deduplication report against your Kustomer data: SELECT email, COUNT(*) FROM customers GROUP BY email HAVING COUNT(*) > 1 against your export, or equivalent via the search API.

Timestamps and Timezone Handling

Kustomer stores timestamps in ISO 8601 UTC format. Crisp's import tool expects Unix timestamps in milliseconds. A common bug: using seconds instead of milliseconds, which places all your messages in January 1970. Always multiply by 1000 when converting. The parse_iso_to_ms function in Step 1 handles this correctly.

Idempotency and Resume

Both APIs will throw 429 Too Many Requests errors under load. Your migration script must be idempotent. Store a local mapping of kustomer_conversation_id to crisp_session_id in SQLite or Redis. If the script crashes, check this mapping and resume without duplicating messages. The resume: true option in crisp-import-conversations handles this automatically using a local state file.

Validation Strategy

After the import completes, validate before cutting over:

  1. Count verification: Compare total conversations and messages in Kustomer (via the search API count endpoint) against Crisp (via the list conversations API with pagination). A ±1% discrepancy typically indicates deleted conversations being correctly skipped; larger gaps require investigation.
  2. Spot-check 50+ conversations: Manually verify message order, sender attribution, timestamps, and note content across a random sample spanning different date ranges and channels.
  3. Contact integrity: Verify that customer emails, names, and company fields transferred correctly. Check for unexpected merges from duplicate emails.
  4. Attachment accessibility: Open 20+ attachment links in Crisp to confirm files were re-uploaded and accessible.
  5. Segment and tag accuracy: Verify that Kustomer conversation tags mapped correctly to Crisp segments.
  6. Old data coverage: Sample records older than two years to confirm Archive Search extraction worked.
  7. State mapping accuracy: Spot-check resolved conversations to verify they landed as resolved in Crisp, and check that no deleted Kustomer conversations appear.
  8. KObjects integrity: For 10+ customers with KObject data, verify that key fields appear correctly as Crisp contact data segments.
Tip

Build a reconciliation script that compares source and target record counts per customer email. This catches both missing conversations and accidental duplicates from interrupted import runs. Crisp pages conversation lists at 20–50 items per request and exposes separate REST endpoints for meta, participants, and files — enough to build repeatable QA automation rather than relying on manual spot checks alone.

Cutover Planning

  1. T-14 days: Complete a full test migration to a staging Crisp workspace. Validate thoroughly against the checklist above.
  2. T-10 days: Request production plugin token and quota from Crisp Marketplace. Budget 2–5 business days for approval.
  3. T-7 days: Set up Crisp workspace configuration (operators, inboxes, routing rules, chatbot, integrations). Contact Crisp support to enable email suppression before any production data import.
  4. T-3 days: Run the production migration. For 50,000 conversations at 12 messages each, budget approximately 780,000 API calls — plan for 3–6 days of quota consumption depending on your approved daily limit.
  5. T-0 (cutover): Switch your website widget from Kustomer to Crisp. Update email forwarding rules. Disable Kustomer integrations.
  6. T+1 hour: Run a delta migration to catch any conversations that arrived in Kustomer between the last full extraction and the widget cutover.
  7. T+14 days: Monitor for issues — missing attachments, incorrect sender attribution, duplicates, or automation rules touching imported history.

Delta Capture for Low-Downtime Cutover

After the historical backfill, stop bulk polling and switch to event capture. Kustomer documents outbound webhooks for conversation, customer, message, team, and user events. Kustomer also offers a real-time Amazon Kinesis data stream for platform and customer events that does not count against API rate limits. The pattern: backfill once, replay deltas via webhook or Kinesis until cutover, then freeze new writes in Kustomer and switch agents. Kinesis is preferable over webhooks for delta capture because it provides ordered, durable event delivery with a replay window — whereas webhooks have no guaranteed ordering and no built-in replay on failure. (help.kustomer.com)

Keep Kustomer Active Post-Cutover

Do not cancel your Kustomer subscription immediately. Keep it in read-only mode for at least 30 days after cutover. This gives your team a fallback for looking up historical context that may not have transferred cleanly, and ensures attachment URLs remain accessible if you need to re-download anything.

Why This Migration Is Hard

The Kustomer-to-Crisp migration is harder than most helpdesk-to-helpdesk moves for three specific reasons:

  1. No native adaptercrisp-import-conversations doesn't support Kustomer, so you're building the transformation layer from scratch rather than running a supported import path.
  2. CRM-to-chat model mismatch — Kustomer's deep CRM structure (KObjects with custom Klass schemas, standalone Company entities, SLA policies, queue routing, four-state conversation model) has no clean target in Crisp. You're making explicit, irreversible decisions about what to keep and what to discard before you write a line of code.
  3. Dual rate limit regimes — Kustomer's per-minute caps throttle extraction speed, while Crisp's daily quotas cap how much you can load per day. A migration of 50,000 conversations requires multi-day planning across both systems simultaneously.

The debugging cost is where projects stall: quota exhaustion mid-run, orphaned messages from a crashed process, contacts merged unexpectedly due to duplicate emails, HTML email bodies rendering as raw tags in Crisp's chat view, merged Kustomer customer IDs returning 404, snoozed conversations with no Crisp equivalent. These edge cases consume significantly more engineering time than the initial build — plan for 2–3× the initial estimate.

ClonePartner runs engineer-led helpdesk migrations with built-in handling for all of the above. For Kustomer to Crisp specifically, that means: full API extraction with managed rate limiting, custom transformation from Kustomer's CRM model to Crisp's conversation format, quota-aware loading with automatic resume, attachment re-hosting, and a reconciliation pass before cutover.

Frequently Asked Questions

Does Crisp have a native Kustomer import adapter?
No. Crisp's official crisp-import-conversations tool supports adapters for Gorgias, Groove, Help Scout, Tidio, WHMCS, and Zendesk — but not Kustomer. You need to extract data from Kustomer's API and transform it to Crisp's expected JSON format before importing.
Can I migrate Kustomer conversations using CSV exports?
Not reliably. Kustomer's CSV and saved-search exports are useful for metadata but do not include full message bodies. Saved search is limited to the past 2 years and capped at 50,000 rows. For a complete migration with full transcripts, you must use the Kustomer REST API.
What are the API rate limits for a Kustomer to Crisp migration?
Kustomer enforces 1,000 requests per minute on Enterprise plans and 2,000 per minute on Ultimate plans, org-wide across all API keys. Crisp uses daily quotas on plugin tokens that reset every 24 hours. Calculate your Crisp quota as (conversations × 5) + (conversations × average messages per conversation).
How long does a Kustomer to Crisp migration take?
It depends on data volume. Crisp's daily API quotas cap how much you can load per day. For 20,000 conversations averaging 12 messages each, you need roughly 340,000 API calls — which may span multiple days of quota resets. Plan for 3–7 days for data transfer, plus time for validation and cutover.
How do I handle Kustomer data older than two years?
Kustomer's standard Search API only returns records updated within the last two years. Use Kustomer's Archive Search endpoint for older conversations. Do not assume a standard paginated export covers your full history.

More from our Blog