Skip to content

Ada to Crisp Migration: A Technical Guide

Technical guide to migrating from Ada to Crisp: Data Export API constraints, object mapping, Crisp import tooling, rate limits, edge cases, and validation.

Nachi Nachi · · 20 min read
Ada 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

Ada to Crisp Migration: A Technical Guide

Migrating from Ada to Crisp is an API-to-API project. Ada does not offer a dashboard export for conversation data, and Crisp has no built-in Ada import adapter. The practical path is to extract conversations and messages via Ada's Data Export API, transform the data to fit Crisp's schema, and load it using the Crisp REST API or the open-source crisp-import-conversations tool. There is no native migration path, no shared CSV format, and no built-in connector between the two platforms.

This guide covers the full technical architecture: data model differences, API constraints on both sides, extraction and loading patterns, edge cases, and validation. It's written for engineering leads and CX ops teams planning the move from Ada's AI-agent-first platform to Crisp's chat-centric workspace.

All API references target Ada Data Export API v2 and Crisp REST API V1 unless otherwise noted. Both platforms update their APIs — verify endpoints against current documentation before writing migration code.

Why Teams Move from Ada to Crisp

Ada is an enterprise AI customer service agent platform. It automates conversations across web chat, email, voice, and social channels using a proprietary Reasoning Engine. Ada is purpose-built for deflection at scale — combining a configurable reasoning layer, business knowledge, integrations, handoffs, APIs, and analytics. It is not designed for human-led inbox workflows.

Crisp is a chat-first customer messaging platform with a shared inbox, CRM, knowledge base, and chatbot builder. It is designed for teams where operators (human agents) handle conversations directly, with automation layered on top.

The most common triggers for this migration:

  • Cost reduction: Ada does not publish pricing publicly. Based on third-party procurement data: Ada starts at approximately $30,000/year (Salesforce AppExchange listing), with median contract values around $70K/year (Vendr), and large enterprise deployments reaching $300K+/year. Crisp starts at €29/month for its Essentials plan — a significant cost delta for teams that no longer need enterprise-grade AI deflection.
  • Shifting to human-first support: Ada is optimized for AI-resolved conversations. Teams moving toward a model where human agents handle the majority of interactions find Crisp's operator-centric inbox a better architectural fit.
  • Stack consolidation: Ada typically sits on top of another helpdesk (Zendesk, Salesforce). Teams eliminating the middleware layer choose Crisp as a single platform for live chat, chatbots, knowledge base, and CRM.

Note: This guide is authored by a team that provides Ada-to-Crisp migration services. All technical recommendations below are based on documented API behavior and are independently verifiable.

Data Model Mapping: Ada vs Crisp

Ada and Crisp have fundamentally different data architectures. Ada is AI-agent-centric — conversations are primarily between an AI agent and an end user, with optional handoff to human agents. Crisp is operator-centric — conversations happen between human operators and visitors/contacts, with bots as optional automation.

To migrate, you must flatten Ada's bot-driven structure into Crisp's linear message model.

Ada Concept Crisp Equivalent Mapping Notes
Conversation Conversation (session) 1:1 mapping by ID. Crisp uses session_id (UUID).
Message (bot) Message (from: operator) Ada bot messages map to operator-origin messages. Use automated: true to distinguish from human replies.
Message (end user) Message (from: user) Direct mapping.
End User People (Contact) Map Ada end_user_id → Crisp People profile.
Agent (human handoff) Operator Ada's agent_id maps to Crisp operator assignment.
Channel (chat/email/voice) Origin (chat, email) Crisp has fewer origin types. Voice has no native equivalent.
Variables (metadata) Conversation Data / People Data Ada variables → Crisp custom data fields.
Tags Segments Ada tags → Crisp segments on contacts.
Handoff events Internal note (recommended) No direct equivalent. Serialize the event as a private note: " [HANDOFF] Transferred to {agent_name} at {timestamp} from {channel}".
CSAT / Resolution status Conversation state Partially mappable; Crisp supports resolved/unresolved states.

Ada Export Response Schemas

Before writing transform code, you need to know the actual field names the Ada Data Export API returns. Representative response structures for both endpoints:

GET /api/v2/export/conversations — response shape:

