---
title: "Ada to Crisp Migration: A Technical Guide"
slug: ada-to-crisp-migration-a-technical-guide
date: 2026-08-17
author: Nachi
categories: [Migration Guide, Help Desk]
excerpt: "Technical guide to migrating from Ada to Crisp: Data Export API constraints, object mapping, Crisp import tooling, rate limits, edge cases, and validation."
tldr: "Ada to Crisp migration requires extracting via Ada's Data Export API (12-month limit, 60-day query windows), transforming to Crisp's schema, and loading via the crisp-import-conversations tool or REST API."
canonical: https://clonepartner.com/blog/ada-to-crisp-migration-a-technical-guide/
---

# Ada to Crisp Migration: A Technical Guide


# 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:**
```json
{
  "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:**
```json
{
  "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](https://clonepartner.com/blog/blog/freshchat-to-ada-migration-a-technical-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

> [!CAUTION]
> 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`).

```python
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](https://clonepartner.com/blog/blog/kustomer-to-crisp-migration-a-technical-guide/). 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:

```json
{
  "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.

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

```json
{
  "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):

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

```bash
git clone https://github.com/crisp-im/crisp-import-conversations.git
cd crisp-import-conversations
npm install
```

```javascript
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:

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

> Need help migrating from Ada to Crisp? Our engineers will review your setup, map your data model, and handle the full extraction, transformation, loading, and validation — zero data loss, zero downtime. Book a free 30-minute call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

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