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.
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
datakey; messages return underitems. This inconsistency breaks generic parsers — handle each endpoint separately. sendervalues:"bot","chatter"(end user),"agent"(human agent during handoff).quick_repliesis an array of strings or null. These button labels have no clickable equivalent in Crisp.tool_callcontains the function name, arguments, and result from Ada's Reasoning Engine integrations. Neither the call nor the result has a first-class Crisp target.variablesin 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
bodyfield contains the transcribed text, and achannel: "voice"field identifies the source. Treat these as text-only records in Crisp and tag them with aada-voice-transcriptsegment 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 recordsGET /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
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_beforeend date, Ada defaults to a 7-day slice fromcreated_since. This quietly creates holes in long backfills. - Conversation queries support
created_sinceorupdated_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_messagesDo 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: resolvedimmediately 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:
- Conversations (parent records,
datakey) - Messages (child records,
itemskey, linked byconversation_id) - End Users (contact profiles)
- 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
timestampfield, 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.
automatedflag: Settingautomated: trueon 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-resolvedso agents can filter historical bot sessions from live operator queues.
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_codeA 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:
- Download the file from the Ada URL to your staging server.
- Request an upload URL from Crisp:
POST /website/{website_id}/bucket/url/generate - Upload the file to the provided Crisp S3 bucket URL via HTTP PUT.
- Send a message to the Crisp conversation with
type: filereferencing 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 installvar 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:
POST /conversation— create the conversation, capture the returnedsession_idPATCH /conversation/{session_id}/meta— set nickname, email, avatar, phonePOST /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_codeEach 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:
- Historical backfill: Migrate all closed Ada conversations up to the current date.
- User validation: Have business users review a sample of 50–100 conversations in Crisp against the Ada original.
- Freeze: Pause non-essential Ada configuration changes.
- Delta pass: Export recently updated and newly created Ada conversations using
updated_since. Load them into Crisp. - 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→ Crispsession_id - Ada
message_id→ Crisp messagefingerprint - Ada
end_user_id/chatter_id→ Crisppeople_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.