{
  "data": [
    {
      "id": "conv_abc123",
      "created_at": "2024-01-15T10:22:00Z",
      "updated_at": "2024-01-15T10:45:00Z",
      "status": "resolved",
      "channel": "chat",
      "chatter_id": "user_xyz789",
      "chatter_email": "user@example.com",
      "tags": ["billing", "urgent"],
      "resolution": "bot_resolved",
      "variables": {
        "account_id": "ACC-1234",
        "plan_type": "enterprise"
      }
    }
  ],
  "next_cursor": "eyJpZCI6Imxhc3RfaWQifQ=="
}

GET /api/v2/export/messages — response shape:

{
  "items": [
    {
      "id": "msg_def456",
      "conversation_id": "conv_abc123",
      "created_at": "2024-01-15T10:22:05Z",
      "sender": "bot",
      "body": "Hi there! How can I help you today?",
      "type": "text",
      "agent_id": null,
      "agent_name": null,
      "quick_replies": ["Check my order", "Get a refund", "Talk to an agent"],
      "tool_call": null
    },
    {
      "id": "msg_def457",
      "conversation_id": "conv_abc123",
      "created_at": "2024-01-15T10:22:30Z",
      "sender": "chatter",
      "body": "Check my order",
      "type": "text",
      "agent_id": null,
      "agent_name": null,
      "quick_replies": null,
      "tool_call": null
    },
    {
      "id": "msg_def460",
      "conversation_id": "conv_abc123",
      "created_at": "2024-01-15T10:35:00Z",
      "sender": "agent",
      "body": "Let me pull up your order history.",
      "type": "text",
      "agent_id": "agent_001",
      "agent_name": "Sarah Chen",
      "quick_replies": null,
      "tool_call": {
        "name": "get_order_status",
        "arguments": {"order_id": "ORD-9876"},
        "result": {"status": "shipped", "eta": "2024-01-17"}
      }
    }
  ],
  "next_cursor": null
}

Key field notes:

  • Conversations return under the data key; messages return under items. This inconsistency breaks generic parsers — handle each endpoint separately.
  • sender values: "bot", "chatter" (end user), "agent" (human agent during handoff).
  • quick_replies is an array of strings or null. These button labels have no clickable equivalent in Crisp.
  • tool_call contains the function name, arguments, and result from Ada's Reasoning Engine integrations. Neither the call nor the result has a first-class Crisp target.
  • variables in conversations is a flat key-value object mapping Ada workflow variables.

Verify these field names against your actual Ada export — Ada does not publicly document a stable message schema and field names may vary by account configuration or Ada version.

Handling quick_replies and tool_call Payloads

Ada messages with quick_replies or tool_call payloads need explicit handling. The recommended approach for each:

quick_replies: Append the button options as a parenthetical to the message body, then null out the array.

"Hi there! How can I help you today? [Options: Check my order | Get a refund | Talk to an agent]"

Historical buttons do not need to stay clickable. The label text is the informational content.

tool_call: Serialize as a private internal note attached to the conversation, not as a visible message. Structure:

[TOOL CALL] get_order_status
Args: {"order_id": "ORD-9876"}
Result: {"status": "shipped", "eta": "2024-01-17"}

This preserves the integration trace without polluting the visible conversation transcript. Alternatively, store the raw tool_call JSON in the conversation's custom data field for audit purposes.

What Doesn't Transfer

  • AI coaching data and playbooks: Ada's Coaching, Playbooks, and Simulations are proprietary to its Reasoning Engine. None of this transfers.
  • Bot decision trees / Answer flows: Ada Answer content and conversation logic cannot be exported and reused. Because Ada uses a declarative AI model rather than traditional scripted paths (a paradigm shift we detail in our Freshchat to Ada guide), you'll rebuild bot automation from scratch using Crisp's chatbot builder.
  • Analytics and performance data: Ada's resolution rates, AI confidence scores, and Performance Center metrics stay in Ada. Export reports manually before decommissioning.
  • Handoff configuration: Ada's handoff integrations (to Zendesk, Salesforce, etc.) are irrelevant in Crisp. You'll configure Crisp's native routing instead.
  • Voice channel audio: Voice transcripts can be imported as text messages, but audio data, call duration, IVR paths, and telephony metadata will not survive the migration. In the Ada export, voice conversations appear as transcript JSON — the body field contains the transcribed text, and a channel: "voice" field identifies the source. Treat these as text-only records in Crisp and tag them with a ada-voice-transcript segment so agents can identify the source.

Ada Data Export API: Extraction Constraints

