---
title: "Ada to Ada Migration: The CTO's Technical Guide"
slug: ada-to-ada-migration-the-ctos-technical-guide
date: 2026-07-28
author: Nachi
categories: [Migration Guide]
excerpt: "A technical guide to migrating between Ada instances — covering API constraints, knowledge transfer, conversation archival, end-user handling, and the edge cases that break DIY scripts."
tldr: Ada has no native instance-to-instance transfer. Knowledge and end users migrate via API; conversations can only be archived externally. Playbooks require manual recreation.
canonical: https://clonepartner.com/blog/ada-to-ada-migration-the-ctos-technical-guide/
---

# Ada to Ada Migration: The CTO's Technical Guide


# Ada to Ada Migration: The CTO's Technical Guide

*Last updated: June 2025. Covers Ada Platform API v2.*

Migrating from one Ada instance to another — whether you're consolidating after an acquisition, restructuring regions, or moving to a new contract — requires extracting conversations, end users, knowledge articles, and configuration from a source Ada account and loading them into a target account. Ada does not offer a native instance-to-instance transfer feature. Every data object must be pulled through the Data Export API or platform APIs, transformed, and pushed into the destination via the corresponding import endpoints.

The critical constraint you need to know upfront: **conversations and messages can be exported but cannot be imported back into a new Ada instance via API.** There is no `POST /conversations` endpoint that accepts historical conversation data. The Conversations API creates *new, live* conversations — it does not accept historical records. This single limitation shapes every architectural decision in an Ada-to-Ada migration.

The right mental model — and the one that should guide every planning decision — is **rebuild plus archive plus staged cutover**: knowledge transfers cleanly via bulk upsert, end users transfer with timing caveats, conversations can only be archived externally, configuration migrates with re-authentication requirements, and playbooks require manual work.

This guide covers the API constraints, data mapping, migration architecture, timing estimates, and edge cases you'll encounter.

## Why Teams Run an Ada-to-Ada Migration

Ada is an agentic customer experience (ACX) platform that automates customer conversations using AI agents across messaging, email, and voice channels. Common triggers for moving between Ada instances:

- **M&A consolidation:** Two companies both run Ada. Post-acquisition, leadership wants a single instance with unified reporting, knowledge, and bot configuration.
- **Regional restructuring:** A global org moves from per-region Ada instances to a centralized account for cost and management simplicity.
- **Contract or plan migration:** Switching from one Ada subscription or contract entity to another (e.g., a parent company takes over billing) often requires standing up a fresh instance.
- **Agency transitions:** Taking ownership of an Ada instance previously managed by a third-party agency.
- **Scripted-to-generative migration:** Moving from an older scripted Ada bot (Answers, training, blocks, request blocks) to Ada's newer knowledge-and-playbook model. The data models diverge significantly — an export useful on one side is often not load-ready on the other. ([docs.ada.cx](https://docs.ada.cx/scripted/docs/build-and-maintain-your-bot/train-and-manage-answers/export-your-bot-s-answer-content-and-training))
- **Environment promotion:** Moving bot configuration, knowledge, and test data from a staging environment to production.
- **Data residency compliance:** Relocating to a different Ada hosting region to meet GDPR, HIPAA, or other regulatory requirements.

None of these scenarios have a "click to migrate" solution. Every one requires API work.

## Ada's Data Model: What You're Actually Moving