The Data Export API allows authenticated access to conversation and message data. Two endpoints matter for migration:

  • GET /api/v2/export/conversations — returns conversation-level records
  • GET /api/v2/export/messages — returns individual message records

Authentication

Log into your Ada dashboard and navigate to Settings > Integrations > APIs, then click New API key. Use Bearer authentication in the Authorization header:

Authorization: Bearer your-ada-api-key

Confirm Data Export API access is included in your Ada contract before starting — this feature is not included in all subscription packages.

Rate Limits, Pagination, and Date Windows

Ada's documentation currently shows conflicting throughput numbers:

Source Rate Limit
Data Export API overview page 10 requests/second per endpoint
Global API limits page 3 requests/second, 15,000 requests/day

For migration planning, use 3 requests/second until Ada confirms your tenant's actual allowance. Both sources agree on:

  • Maximum page size: 10,000 records per page
  • Maximum date range per request: 60 days
  • Historical data access: 12 months rolling
  • Pagination method: cursor-based
Danger

The 12-month historical limit is the single biggest constraint for this migration. If you have conversation data older than 12 months that must be preserved, you need to have already been archiving it to your own data warehouse — the Ada API will not return it. Start your extraction as early as possible. Every day you wait, one day of history rolls off the accessible window.

Two additional behaviors that are not prominently documented:

  • If you omit the created_before end date, Ada defaults to a 7-day slice from created_since. This quietly creates holes in long backfills.
  • Conversation queries support created_since or updated_since, but not both simultaneously in a single request.

Extraction Pattern

Because the date range is capped at 60 days per request, you need to iterate in windows. Extract conversations first (response key: data), then messages (response key: items).

import requests
import time
from datetime import datetime, timedelta
 
ADA_API_KEY = "your-ada-api-key"
ADA_BASE_URL = "https://your-instance.ada.support/api/v2/export"
 
def extract_ada_conversations(start_date, end_date):
    """Extract conversations in 60-day windows with cursor pagination."""
    all_conversations = []
    window_start = start_date
 
    while window_start < end_date:
        window_end = min(window_start + timedelta(days=60), end_date)
        cursor = None
 
        while True:
            params = {
                "created_since": window_start.strftime("%Y-%m-%dT%H:%M:%SZ"),
                "created_before": window_end.strftime("%Y-%m-%dT%H:%M:%SZ"),
                "page_size": 10000,
            }
            if cursor:
                params["cursor"] = cursor
 
            response = requests.get(
                f"{ADA_BASE_URL}/conversations",
                headers={"Authorization": f"Bearer {ADA_API_KEY}"},
                params=params,
            )
 
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 1))
                time.sleep(retry_after)
                continue
 
            response.raise_for_status()
            data = response.json()
            all_conversations.extend(data.get("data", []))  # Note: "data" key
 
            cursor = data.get("next_cursor")
            if not cursor:
                break
 
            time.sleep(0.35)  # Stay under 3 req/s (conservative)
 
        window_start = window_end
 
    return all_conversations
 
def extract_ada_messages(conversation_ids, start_date, end_date):
    """Extract messages — note the response key is 'items', not 'data'."""
    all_messages = []
    window_start = start_date
 
    while window_start < end_date:
        window_end = min(window_start + timedelta(days=60), end_date)
        cursor = None
 
        while True:
            params = {
                "created_since": window_start.strftime("%Y-%m-%dT%H:%M:%SZ"),
                "created_before": window_end.strftime("%Y-%m-%dT%H:%M:%SZ"),
                "page_size": 10000,
            }
            if cursor:
                params["cursor"] = cursor
 
            response = requests.get(
                f"{ADA_BASE_URL}/messages",
                headers={"Authorization": f"Bearer {ADA_API_KEY}"},
                params=params,
            )
 
            if response.status_code == 429:
                retry_after = int(response.headers.get("Retry-After", 1))
                time.sleep(retry_after)
                continue
 
            response.raise_for_status()
            data = response.json()
            all_messages.extend(data.get("items", []))  # Note: "items" key, not "data"
 
            cursor = data.get("next_cursor")
            if not cursor:
                break
 
            time.sleep(0.35)
 
        window_start = window_end
 
    return all_messages
Warning

Do not transform during extraction. Write raw JSON to local disk or a staging database. Keeping a pristine copy of source data lets you re-process without re-querying Ada's API — which matters when working against a rate limit and a 12-month data cliff.

Crisp API: Import Constraints

Path 1: crisp-import-conversations Tool

Crisp maintains an official open-source import tool: crisp-import-conversations. Current supported adapters are: Gorgias, GrooveHQ, Help Scout, Tidio, WHMCS, and Zendesk.

There is no Ada adapter. You'll need to either write a custom adapter or pre-transform your Ada export into Crisp's expected JSON format before feeding it to the tool—the same pre-transformation strategy required for a Kustomer to Crisp migration. Since Ada's export is already JSON, pre-transformation is the simpler path.

Path 2: Direct Crisp REST API

The Crisp REST API V1 exposes these endpoints for migration:

Endpoint Purpose
POST /v1/website/{website_id}/conversation Create a conversation
POST /v1/website/{website_id}/conversation/{session_id}/message Send a message
PATCH /v1/website/{website_id}/conversation/{session_id}/meta Set conversation metadata
PATCH /v1/website/{website_id}/conversation/{session_id}/state Change state (resolved/unresolved)
PATCH /v1/website/{website_id}/conversation/{session_id}/routing Assign operator

Crisp Rate Limits and Token Strategy

The Crisp API has a multi-level rate limit based on client IP and user identifier. Plugin tokens bypass the standard API Global and API Route rate limiters, but are subject to a separate daily Plugin Quota system. Website tokens have a documented 10,000-request daily quota. Plugin token quotas default to 5,000/day but are configurable.

The API request cost formula for the import tool is approximately:

Total API calls = (n × 5) + (n × m)

Where n = number of conversations and m = average messages per conversation. For 10,000 conversations averaging 8 messages each, that's 130,000 API calls — a default development quota will not cover this. Request a production-tier quota increase from Crisp before starting any large import.

Error responses by type:

HTTP Status Crisp Error Code Meaning
429 rate_limited Quota exhausted; wait for daily reset or request higher quota
409 too_many_messages Conversation hit the 10,000+ message storage limit
422 invalid_data Schema violation; check required fields
403 not_allowed Token lacks required scope; check plugin permissions
404 not_found session_id or website_id does not exist

If you hit quota limits, request a higher quota from Crisp, then restart the import with the resume: true option so already-imported conversations are skipped.

Crisp Plan Limits

Plan Internal Notes Max Extra Participants
Free Not supported 1
Mini Supported 3
Essentials / Plus Supported 10

If your Ada conversations include internal notes from human agents during handoff, you need at least the Mini plan. On the Free plan, note-type messages will fail silently or be rejected — plan your Crisp tier before starting the import.

Crisp limits the total number of messages a conversation can store to 10,000+. If this limit is reached during import, the API returns 409 "too_many_messages". Ada conversations are typically shorter (AI-resolved interactions run 5–15 messages), but long handoff threads in enterprise accounts can approach this ceiling.

Conversation Visibility Quirk

A newly created Crisp conversation is not visible in the Inbox until a message is sent with from: user. Ada conversations where no end user ever typed a message — bot-only sessions or abandoned interactions — need explicit handling:

  • Option A: Inject a synthetic user message with the original timestamp and content like " [No user message — automated session]"
  • Option B: Import them with state: resolved immediately so they don't appear as open tickets
  • Option C: Skip them entirely if they have no business value

Crisp Fingerprint Collision Behavior

The fingerprint field in Crisp messages acts as a deduplication key for reruns. When a message is submitted with a fingerprint that already exists in the conversation, Crisp silently skips the duplicate — it does not overwrite and does not return an error. This makes fingerprint-based idempotency reliable for migration reruns, provided you use a stable key (Ada message ID or a hash of Ada message ID + conversation ID) rather than a timestamp that might collide across separate messages.

Attachment Upload Size Limits

Crisp's bucket upload endpoint (POST /website/{website_id}/bucket/url/generate) supports files up to 100 MB per upload. There is no documented limit on the number of uploads per conversation, but each file requires a separate API call (generate URL → upload to S3 → reference in message), which counts against your daily quota. For migrations with high attachment volume, factor attachment uploads into your total API call estimate.

Step-by-Step Migration Architecture

Step 1: Audit Your Ada Data

Before writing any code:

  • Total conversation count (check Ada Analytics dashboard)
  • Date range of conversations you need (remember the 12-month API limit)
  • Channels used (chat, email, voice, social)
  • Whether conversations include human handoff segments with internal notes
  • Custom variables and metadata fields in use
  • Attachment volume and file types
  • Knowledge base article count (if migrating self-service content)
  • Whether any required data falls outside the 12-month API window