Before choosing an approach, map out what needs to transfer. Ada's data model is not a traditional helpdesk or CRM — it's a conversational AI platform with distinct object types. The documented objects are end users, conversations, messages, knowledge sources, knowledge articles, tags, variables, persona, custom instructions, integrations, and webhook surfaces — not Accounts, Leads, or Opportunities. ([docs.ada.cx](https://docs.ada.cx/generative/reference/introduction/overview))

### Core Data Objects in Ada

| Object | Description | API Available | Exportable | Importable |
|---|---|---|---|---|
| **Conversations** | Full chat sessions with metadata (CSAT, resolution, channel, timestamps) | Data Export API | ✅ (last 12 months) | ❌ (no bulk import endpoint) |
| **Messages** | Individual messages within conversations | Data Export API | ✅ (last 12 months) | ❌ (no bulk import endpoint) |
| **End Users** | Customer profiles with metadata and metavariables | End Users API | ✅ | ✅ (create/update) |
| **Knowledge Sources** | Containers for knowledge content | Knowledge API | ✅ | ✅ |
| **Knowledge Articles** | Individual articles within sources | Knowledge API | ✅ | ✅ (bulk upsert) |
| **Knowledge Tags** | Labels for organizing articles | Knowledge API | ✅ | ✅ (bulk upsert) |
| **Variables** | Metavariable definitions (keys, types, display names) | Variables API | ✅ (read-only) | ❌ |
| **Custom Instructions** | AI behavior rules and guardrails | Custom Instructions API | ✅ | ✅ |
| **Persona** | Bot personality and tone settings | Persona API | ✅ | ✅ (update) |
| **Integrations** | Connected external platforms | Integrations API | ✅ | ✅ |
| **Webhooks** | Event subscription configurations | Webhooks API | ✅ | ✅ |
| **Playbooks & Coaching** | Bot training and optimization data | Dashboard only | ❌ | ❌ |
| **Glossary** | Terms, definitions, voice flags | CSV export/import | ✅ | ✅ (full replace) |
| **Audit Log** | Activity and change tracking | Audit Log API | ✅ | ❌ |

### The Scripted vs. Generative Gap

If you're migrating from a scripted Ada bot to a generative Agent, the data models diverge in ways that matter:

| Scripted Bot Objects | Generative Agent Objects |
|---|---|
| Answers | Knowledge Articles |
| Training Questions | (absorbed into knowledge; no 1:1 equivalent) |
| Blocks / Request Blocks | Actions + Playbooks |
| N/A | Custom Instructions |
| N/A | Persona |
| Variables | Variables (schema may differ) |

Plan for a rebuild, not a copy-paste. Answers must be restructured as knowledge articles. Blocks and request blocks must be reimplemented as Actions and Playbooks through the dashboard — there's no automated conversion.

## Migration Approaches

### 1. API-Based Migration (Ada Export APIs → Ada Import APIs)

**How it works:** Extract data from the source instance using Ada's Data Export API (conversations/messages), End Users API, and Knowledge API. Transform the data as needed. Load it into the target instance using the Knowledge API (bulk upsert), End Users API (create), Custom Instructions API, and Persona API.

**When to use:** Every Ada-to-Ada migration will use this approach for some portion of the data. It's the only programmatic path for a full migration.

**Pros:**
- Full control over data transformation
- Handles knowledge and end-user migration end-to-end
- Scriptable, repeatable, auditable

**Cons:**
- Conversations and messages cannot be loaded into the target Ada instance
- Data Export API only surfaces the last 12 months of data ([docs.ada.cx](https://docs.ada.cx/generative/reference/data-export/overview))
- Rate limits constrain throughput (10 req/sec for Data Export, 60,000 req/day for End Users and Conversations APIs)
- Multiple API surfaces to coordinate — not every object has the same write path

**Time-to-complete estimates** (based on API rate limits):
- 50,000 knowledge articles via bulk upsert at 200 req/sec: **under 5 minutes**
- 500,000 end users at 60,000 req/day: **minimum 9 days**
- 12 months of conversations (Data Export, 60-day windows): **7 sequential queries minimum**, typically 1–3 hours depending on volume

**Complexity:** Medium to High

### 2. Knowledge-Only Migration via Knowledge API

**How it works:** If the migration goal is primarily transferring the AI agent's knowledge base — articles, sources, and tags — the Knowledge API provides a clean path with full read/write coverage.

**When to use:** Environment promotion (staging → production), bot cloning for a new brand, or when only the knowledge layer needs to move.

**Pros:**
- Cleanest migration path in Ada's ecosystem
- Bulk upsert handles both creates and updates idempotently
- Tags and source associations preserved

**Cons:**
- 50,000 article limit per instance (default; higher limits available on some plans)
- Individual article max size: 100KB
- Max request payload: 10MB
- Indexing delay: articles become available to AI responses within ~5 minutes

**Complexity:** Low to Medium

### 3. CSV/Manual Export (Content and Archival)

**How it works:** Export conversations and messages via the Data Export API, transform to CSV or flat format, and store in your data warehouse. For scripted bots, export Answers and training content plus the glossary CSV. This isn't a full migration into the target Ada instance — it's an archival and content-rebuild strategy.

**When to use:** Small scripted-bot content moves, compliance-driven conversation archival, or when you accept the target won't contain historical chats natively.

**Pros:**
- Low engineering effort for content transfer
- Full conversation data preserved externally
- Meets compliance requirements (GDPR, HIPAA archival)
- Can feed BI tools for historical reporting

**Cons:**
- No full tenant clone — agents can't reference archived data in the new instance
- Glossary CSV import replaces the entire glossary with the uploaded file — it's a full replace, not a merge ([docs.ada.cx](https://docs.ada.cx/docs/automation/glossary))
- No channel or automation parity

**Complexity:** Low

### 4. Custom ETL Pipeline

**How it works:** Build an orchestrated pipeline (using tools like AWS Glue, dbt, or Airflow) that extracts all API-accessible objects from the source, transforms and enriches them (deduplication, field normalization, metadata cleanup), and loads what's importable into the target. Conversations get routed to cold storage.

**When to use:** Enterprise migrations with large data volumes, complex metadata schemas, strict compliance requirements, or when you need replayability and audit reports.

**Pros:**
- Most complete approach — handles all data types with appropriate routing
- Best observability and rollback story
- Auditable and repeatable

**Cons:**
- Highest engineering investment
- Still cannot import conversations into Ada
- Requires monitoring and error handling for rate limits
- Teams usually underestimate transform logic complexity

**Complexity:** High

### 5. Middleware Platforms (Zapier, Make)

**How it works:** Use an integration platform to connect source and target Ada instances, triggering transfers on specific events. Ada webhooks require a 2xx response within 15 seconds and retry for up to a week. ([docs.ada.cx](https://docs.ada.cx/reference/introduction/webhooks))

**When to use:** Ongoing sync between two Ada instances during a coexistence period — not for bulk historical migration.

**Pros:**
- No custom code required
- Good for incremental, event-driven sync (profile updates, notifications)

**Cons:**
- Cannot handle bulk historical data transfer
- No conversation import capability
- Expensive at scale (per-operation pricing)
- Weak batch control and ID crosswalks
- Zapier documents webhook throttling and delayed processing under load ([help.zapier.com](https://help.zapier.com/hc/en-us/articles/29972220283789-Webhooks-by-Zapier-rate-limits))

**Complexity:** Low (but limited scope)

### Approach Comparison Table

| Criteria | API-Based | Knowledge-Only | CSV/Manual | Custom ETL | Middleware |
|---|---|---|---|---|---|
| **Conversations migrated into target** | ❌ | N/A | ❌ | ❌ | ❌ |
| **Knowledge base transferred** | ✅ | ✅ | Partial | ✅ | Partial |
| **End users transferred** | ✅ | ❌ | ❌ | ✅ | Partial |
| **Configuration transferred** | ✅ | ❌ | ❌ | ✅ | ❌ |
| **Historical data preserved** | External only | N/A | ✅ (external) | ✅ (external) | ❌ |
| **Engineering effort** | Medium | Low | Low | High | Low |
| **Best for** | Full migration | Bot cloning | Compliance archival | Enterprise | Interim sync |

### Recommendations by Scenario

- **Small team, simple bot:** Knowledge-Only migration plus manual persona/instruction setup. Archive conversations to CSV if needed.
- **Enterprise, multi-brand:** Custom ETL pipeline with staged rollout. Invest in proper archival and validation.
- **M&A consolidation:** API-Based migration for all importable objects plus ETL pipeline for conversation archival to data warehouse.
- **One-time move, low engineering bandwidth:** Managed migration service.
- **Ongoing sync during transition:** Middleware for real-time end-user sync; batch jobs for knowledge sync.
- **Scripted-to-generative move:** API-based rebuild. Plan for manual recreation of Answers as knowledge articles and Playbooks.

## Pre-Migration Planning

### Data Audit Checklist

Before writing a single line of migration code, inventory what exists in the source instance:

- [ ] **Conversations:** Total count, date range, channels used (chat, email, voice, social)
- [ ] **Messages:** Total count, average per conversation
- [ ] **End Users:** Total count, metadata fields in use, sensitive metadata usage
- [ ] **Knowledge Sources:** Count, types (API, web import, Zendesk sync, Salesforce sync, manual)
- [ ] **Knowledge Articles:** Count per source, languages, tag usage, active vs. inactive
- [ ] **Custom Instructions:** Count, complexity
- [ ] **Persona:** Current configuration
- [ ] **Variables:** List of all metavariables, their types, and variable IDs
- [ ] **Glossary:** Terms, definitions, voice flags, CSV limits ([docs.ada.cx](https://docs.ada.cx/docs/automation/glossary))
- [ ] **Integrations:** Active integrations (Zendesk, Salesforce, Genesys, etc.)
- [ ] **Webhooks:** Active subscriptions, event types, and target URLs
- [ ] **Playbooks, Coaching, Actions:** Number and complexity (these require manual recreation)
- [ ] **Channel configuration:** Approved domains, rollout settings, email BYOD/forwarding

### Identify What to Leave Behind

Not everything should move:

- **Inactive knowledge articles** — clean them out instead of porting dead content
- **Test conversations** — filter out internal testing data
- **Stale end users** — users with no conversations in 12+ months add noise, not value
- **Deprecated integrations** — if you're consolidating, some integrations won't apply to the target

> [!WARNING]
> **Two Ada-specific traps catch teams early:** The Data Export API only exposes the last 12 months of history, and Glossary CSV import replaces the entire glossary with the file you upload — it's a full replace, not a merge. ([docs.ada.cx](https://docs.ada.cx/generative/reference/data-export/overview))

### Migration Strategy

| Strategy | Description | Best For |
|---|---|---|
| **Big Bang** | Migrate everything in a single window, then cut over | Simple bots, low conversation volume |
| **Phased** | Migrate knowledge first, then end users, then go live. Archive conversations separately. | Most scenarios |
| **Incremental** | Sync data continuously during a coexistence period, then cut over | M&A with overlapping operations |

For most Ada-to-Ada migrations, **phased is the right call**. Knowledge transfer is the highest-value, lowest-risk step. Get that right and validated before touching end users or going live. Ada's staged chat rollout controls and email percentage-based routing support gradual traffic shifts instead of hard cutovers. ([docs.ada.cx](https://docs.ada.cx/generative/docs/channels/chat/chat-configuration/launch-options))

## Data Mapping: Source Ada → Target Ada

Since both sides are Ada, field mapping is theoretically 1:1. In practice, it's not that simple — especially if your source and target instances have different variable schemas, the source is scripted while the target is generative, or you're consolidating instances with overlapping end-user populations.

### Knowledge Articles

| Source Field | Target Field | Notes |
|---|---|---|
| `id` | `id` | Can reuse the same ID for idempotent upsert |
| `title` | `title` | Direct map |
| `body` | `body` | Max 100KB per article — split if oversized |
| `language` | `language` | BCP 47 tags (e.g., `en-CA`, `fr-FR`) |
| `source_id` | `source_id` | Must create matching source in target first |
| `status` | `status` | `active` / `inactive` |
| `tags` | `tags` | Must upsert tags before associating |
| `external_url` | `external_url` | Direct map |
| `external_id` | `external_id` | Direct map |

### End Users

| Source Field | Target Field | Notes |
|---|---|---|
| `end_user_id` | `end_user_id` | New ID generated in target — cannot preserve source ID |
| `profile.email` | `profile.email` | Use as primary deduplication key |
| `profile.phone` | `profile.phone` | Direct map; secondary dedup key |
| `profile.name` | `profile.name` | Direct map |
| `profile.language` | `profile.language` | Validate locale support on target |
| `profile.metadata` | `profile.metadata` | Key-value pairs must match target variable schema |
| `profile.sensitive_metadata` | `profile.sensitive_metadata` | Handle with encryption; requires special API field |
| `external_id` | `external_id` | Stable identity key — use for deterministic mapping ([docs.ada.cx](https://docs.ada.cx/generative/reference/end-users/overview)) |

> [!WARNING]
> **End users created via API that are not associated with a conversation within 24 hours are automatically deleted.** You cannot pre-populate end-user records in the target instance days before go-live. Time end-user creation to coincide with the bot going live, or accept that user records will be recreated organically as customers interact with the new bot.

### Conversations & Messages (Archive Only)

| Source Field | Target Field | Notes |
|---|---|---|
| `conversation_id` | N/A (archive) | Not importable into target Ada |
| `created` / `ended` | N/A (archive) | Timestamps preserved in archive |
| `channel` | N/A (archive) | chat, email, voice, social |
| `csat_score` | N/A (archive) | Numeric CSAT value |
| `resolution` | N/A (archive) | Whether auto-resolved |
| `agent_id` / `agent_name` | N/A (archive) | Human agent attribution |
| `messages [].body` | N/A (archive) | Full message content |
| `messages [].sender_type` | N/A (archive) | bot, end_user, or agent |
| `messages [].created` | N/A (archive) | Per-message timestamp |

### Configuration Objects

| Object | Migration Method |
|---|---|
| Custom Instructions | Export via `GET /api/v2/custom-instructions`, create in target via `POST`. Create inactive first, activate after validation. |
| Persona | Export via `GET /api/v2/persona`, update in target via `PATCH` |
| Variables | Export list via `GET /api/v2/variables` — must be recreated manually or through dashboard (API is read-only) |
| Webhooks | Export via `GET /api/v2/webhooks`, recreate in target (update target URLs as needed). See webhook migration section below. |
| Integrations | Export config via `GET /api/v2/integrations` — most require re-authentication in target |
| Playbooks & Coaching | Not API-accessible — must be manually recreated in dashboard |
| Glossary | Export CSV from source, import to target (replaces entire glossary) |

### Webhook Migration Reference

Webhooks require special handling during migration because active webhooks on the source instance will continue firing to downstream systems. If you recreate the same webhooks on the target before decommissioning the source, downstream systems receive duplicate events.

| Webhook Event Type | Migration Action | Risk if Mishandled |
|---|---|---|
| `conversation.created` | Create on target only after traffic cutover | Duplicate conversation creation events in downstream systems |
| `conversation.ended` | Create on target only after traffic cutover | Duplicate resolution notifications |
| `conversation.handoff` | Create on target, update target URL if handoff system changed | Failed handoffs to human agents |
| `end_user.created` | Create on target; suppress on source during coexistence | Duplicate user creation in CRM |
| `end_user.updated` | Create on target; suppress on source during coexistence | Conflicting profile updates |

**Recommended sequence:** (1) Document all active webhooks on source. (2) Create webhooks on target in a disabled/paused state. (3) Cut traffic to target. (4) Enable target webhooks. (5) Disable source webhooks. (6) Verify downstream event delivery.

### Business Object Mapping

If your team is coming from a CRM-centric mindset, resist the urge to force CRM vocabulary onto Ada. Here's how business concepts map to Ada's actual objects:

| Business Concept | Ada Equivalent | Migration Note |
|---|---|---|
| Accounts / Companies | No native object | Store as end-user metadata or upstream CRM reference |
| Contacts | End Users | Map to `external_id`, language, and metadata |
| Leads | No native object | Model upstream; optionally flag in metadata |
| Opportunities | No native object | Keep in CRM; pass reference IDs only |
| Activities | Conversations + Messages | Exportable for archive, not importable into target |
| Custom Objects | No generic custom-object surface | Use metadata, variables, tags, or external system of record |

> [!NOTE]
> Knowledge article availability rules reference variable IDs. If source and target do not share the same variable IDs (which they won't — new IDs are generated in the target), remap by variable name before loading rules or playbooks. Export the Variables API response from both instances and build a `{source_variable_name: target_variable_id}` lookup table.

## Migration Architecture

### Data Flow: Extract → Transform → Load

```
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   SOURCE ADA    │     │   STAGING LAYER  │     │   TARGET ADA    │
│   INSTANCE      │────▶│   (Transform)    │────▶│   INSTANCE      │
│                 │     │                  │     │                 │
│ Data Export API │     │ - Deduplicate    │     │ Knowledge API   │
│ End Users API   │     │ - Normalize meta │     │ End Users API   │
│ Knowledge API   │     │ - Map IDs        │     │ Custom Instr API│
│ Config APIs     │     │ - Validate       │     │ Persona API     │
└─────────────────┘     │                  │     └─────────────────┘
                        │        │         │
                        │        ▼         │
                        │  ┌───────────┐   │
                        │  │ DATA      │   │
                        │  │ WAREHOUSE │   │
                        │  │ (archive) │   │
                        │  └───────────┘   │
                        └──────────────────┘
```

### API Authentication

Both instances require v2 API tokens. Generate these from **Settings > Integrations > APIs** in each Ada dashboard. In v2, a single token works across all API endpoints — you no longer need separate keys per API family.

```python
SOURCE_BASE = "https://source-company.ada.support"
TARGET_BASE = "https://target-company.ada.support"

HEADERS_SOURCE = {"Authorization": "Bearer <SOURCE_API_TOKEN>"}
HEADERS_TARGET = {"Authorization": "Bearer <TARGET_API_TOKEN>"}
```

### Rate Limit Management

Ada's rate limits require careful orchestration:

| API | Rate Limit | Practical Impact |
|---|---|---|
| Data Export (conversations, messages) | 10 req/sec per endpoint | Page size max 10,000 records; 60-day date range per query. Full 12-month extraction = 7 sequential queries minimum. |
| End Users API | 60,000 req/day | 500K users = **~9 days minimum**. ([docs.ada.cx](https://docs.ada.cx/2026-03-03-increased-api-rate-limits)) |
| Knowledge API (bulk upsert) | 60,000/day, 1,000/min, 200/sec | 50K articles at ~500/batch = 100 requests = **under 5 minutes**. 10MB max payload, 50K article cap. ([docs.ada.cx](https://docs.ada.cx/reference/knowledge/overview)) |
| Conversations API | 60,000 req/day | Creates new live conversations only — not a history import path |

> [!NOTE]
> The Data Export API limits queries to a **60-day date range** and provides access to only the **last 12 months** of data. For a full year extraction, you need a minimum of 7 sequential API calls (6 × 60-day windows + remainder). ([docs.ada.cx](https://docs.ada.cx/generative/reference/data-export/overview))

Key vendor constraints beyond rate limits:

- **Conversations API:** No native Chat or Voice history creation; text-only messages at launch; attachments are temporary with 50MB max files and 7-day URLs. ([docs.ada.cx](https://docs.ada.cx/reference/conversations/overview))
- **Actions:** Default limit is five active customer-facing Actions per Agent. ([docs.ada.cx](https://docs.ada.cx/docs/automation/actions/action-control))
- **Variables:** Can store up to 100,000 characters — do not use them as pseudo-custom-object storage. ([docs.ada.cx](https://docs.ada.cx/docs/automation/variables/using-variables))

### API Error Taxonomy

Build your retry logic around these specific error responses:

| HTTP Status | Ada Error | Cause | Recovery Action |
|---|---|---|---|
| `400` | `invalid_request` | Malformed payload, missing required field | Log the specific record ID and error detail; skip and continue. Do not retry without fixing the payload. |
| `400` | `article_too_large` | Article body exceeds 100KB | Split the article into two or more parts; reload. |
| `400` | `article_limit_exceeded` | Instance has reached 50,000 article cap | Delete inactive articles or request limit increase from Ada. |
| `400` | `invalid_language` | Non-BCP 47 language code | Map to valid BCP 47 tag (e.g., `en` → `en-US`). |
| `401` | `unauthorized` | Invalid or expired API token | Regenerate token in Ada dashboard. Fatal — do not retry. |
| `403` | `forbidden` | Token lacks required scope | Verify API token permissions. Fatal — do not retry. |
| `404` | `not_found` | Referenced resource (source, article, user) doesn't exist | Verify resource was created before referencing it. Check ID mapping table. |
| `409` | `conflict` | Duplicate `external_id` or ID collision on upsert | For upserts, this is expected — confirms idempotency. Log and continue. |
| `413` | `payload_too_large` | Request body exceeds 10MB | Reduce batch size (halve and retry). |
| `429` | `rate_limit_exceeded` | Rate limit hit | Read `Retry-After` header; back off for specified duration. Use exponential backoff as fallback. |
| `500` | `internal_server_error` | Ada server error | Retry with exponential backoff (max 5 attempts). If persistent, contact Ada support. |
| `503` | `service_unavailable` | Ada platform maintenance or overload | Retry after 30–60 seconds. Check Ada status page. |

## Step-by-Step Migration Process

### Step 1: Extract Knowledge from Source

```python
import requests
import json
import time

def extract_knowledge_articles(base_url, headers):
    articles = []
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"{base_url}/api/v2/knowledge/articles",
            headers=headers, params=params
        )
        resp.raise_for_status()
        data = resp.json()
        articles.extend(data.get("data", []))
        cursor = data.get("pagination", {}).get("next_cursor")
        if not cursor:
            break
    return articles
```

### Step 2: Extract End Users

```python
def extract_end_users(base_url, headers):
    users = []
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"{base_url}/api/v2/end-users",
            headers=headers, params=params
        )
        resp.raise_for_status()
        data = resp.json()
        users.extend(data.get("data", []))
        cursor = data.get("pagination", {}).get("next_cursor")
        if not cursor:
            break
    return users
```

### Step 3: Extract Conversations for Archival

```python
from datetime import datetime, timedelta

def extract_conversations(base_url, headers, months_back=12):
    all_convos = []
    end_date = datetime.utcnow()
    start_date = end_date - timedelta(days=30 * months_back)
    
    # Iterate in 60-day windows (API constraint)
    window_start = start_date
    while window_start < end_date:
        window_end = min(window_start + timedelta(days=60), end_date)
        cursor = None
        while True:
            params = {
                "start_date": window_start.isoformat() + "Z",
                "end_date": window_end.isoformat() + "Z",
                "limit": 10000
            }
            if cursor:
                params["cursor"] = cursor
            resp = requests.get(
                f"{base_url}/api/v2/export/conversations",
                headers=headers, params=params
            )
            if resp.status_code == 429:
                wait = int(resp.headers.get("Retry-After", 2 ** 1))
                time.sleep(wait)
                continue
            resp.raise_for_status()
            data = resp.json()
            all_convos.extend(data.get("data", []))
            cursor = data.get("pagination", {}).get("next_cursor")
            if not cursor:
                break
        window_start = window_end
    return all_convos
```

### Step 4: Transform and Deduplicate End Users

```python
def deduplicate_users(users):
    seen = {}
    for user in users:
        email = user.get("profile", {}).get("email")
        if email and email in seen:
            # Merge metadata, keep most recent
            existing = seen[email]
            if user.get("updated_at", "") > existing.get("updated_at", ""):
                seen[email] = user
        else:
            key = email or user.get("end_user_id")
            seen[key] = user
    return list(seen.values())
```

### Step 5: Load Knowledge into Target

```python
def load_articles(base_url, headers, articles, source_id):
    # Batch in chunks that stay under 10MB
    batch_size = 100
    for i in range(0, len(articles), batch_size):
        batch = articles[i:i + batch_size]
        payload = {
            "articles": [
                {
                    "id": a["id"],
                    "title": a["title"],
                    "body": a["body"],
                    "language": a.get("language", "en"),
                    "source_id": source_id,
                    "status": a.get("status", "active")
                }
                for a in batch
            ]
        }
        resp = requests.post(
            f"{base_url}/api/v2/knowledge/bulk/articles/",
            headers=headers, json=payload
        )
        resp.raise_for_status()
        print(f"Loaded articles {i} to {i + len(batch)}")
```

### Step 6: Load Configuration

Recreate custom instructions, update persona, configure webhooks, and set up integrations in the target. Create custom instructions as **inactive first** — activate them only after validation is complete.

### Step 7: Load End Users (Close to Go-Live)

Due to the 24-hour auto-deletion constraint, load end users only when you're ready to start routing traffic to the target instance. Alternatively, accept that users will be recreated organically as they interact with the new bot.

### Step 8: Archive Conversations Externally

Route exported conversations and messages to your data warehouse (BigQuery, Snowflake, Redshift) for compliance and historical reporting. Build queryable tables so your team can still reference historical data even though it won't live inside Ada.

### Step 9: Delta Migration (Catch-Up Pass)

Between the initial extraction and go-live, new records accumulate in the source instance. A delta migration captures those records:

```python
def delta_extract_and_load(source_base, target_base, src_headers, tgt_headers,
                           initial_extraction_timestamp, source_id):
    """
    Extract records created/updated after the initial extraction
    and load them into the target.
    """
    # Delta knowledge articles
    cursor = None
    delta_articles = []
    while True:
        params = {
            "limit": 100,
            "updated_after": initial_extraction_timestamp  # ISO 8601
        }
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(
            f"{source_base}/api/v2/knowledge/articles",
            headers=src_headers, params=params
        )
        resp.raise_for_status()
        data = resp.json()
        delta_articles.extend(data.get("data", []))
        cursor = data.get("pagination", {}).get("next_cursor")
        if not cursor:
            break
    
    if delta_articles:
        load_articles(target_base, tgt_headers, delta_articles, source_id)
        print(f"Delta: loaded {len(delta_articles)} updated articles")
    
    # Delta conversations (archive only — cannot import)
    delta_convos = extract_conversations(
        source_base, src_headers, months_back=1  # Last 30 days covers the gap
    )
    # Filter to only conversations after initial extraction
    delta_convos = [
        c for c in delta_convos
        if c.get("created", "") > initial_extraction_timestamp
    ]
    if delta_convos:
        archive_conversations(delta_convos)  # Route to data warehouse
        print(f"Delta: archived {len(delta_convos)} new conversations")
    
    # Delta end users — only if loading users pre-go-live
    # (subject to 24-hour auto-deletion constraint)
```

**Timing:** Run the delta pass as close to the traffic cutover as possible. For a phased migration, schedule the delta pass during the final cutover window.

### Step 10: Validate

```python
def validate_migration(source_base, target_base, src_headers, tgt_headers):
    # Compare knowledge article counts
    src_articles = extract_knowledge_articles(source_base, src_headers)
    tgt_articles = extract_knowledge_articles(target_base, tgt_headers)
    print(f"Source articles: {len(src_articles)}")
    print(f"Target articles: {len(tgt_articles)}")
    assert len(src_articles) == len(tgt_articles), "Article count mismatch!"
    
    # Spot-check random articles
    import random
    sample = random.sample(src_articles, min(10, len(src_articles)))
    for article in sample:
        target_match = next(
            (a for a in tgt_articles if a["id"] == article["id"]), None
        )
        assert target_match, f"Missing article: {article['id']}"
        assert target_match["title"] == article["title"], "Title mismatch"
```

Use structured logging from day one: source ID, target ID, entity type, batch number, retry count, and final status. If an end user fails to create due to a malformed email, the script should log the specific `end_user_id` and the error message, then continue processing the next batch.

## Edge Cases and Challenges

### Conversation History Cannot Be Imported

The Conversations API's `POST` endpoint creates *new, live* conversations — it is not a historical import tool. There is no way to populate the target instance's conversation view with source instance history. Your options:

1. **Archive to a data warehouse** (BigQuery, Snowflake, Redshift) and build reporting there
2. **Export to CSV/JSON** and store in cloud storage for compliance
3. **Maintain read-only access** to the source instance for a transition period

### End-User 24-Hour Auto-Deletion

End users created via `POST /api/v2/end-users/` that are not linked to a conversation within 24 hours are automatically purged. You must either time end-user creation to coincide with the bot going live on the target instance, or accept that end-user records will be recreated organically as customers interact with the new bot.

### Metavariable Schema Mismatches

If the source and target instances have different metavariable definitions (different keys, types, or naming conventions), metadata won't map cleanly. Audit both schemas before migration and create matching variables in the target instance first. Mismatches here cause silent data loss — your scripts will run without errors, but the bot won't behave correctly because metadata keys that don't match the target's variable schema are silently dropped.

### Playbooks, Coaching, and Actions

These are not API-accessible. They must be manually recreated in the target instance through the Ada dashboard. For complex bots with dozens of playbooks, budget significant time for this — it's the most labor-intensive part of the migration. Document each playbook's trigger conditions, actions, and routing logic from the source before beginning recreation.

### Knowledge Indexing Lag

After bulk-upserting articles, Ada begins indexing within ~30 seconds, but content typically becomes available to AI responses within a few minutes. Do not go live immediately after loading knowledge. Wait for indexing to complete and test the bot's responses against known questions before cutting over.

### Duplicate End Users Across Instances

In M&A scenarios, the same customer may exist in both Ada instances. Use email as the primary deduplication key and `external_id` for deterministic matching. Phone number can serve as a secondary key. Without deduplication, you'll have fragmented user profiles in the target.

### File Attachments

The Data Export API returns message content but does not provide direct download URLs for file attachments shared during conversations. If attachment preservation is required, coordinate with Ada's support team. For the Conversations API, uploads are for live flows only — attachment URLs are temporary with a 50MB max file size and 7-day expiry. ([docs.ada.cx](https://docs.ada.cx/generative/reference/conversations/upload-attachment))

### Language Support Discrepancy

As of April 2025, Ada's release notes state that native generation and knowledge ingestion cover all 60 supported languages, but the Knowledge API overview still lists nine supported knowledge-content languages. Validate language behavior in your specific tenant before a bulk import — test article creation and bot responses in each required language.

### Zendesk Messaging Handoff

During a multi-Agent chat rollout, Zendesk Messaging handoff can only be used by one AI Agent at a time. Plan your cutover sequence accordingly if your bot uses Zendesk handoff. ([docs.ada.cx](https://docs.ada.cx/generative/docs/channels/chat/chat-configuration/launch-options))

### Multi-Instance M&A Consolidation (3+ Instances)

When consolidating three or more Ada instances into one target, additional architectural concerns emerge:

- **End-user collision rates increase:** The same customer may exist across multiple source instances. Run cross-source deduplication before loading into the target — not just within each source.
- **Knowledge article conflicts:** Multiple sources may have articles covering the same topic with different content. Designate a "primary" source per topic and flag duplicates for manual review.
- **Variable schema union:** Each source may define different variables. Build a union schema in the target that accommodates all sources, and create a per-source metadata mapping table.
- **Sequential loading order matters:** Load knowledge from all sources first, validate, then load end users from all sources (deduplicated), then cut over traffic one source instance at a time using staged rollout percentages.

## Limitations and Constraints Summary

| Constraint | Impact |
|---|---|
| 12-month historical data access (Data Export API) | Data older than 12 months is not retrievable via API |
| No conversation import endpoint | Historical conversations cannot be migrated into the target instance |
| 60-day query window (Data Export) | Must paginate through time windows; minimum 7 queries for full year |
| 10,000 records per page (Data Export) | High-volume extractions require many paginated requests |
| 60,000 requests/day (End Users + Conversations APIs) | 500K end users = ~9 days minimum |
| 50,000 article limit (Knowledge API) | Knowledge base size is capped (higher limits on some plans) |
| 100KB per article | Large articles must be split |
| 10MB per request payload | Constrains bulk upsert batch sizes |
| 24-hour end-user auto-deletion | End users without conversations are purged |
| Playbooks/Coaching not API-accessible | Must be manually recreated |
| New IDs generated in target | Source IDs for end users are not preserved |
| Variables API is read-only | Must recreate variables manually or via dashboard |
| 5 active customer-facing Actions per Agent (default) | May need to request limit increase |
| Glossary import is full-replace | Cannot merge; must upload complete glossary |

## Validation and Testing

### Record Count Comparison

After migration, compare counts across all migrated object types:

- Knowledge sources: source count vs. target count
- Knowledge articles (by source): source count vs. target count, broken down by status
- Knowledge tags: source count vs. target count
- End users: source count vs. target count (accounting for deduplication)
- Custom instructions: source count vs. target count

### Field-Level Validation

For each object type, sample 5–10% of records and compare field values between source and target. Pay attention to:

- Article body content (check for truncation at the 100KB limit)
- End-user metadata key-value pairs
- Language codes (ensure BCP 47 format consistency)
- Tag associations on articles
- `external_id` mapping on end users

### Bot Response Testing (UAT)

Record counts aren't enough — the real test is whether the bot works correctly:

1. Prepare 20–30 test queries that cover your most common intents
2. Run each query against both source and target bots
3. Compare resolution quality, article citations, and response accuracy
4. Test edge cases: multilingual queries, handoff triggers, action execution
5. Verify webhooks fire correctly to downstream systems

### Rollback Plan

Since the target is a separate Ada instance, rollback is straightforward: keep the source instance active and pointing at production until the target is fully validated. If validation fails, continue using the source. Use Ada's staged chat rollout controls to shift traffic percentages — roll back immediately if UAT fails. This is one advantage of Ada-to-Ada migrations over cross-platform moves.

## Post-Migration Tasks

### Rebuild Playbooks and Coaching

Manually recreate all playbooks, coaching data, and simulations in the target instance. Use screenshots or documentation of the source instance's configuration as reference. Start this work early, in parallel with the API migration — it's not automatable and takes the most hands-on time.

### Re-authenticate Integrations

Even if you recreate integration configurations via API, most external connections (Zendesk, Salesforce, Genesys, etc.) require re-authentication in the target instance. Coordinate with the teams that own each integrated platform.

### Update Embed Scripts and SDKs

If the bot is deployed via Ada's Chat SDK (Embed2), update the script on your website and apps to point to the target instance's subdomain. This is the actual "cutover" moment — all other migration steps are preparation.

### Train Your Team

If you're migrating from scripted to generative Ada, your bot managers will work differently. A team used to Answers and request blocks needs training on knowledge, playbooks, custom instructions, and persona as the primary levers.

### Monitor and Stabilize

For the first 1–2 weeks after cutover:

- Watch CSAT scores for drops compared to the source instance
- Monitor conversation resolution rates
- Check for missing knowledge responses (indicates indexing or article mapping issues)
- Verify webhook events are firing correctly to downstream systems
- Watch for spikes in unresolved or misrouted conversations

### Decommission Source Instance

Do not delete the source instance immediately. Keep it in read-only mode for at least 30 days to handle:

- Late-arriving compliance requests for historical conversation data
- Edge cases discovered after cutover
- Audit trail requirements

## Best Practices

1. **Back up everything before starting.** Export all API-accessible data from the source to your own storage before making any changes.
2. **Run a test migration first.** If possible, use a staging Ada instance as a dry-run target. Validate the full pipeline before touching production.
3. **Migrate knowledge first, validate, then proceed.** Knowledge is the highest-value, lowest-risk migration step.
4. **Respect the 24-hour end-user window.** Time end-user creation carefully relative to your go-live moment.
5. **Build idempotent scripts.** Use the bulk upsert's ID-based deduplication — if you run the knowledge import twice, it updates rather than duplicates.
6. **Create custom instructions inactive first.** Activate only after validation is complete.
7. **Use deterministic IDs wherever the target allows.** Especially for knowledge source IDs, article IDs, and end-user `external_id`.
8. **Automate validation.** Don't rely on manual spot-checks for anything beyond UAT. Script your record-count and field-level comparisons.
9. **Document your metavariable schema.** Align variable definitions between source and target before migration. Mismatches cause silent data loss.
10. **Plan for the Playbook gap.** Start recreating playbooks and coaching early, in parallel with the API migration work.
11. **Keep history and operational data as separate concerns.** Archive conversations to your data warehouse; don't try to force them into Ada's live UI.
12. **Suppress source webhooks during coexistence.** Prevent duplicate events in downstream systems by disabling source webhooks after traffic cuts to the target.
13. **Use middleware only for post-cutover sync.** Zapier and Make work for ongoing light sync — not for bulk historical migration.

## Automation Script Outline

Below is a high-level Python structure for a complete API-based Ada-to-Ada migration:

```python
import requests
import json
import time
import logging
from datetime import datetime, timedelta

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ada_migration")

class AdaMigration:
    def __init__(self, source_url, source_token, target_url, target_token):
        self.source = {"url": source_url, "headers": {"Authorization": f"Bearer {source_token}"}}
        self.target = {"url": target_url, "headers": {"Authorization": f"Bearer {target_token}"}}
        self.extraction_timestamp = None
    
    def run(self):
        """Execute full migration pipeline."""
        # Phase 1: Extract
        logger.info("Phase 1: Extracting from source...")
        self.extraction_timestamp = datetime.utcnow().isoformat() + "Z"
        sources = self.extract_knowledge_sources()
        articles = self.extract_knowledge_articles()
        tags = self.extract_knowledge_tags()
        users = self.extract_end_users()
        instructions = self.extract_custom_instructions()
        persona = self.extract_persona()
        conversations = self.extract_conversations()  # For archival
        
        # Phase 2: Transform
        logger.info("Phase 2: Transforming...")
        users = self.deduplicate_users(users)
        articles = self.validate_article_sizes(articles)
        
        # Phase 3: Load
        logger.info("Phase 3: Loading into target...")
        source_id_map = self.load_knowledge_sources(sources)
        self.load_knowledge_tags(tags)
        self.load_knowledge_articles(articles, source_id_map)
        self.load_custom_instructions(instructions)  # Create inactive
        self.load_persona(persona)
        # End users loaded last, close to go-live
        # self.load_end_users(users)  # Uncomment at go-live
        
        # Phase 4: Archive
        logger.info("Phase 4: Archiving conversations...")
        self.archive_conversations(conversations)
        
        # Phase 5: Delta catch-up
        logger.info("Phase 5: Delta migration...")
        self.delta_migration(source_id_map)
        
        # Phase 6: Validate
        logger.info("Phase 6: Validating...")
        self.validate()
    
    def api_call(self, instance, method, endpoint, **kwargs):
        """Wrapper with retry logic, rate limiting, and structured logging."""
        url = f"{instance['url']}{endpoint}"
        for attempt in range(5):
            resp = getattr(requests, method)(
                url, headers=instance["headers"], **kwargs
            )
            if resp.status_code == 429:
                wait = int(resp.headers.get("Retry-After", 2 ** attempt))
                logger.warning(f"Rate limited on {endpoint}. Waiting {wait}s (attempt {attempt + 1}/5)")
                time.sleep(wait)
                continue
            if resp.status_code >= 500:
                wait = 2 ** attempt
                logger.warning(f"Server error {resp.status_code} on {endpoint}. Retrying in {wait}s")
                time.sleep(wait)
                continue
            if resp.status_code == 400:
                logger.error(f"Bad request on {endpoint}: {resp.text}")
                return None  # Caller handles skip logic
            resp.raise_for_status()
            return resp.json()
        raise Exception(f"Failed after 5 retries: {endpoint}")
    
    def delta_migration(self, source_id_map):
        """Catch-up pass for records created after initial extraction."""
        delta_articles = self.extract_knowledge_articles(
            updated_after=self.extraction_timestamp
        )
        if delta_articles:
            self.load_knowledge_articles(delta_articles, source_id_map)
            logger.info(f"Delta: loaded {len(delta_articles)} updated articles")
    
    # ... implement remaining extract_, load_, and validate methods

if __name__ == "__main__":
    migration = AdaMigration(
        source_url="https://source.ada.support",
        source_token="YOUR_SOURCE_TOKEN",
        target_url="https://target.ada.support",
        target_token="YOUR_TARGET_TOKEN"
    )
    migration.run()
```

Each `extract_*` and `load_*` method follows the pagination and batching patterns shown in the step-by-step section above.

## When to Use a Managed Migration Service

Building an Ada-to-Ada migration pipeline is deceptively complex. The API surface looks manageable — until you hit the edge cases.

Consider external help when:

- Your team doesn't have dedicated backend engineers available for 2–4 weeks
- You're migrating more than 500K conversations and need compliant archival
- You have complex metadata schemas with dozens of metavariables that need remapping
- You're under time pressure (M&A deadlines, contract switchovers)
- You need zero-downtime migration with delta sync to catch records created during the migration window

### Risks of DIY Migration

- **Data loss from API constraints:** The Data Export API only surfaces 12 months of history. If you have older data, you need direct coordination with Ada's team — and coverage isn't guaranteed.
- **End-user duplication:** Without proper deduplication logic, the same customer can end up as multiple end-user records in the target instance.
- **Knowledge indexing delays:** Bulk-upserted articles take minutes to become available. If you flip the bot live before indexing completes, it'll serve incomplete answers.
- **Rate limit throttling:** Hitting the 60,000 req/day limit on the End Users API mid-migration halts progress for 24 hours.
- **Hidden engineering cost:** What looks like a "2-day script" typically balloons to 2–3 weeks once you account for pagination edge cases, retry logic, validation, and rollback planning.

*Disclosure: ClonePartner offers managed Ada-to-Ada migration services. We've run [1,500+ data migrations](https://clonepartner.com/blog/blog/how-we-run-migrations-at-clonepartner/) across platforms including CRMs, help desks, and conversational AI tools. If you'd prefer to hand off the API rate limit management, pagination, error recovery, and validation, [talk to our team](https://clonepartner.com/blog/blog/how-we-run-migrations-at-clonepartner/).*

## What This Comes Down To

An Ada-to-Ada migration is straightforward in concept and tricky in execution. The 1:1 data model means you don't have to worry about schema translation — but Ada's API constraints (12-month history limit, no conversation import, end-user auto-deletion, rate limits, non-API-accessible playbooks) force architectural decisions that determine whether the migration succeeds cleanly or turns into a multi-week firefight.

**Rebuild plus archive plus staged cutover:** knowledge transfers cleanly via bulk upsert (50K articles in under 5 minutes). End users transfer with timing caveats (500K users = ~9 days at rate limit). Conversations can only be archived externally. Configuration migrates with re-authentication requirements. Playbooks require manual work. Plan accordingly.

> Need help migrating between Ada instances? Book a 30-minute call to scope your migration.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can you migrate conversation history between Ada instances?

No. Ada's Conversations API creates new live conversations — it does not accept historical imports. Conversation data can be exported via the Data Export API (last 12 months only) and archived to an external data warehouse, but it cannot be loaded into a new Ada instance.

### What are Ada's API rate limits during migration?

The Data Export API allows 10 requests per second per endpoint, with a maximum of 10,000 records per page, a 60-day date range per query, and access to only the last 12 months of data. The End Users and Conversations APIs have a limit of 60,000 requests per day. The Knowledge API allows 60,000 requests per day with a 10MB max payload and 50,000 article cap per instance.

### How do you migrate an Ada knowledge base to another Ada instance?

Use the Knowledge API to export sources, articles, and tags from the source instance, then bulk upsert them into the target via POST /api/v2/knowledge/bulk/articles/. The upsert is idempotent — running it twice updates existing articles rather than creating duplicates. Wait for indexing to complete (typically under 5 minutes) before testing bot responses.

### What happens to Ada end users created via API without a conversation?

End users created through the API that are not associated with a conversation within 24 hours are automatically deleted. You must time end-user creation close to your go-live moment or accept that users will be recreated as they interact with the new bot.

### How long does an Ada-to-Ada migration take?

A knowledge-only migration can complete in hours. A full migration including end users, configuration, conversation archival, and validation typically takes 3–7 days depending on data volume and the number of playbooks that need manual recreation. API rate limits are the primary throughput constraint.