Step 2: Extract from Ada

Use the extraction pattern above. Store raw JSON to a staging location. Extract in this order:

  1. Conversations (parent records, data key)
  2. Messages (child records, items key, linked by conversation_id)
  3. End Users (contact profiles)
  4. Knowledge articles (if migrating self-service content)

For delta sweeps during cutover, switch to updated_since for conversations. Ada's Data Export API data freshness is near real-time.

Step 3: Transform to Crisp Format

The transform layer is where most work happens.

Crisp conversation import JSON schema — required and optional fields:

{
  "email": "string (required if no people_id)",
  "people_id": "string (Crisp People UUID — alternative to email)",
  "phone": "string (optional)",
  "segments": ["array", "of", "strings"],
  "data": {
    "custom_key": "custom_value"
  },
  "messages": [
    {
      "type": "text|file|animation|audio|picker|field|note|event",
      "content": "string (required for text type)",
      "from": "user|operator (required)",
      "origin": "chat|email|urn:ietf:params:oauth:...",
      "timestamp": 1705312920000,
      "fingerprint": 1705312920000,
      "automated": false,
      "user": {
        "nickname": "string",
        "avatar": "string (URL)"
      }
    }
  ]
}

Required fields: email or people_id; at least one message with from: user for inbox visibility; type and from on each message.

from datetime import datetime
 
def transform_ada_to_crisp(ada_conversation, ada_messages):
    """Transform a single Ada conversation into Crisp import format."""
    crisp_messages = []
    private_notes = []
 
    for msg in sorted(ada_messages, key=lambda m: m["created_at"]):
        sender = msg.get("sender", "")
        ts_ms = int(
            datetime.fromisoformat(
                msg["created_at"].replace("Z", "+00:00")
            ).timestamp() * 1000
        )
 
        # Handle tool_call payloads as private notes
        if msg.get("tool_call"):
            tc = msg["tool_call"]
            import json
            note_content = (
                f"[TOOL CALL] {tc.get('name', 'unknown')}\n"
                f"Args: {json.dumps(tc.get('arguments', {}))}\n"
                f"Result: {json.dumps(tc.get('result', {}))}"
            )
            private_notes.append({
                "type": "note",
                "content": note_content,
                "from": "operator",
                "origin": "chat",
                "timestamp": ts_ms,
                "fingerprint": hash(f"toolcall_{msg['id']}") & 0x7FFFFFFF,
                "automated": True,
            })
 
        # Flatten quick_replies into message body
        body = msg.get("body", "")
        if msg.get("quick_replies"):
            options = " | ".join(msg["quick_replies"])
            body = f"{body} [Options: {options}]"
 
        crisp_msg = {
            "type": "text",
            "content": body,
            "from": "operator" if sender in ("bot", "agent") else "user",
            "origin": "chat",
            "timestamp": ts_ms,
            "fingerprint": hash(f"msg_{msg['id']}") & 0x7FFFFFFF,
        }
 
        if sender == "bot":
            crisp_msg["automated"] = True
        elif sender == "agent":
            crisp_msg["automated"] = False
            crisp_msg["user"] = {
                "nickname": msg.get("agent_name", "Agent"),
            }
 
        crisp_messages.append(crisp_msg)
 
    # Add tool call notes after their associated messages
    all_messages = sorted(
        crisp_messages + private_notes,
        key=lambda m: m["timestamp"]
    )
 
    return {
        "email": ada_conversation.get("chatter_email", ""),
        "segments": ada_conversation.get("tags", []),
        "data": {
            "ada_conversation_id": ada_conversation.get("id"),
            "ada_channel": ada_conversation.get("channel"),
            "ada_resolution": ada_conversation.get("resolution"),
            "ada_chatter_id": ada_conversation.get("chatter_id"),
        },
        "messages": all_messages,
    }

Key transform details:

  • Timestamps: Crisp expects millisecond-precision Unix timestamps. If you omit the timestamp field, Crisp stamps the message with import time, destroying your historical timeline.
  • Fingerprint: Use a hash of the Ada message ID. Fingerprint collisions cause silent skips (not errors), so stable keys based on source IDs are safer than timestamp-derived keys.
  • automated flag: Setting automated: true on bot messages lets Crisp distinguish AI replies from human ones in reporting and filtering.
  • Bot-heavy conversations: Ada AI-resolved conversations are often 90%+ bot messages. In Crisp, these appear as operator messages. Add a segment like ada-ai-resolved so agents can filter historical bot sessions from live operator queues.
Tip

Always store the original Ada conversation_id, chatter_id, and end_user_id in Crisp's custom data fields. This creates the crosswalk for post-migration validation and GDPR deletion requests that reference Ada record IDs.

Step 4: Handle Contacts / People

Ada tracks end users via its End Users API (GET /api/v1/end_users). Key fields returned:

{
  "id": "user_xyz789",
  "email": "user@example.com",
  "name": "Jane Doe",
  "phone": "+1-555-0100",
  "external_id": "CRM-4567",
  "created_at": "2023-06-01T09:00:00Z",
  "attributes": {
    "plan_type": "enterprise",
    "account_id": "ACC-1234"
  }
}

Rate limit: Ada's End Users API is subject to the standard API limits (not the Data Export limits) — check your contract for the applicable rate. The external_id field is only populated for users from custom channels; native web chat users may return null.

Option A — CSV import (fastest for one-time loads):

Export Ada end users, format as CSV with columns email, name, phone, and any custom fields, then import via Crisp's CRM. CSV files must be under 10 MB. Duplicate emails update existing profiles rather than creating new ones.

Option B — Crisp People API sync (required for custom metadata):

import requests
 
CRISP_BASE = "https://api.crisp.chat/v1"
WEBSITE_ID = "your-website-id"
 
def upsert_crisp_contact(ada_user, identifier, key):
    """Create or update a Crisp People profile from an Ada end user record."""
    payload = {
        "email": ada_user.get("email"),
        "person": {
            "nickname": ada_user.get("name", ""),
            "phone": ada_user.get("phone", ""),
        },
        "data": {
            "ada_user_id": ada_user.get("id"),
            "ada_external_id": ada_user.get("external_id"),
            "ada_plan_type": ada_user.get("attributes", {}).get("plan_type"),
            "ada_account_id": ada_user.get("attributes", {}).get("account_id"),
        },
    }
 
    # Try to find existing contact by email first
    search_response = requests.get(
        f"{CRISP_BASE}/website/{WEBSITE_ID}/people/profiles",
        params={"search_query": ada_user.get("email"), "search_type": "email"},
        auth=(identifier, key),
        headers={"X-Crisp-Tier": "plugin"},
    )
 
    if search_response.status_code == 200:
        results = search_response.json().get("data", {}).get("items", [])
        if results:
            people_id = results[0]["people_id"]
            # Update existing profile
            update_response = requests.patch(
                f"{CRISP_BASE}/website/{WEBSITE_ID}/people/profile/{people_id}",
                json=payload,
                auth=(identifier, key),
                headers={
                    "Content-Type": "application/json",
                    "X-Crisp-Tier": "plugin",
                },
            )
            return people_id, update_response.status_code
 
    # Create new profile
    create_response = requests.post(
        f"{CRISP_BASE}/website/{WEBSITE_ID}/people/profile",
        json=payload,
        auth=(identifier, key),
        headers={
            "Content-Type": "application/json",
            "X-Crisp-Tier": "plugin",
        },
    )
    created = create_response.json().get("data", {})
    return created.get("people_id"), create_response.status_code

A common pattern: preload contacts via CSV for speed, then use the API to patch custom data fields like ada_user_id and account identifiers.

If an Ada end user is completely anonymous (no email or phone), Crisp treats them as a transient visitor. Store the Ada chatter_id in the conversation's custom data object so you can still trace the record.

Step 5: Re-host Attachments

Ada conversations often contain image uploads and PDFs. Ada media URLs are scoped to your instance and are typically authenticated or time-limited — you cannot pass them directly to Crisp.

To migrate attachments:

  1. Download the file from the Ada URL to your staging server.
  2. Request an upload URL from Crisp: POST /website/{website_id}/bucket/url/generate
  3. Upload the file to the provided Crisp S3 bucket URL via HTTP PUT.
  4. Send a message to the Crisp conversation with type: file referencing the new URL.

Crisp's bucket upload supports files up to 100 MB. Each attachment requires 2 API calls (generate + upload reference), factored into your quota calculation.

If you skip attachment re-hosting, all attachment links break the moment your Ada instance is deactivated — or earlier if Ada uses time-limited URL signatures.

Step 6: Load into Crisp

Using the crisp-import-conversations tool with pre-transformed JSON:

git clone https://github.com/crisp-im/crisp-import-conversations.git
cd crisp-import-conversations
npm install
var CrispImport = require("./lib/import");
 
var Import = new CrispImport(
  {
    websiteId: "YOUR_WEBSITE_ID",
    tier: "plugin",
    identifier: "YOUR_TOKEN_IDENTIFIER",
    key: "YOUR_TOKEN_KEY",
    urn: "YOUR_PLUGIN_URN",
    name: "ada-migration"
  },
  {}  // No adapter — data is pre-transformed to Crisp format
);
 
Import.importFromFile("./res/conversations.json")
  .then((result) => {
    console.log("Import complete:", result);
  })
  .catch((error) => {
    console.error("Import failed:", error);
  });

If you prefer the direct REST API, the sequence for each conversation is:

  1. POST /conversation — create the conversation, capture the returned session_id
  2. PATCH /conversation/{session_id}/meta — set nickname, email, avatar, phone
  3. POST /conversation/{session_id}/message — send each message in chronological order with preserved timestamps

Step 7: Set Conversation States

After import, all conversations land in Crisp as "unresolved" by default. Batch-update resolved Ada conversations:

import requests
 
CRISP_BASE = "https://api.crisp.chat/v1"
WEBSITE_ID = "your-website-id"
 
def resolve_conversation(session_id, identifier, key):
    response = requests.patch(
        f"{CRISP_BASE}/website/{WEBSITE_ID}/conversation/{session_id}/state",
        json={"state": "resolved"},
        auth=(identifier, key),
        headers={
            "Content-Type": "application/json",
            "X-Crisp-Tier": "plugin",
        },
    )
    if response.status_code == 403:
        print(f"Scope error on {session_id} — check plugin permissions")
    elif response.status_code == 404:
        print(f"Session not found: {session_id} — verify import completed")
    return response.status_code

Each state change counts as one API call against your daily quota. For 10,000 resolved conversations, that's 10,000 additional calls — include this in your quota calculation.

Step 8: Delta Sync and Cutover

A safe cutover sequence:

  1. Historical backfill: Migrate all closed Ada conversations up to the current date.
  2. User validation: Have business users review a sample of 50–100 conversations in Crisp against the Ada original.
  3. Freeze: Pause non-essential Ada configuration changes.
  4. Delta pass: Export recently updated and newly created Ada conversations using updated_since. Load them into Crisp.
  5. Cutover: Switch your live chat widget, email routing, and handoff destination to Crisp.

Do not run operators in both systems simultaneously — that creates data forks that require manual reconciliation.

Knowledge Base Migration

If you're using Ada's Knowledge Hub, you'll want to migrate articles to Crisp's Helpdesk. Crisp's native knowledge-base importer supports: Front, Help Scout, HubSpot, Intercom, LiveAgent, WordPress, and Zendesk — not Ada.

Ada's Knowledge API rate limit: up to 200 requests/second, with a default article limit of 50,000 and a 100 KB cap per article.

Your options:

  • If Ada's knowledge base is public-facing, test whether Crisp's URL-based importer can crawl it (Ada is not officially supported, but the crawler may work if articles are publicly accessible).
  • Export articles via the Ada Knowledge API and recreate them using Crisp's Helpdesk API: initialize the helpdesk → add locales → create categories/sections → create articles → publish.
  • Manual Markdown conversion if the article count is under ~50.

After loading, verify: broken internal links, missing images, incorrect locale placement, and category hierarchy integrity.

Validation Checklist

After the migration completes, validate before decommissioning Ada:

Check Method Pass Criteria
Conversation count Compare Ada export count vs Crisp count 100% match
Message count per conversation Spot-check 50+ random conversations All messages present, correct order
Contact profiles Compare Ada end user count vs Crisp People count All contacts with email present
Attachments Spot-check 20 conversations with files All files accessible from new Crisp URLs
Metadata / custom data Spot-check 30 conversations Ada variables present in Crisp data fields
Conversation states Count resolved vs unresolved Matches Ada resolution status
Timestamps Spot-check earliest and latest conversations Correct dates, no timezone drift
Segments / tags Verify Ada tags appear as Crisp segments All tags mapped
Channel coverage Check at least one transcript per source channel No channel missed
Idempotency Re-run the loader against a sample No duplicate records created (fingerprint collision = silent skip, not error)
Knowledge articles Crawl for broken links, missing images All articles accessible
Tool call notes Spot-check conversations with integrations Private notes present with tool call data
Voice transcripts Verify ada-voice-transcript segment applied All voice conversations identified

Build a reconciliation script that automatically compares source counts against target counts — manual spot-checking alone is insufficient for migrations above 5,000 conversations.

Keep a crosswalk table mapping:

  • Ada conversation_id → Crisp session_id
  • Ada message_id → Crisp message fingerprint
  • Ada end_user_id / chatter_id → Crisp people_id

You'll need this for post-migration support tickets and GDPR deletion requests that reference Ada record IDs.

Timeline Estimates

Migration Size Extraction Transform + Load Validation Total
< 5,000 conversations 1–2 hours 4–8 hours 2–4 hours 1–2 days
5,000–50,000 conversations 4–8 hours 1–3 days 1 day 3–5 days
50,000+ conversations 1–2 days 3–7 days (quota-dependent) 2–3 days 1–2 weeks

The bottleneck is almost always the Crisp side — specifically daily plugin quota resets. For migrations over 50,000 conversations, request a production-tier quota increase from Crisp before starting. Factor in at least one full re-run for error recovery when sizing your timeline.

API call count estimate for sizing quota requests:

Quota needed = (conversations × 6) + (conversations × avg_messages_per_conversation) + attachments × 2 + resolved_conversations

For 50,000 conversations averaging 8 messages with 10% attachment rate and 80% resolved: (50,000 × 6) + (50,000 × 8) + (5,000 × 2) + 40,000 = 760,000 API calls — requiring approximately 152 days at default quota, or a single run with a negotiated higher quota.

When to Bring in Help

This migration is technically straightforward for teams with API experience, but edge cases compound: voice conversation handling, attachment re-hosting against the 12-month data cliff, the data vs items response key inconsistency, bot-only conversation visibility, tool call payload serialization, and quota-gated loading all create compounding risk under a deadline.

If your team doesn't have a dedicated engineer available for 1–2 weeks, or if you're migrating more than 50,000 conversations, external help reduces risk. The key in-house risk mitigation remains the same regardless: extract your Ada data now, even before you're ready to load into Crisp. The 12-month window is a hard wall that moves closer every day.

Frequently Asked Questions

Can I export Ada conversation data as CSV for Crisp?
Ada does not offer a CSV export for full conversation and message data. You must use the Ada Data Export API (GET /api/v2/export/conversations and /messages), which returns JSON. The API is rate-limited, capped at 10,000 records per page, restricted to 60-day query windows, and only provides data from the past 12 months.
Does Crisp have a built-in Ada import tool?
Not directly. Crisp offers an open-source crisp-import-conversations tool with adapters for Zendesk, Gorgias, HelpScout, Tidio, WHMCS, and GrooveHQ — but no Ada adapter. You need to pre-transform Ada's JSON export into Crisp's expected format or write a custom adapter.
How long does an Ada to Crisp migration take?
For under 5,000 conversations, expect 1–2 days. For 5,000–50,000 conversations, plan for 3–5 days. Migrations over 50,000 conversations can take 1–2 weeks, primarily due to Crisp's daily API quota resets. Request a production-tier quota increase from Crisp before starting large migrations.
What data is lost when migrating from Ada to Crisp?
Ada's AI coaching data, Playbooks, Simulations, bot decision trees, resolution analytics, AI confidence scores, quick_replies, and tool_call payloads do not transfer. Voice conversation audio and telephony metadata are also lost — only text transcripts can be imported. Conversations older than 12 months are inaccessible via Ada's API.
How do I preserve historical timestamps when importing into Crisp?
Pass the original Ada timestamp as a millisecond-precision Unix timestamp in the 'timestamp' field of your Crisp message JSON payload. If you omit this field, Crisp stamps the message with the current time, destroying your historical timeline. You can also use the 'fingerprint' field as a dedupe key for safe reruns.

More from our Blog

Kustomer to Crisp Migration: A Technical Guide
Kustomer/Migration Guide/Help Desk

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
Freshchat to Ada Migration: A Technical Guide
Migration Guide/Help Desk/Freshdesk

Freshchat to Ada Migration: A Technical Guide

Technical guide to migrating from Freshchat to Ada. Covers API constraints, data mapping, knowledge loading, conversation archiving, and bot flow rebuilds.

Rishabh Rishabh · · 20 min read