---
title: "CSV to Ada Migration: A Technical Guide"
slug: csv-to-ada-migration-a-technical-guide
date: 2026-08-05
author: Wahab
categories: [Migration Guide, Help Desk]
excerpt: "A technical guide to migrating CSV data into Ada. Covers Knowledge API limits, encoding pitfalls, field mapping, transformation scripts, and the critical constraint that conversations can't be imported."
tldr: Ada has no native CSV import. You must transform CSV rows to JSON and push them through Ada's Knowledge API (articles) or End Users API (profiles). Conversations cannot be imported at all.
canonical: https://clonepartner.com/blog/csv-to-ada-migration-a-technical-guide/
---

# CSV to Ada Migration: A Technical Guide


# CSV to Ada Migration: A Technical Guide

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

Migrating data from CSV files into Ada means transforming flat, delimiter-separated records into JSON payloads that Ada's Platform APIs can accept. Ada has no native CSV import feature. Every record — whether it's a knowledge article, an end-user profile, or a tag — must be converted to JSON and pushed through the corresponding REST endpoint. The process is straightforward for small knowledge bases but gets complicated fast when you're dealing with encoding issues, multi-language content, field mapping ambiguity, and Ada's specific API constraints.

This guide covers the full pipeline: CSV preparation, field mapping, transformation scripts, API constraints, and the failure modes you'll hit in production.

## What Data Can You Actually Import into Ada from CSV?

Before you build anything, understand what Ada's APIs accept and what they don't. This is where most CSV-to-Ada migration plans go wrong — teams assume they can load everything from their CSV exports into Ada, and that's not the case.

**What you can import:**

- **Knowledge articles** — via the Knowledge API (`POST /api/v2/knowledge/bulk/articles/`). Full read/write support, including bulk upsert.
- **Knowledge sources** — via the Knowledge API (`POST /api/v2/knowledge/sources/`). Every article must belong to a source.
- **Tags** — via the Knowledge API (`POST /api/v2/knowledge/bulk/tags/`). Labels for organizing articles. See the [Tag Import section](#importing-tags-from-csv) below for the full mapping and code example.
- **End-user profiles** — via the End Users API (`POST /v2/end-users/`). Name, email, language, metadata, and sensitive metadata fields.

**What you cannot import:**

- **Historical conversations and messages.** There is no endpoint that accepts historical conversation records. The Conversations API (`POST /v2/conversations/`) creates *new, live* conversations only — it does not accept historical data. If your CSV contains conversation transcripts, chat logs, or ticket histories, those records cannot be loaded into Ada.
- **Bot configuration, playbooks, or custom instructions from CSV.** These must be configured through the Ada dashboard or their respective single-object API endpoints.
- **File attachments.** Ada's attachment handling is for live conversation flows only, with a 50MB max and 7-day URL expiry.

> [!WARNING]
> The most common mistake in a CSV-to-Ada migration is assuming conversation history can be imported. It cannot. Plan for archival storage (data warehouse, S3, BigQuery) from the start. See our [Ada to Ada migration guide](https://clonepartner.com/blog/blog/ada-to-ada-migration-the-ctos-technical-guide/) for the full conversation constraint breakdown.

## Ada's API Constraints You Need to Know Before Writing Code

Ada's Platform APIs are REST-based with JSON payloads. Every CSV row must be transformed before it can be sent. Here are the hard limits that will shape your migration script:

### Authentication

All Ada Platform API requests require a Bearer token in the `Authorization` header. To obtain a Platform API token:

1. Log in to your Ada dashboard.
2. Navigate to **Settings → Integrations → Platform API**.
3. Generate a new token. Store it securely — it is shown only once.

All requests use this header:

```http
Authorization: Bearer YOUR_PLATFORM_API_TOKEN
Content-Type: application/json
```

A `401 Unauthorized` response means the token is missing or malformed. A `403 Forbidden` response means the token is valid but lacks permission for the requested endpoint — verify the token's scope in your Ada dashboard.

### Knowledge API Limits

| Constraint | Limit |
|---|---|
| Requests per day | 60,000 |
| Requests per minute | 1,000 |
| Requests per second | 200 |
| Max articles per instance | 50,000 (default; higher limits by plan) |
| Max article size | 100KB |
| Max request payload | 10MB |
| Supported content languages | 9 (English, Arabic, Chinese, Dutch, French, German, Italian, Portuguese, Spanish) |

### End Users API Limits

| Constraint | Limit |
|---|---|
| Requests per day | 60,000 |
| Requests per minute | 300 |
| Requests per second | 30 |

### Data Export API Limits (for Extraction, Not Import)

| Constraint | Limit |
|---|---|
| Requests per second | 10 |
| Max page size | 10,000 records |
| Max date range per query | 60 days |
| Historical data availability | 12 months |

These numbers matter. If your CSV contains 60,000 end-user records, you'll need at minimum a full day to push them through at the 60,000 req/day cap — and that assumes zero errors or retries. At 200,000 end users, plan for 3–4 days of continuous API calls.

### API Error Code Reference

| HTTP Status | Meaning | Common Cause | Fix |
|---|---|---|---|
| `400 Bad Request` | Malformed payload or constraint violation | Article count exceeds 50,000 cap; field exceeds character limit | Validate payload before sending; check article count |
| `401 Unauthorized` | Missing or invalid auth token | Token not included or expired | Verify `Authorization: Bearer` header; regenerate token |
| `403 Forbidden` | Token lacks required permissions | Token scope doesn't include target endpoint | Check token permissions in Ada dashboard |
| `404 Not Found` | Resource doesn't exist | Referencing a `source_id` that hasn't been created | Create the source before upserting articles |
| `422 Unprocessable Entity` | Valid JSON but invalid field values | Invalid language code; `id` exceeds 160 characters | Validate field values before sending |
| `429 Too Many Requests` | Rate limit exceeded | Exceeded 60,000/day, 1,000/min, or 200/sec | Respect `Retry-After` header; implement exponential backoff |
| `5xx Server Error` | Ada-side failure | Transient infrastructure issue | Retry with exponential backoff; log for audit |

## CSV Preparation: Encoding, Delimiters, and the Problems Nobody Warns You About

**CSV is a plain-text format with no built-in encoding declaration.** Unlike JSON (which assumes UTF-8) or XML (which has an encoding header), a CSV file carries zero metadata about how its bytes should be interpreted. This is the single biggest source of silent data corruption in CSV-based migrations.

### Encoding Traps

- **Excel's default CSV export uses Windows-1252**, not UTF-8. Accented characters (é, ü, ñ), currency symbols (€, £), and non-Latin scripts will break when your Python script reads them as UTF-8.
- **Excel's "CSV UTF-8" export adds a BOM** (Byte Order Mark) — three invisible bytes (`EF BB BF`) at the start of the file. Ada's API expects clean JSON, and if those BOM bytes leak into your first field value, the API will reject the payload or — worse — silently store corrupted data.
- **European locale CSVs use semicolons** as delimiters because commas serve as decimal separators. If your source data was exported from a European-locale Excel instance, your parser will treat the entire row as a single column.

### How to Fix It Before You Start

```bash
# Detect encoding
file -bi yourfile.csv

# Convert to UTF-8 without BOM
iconv -f WINDOWS-1252 -t UTF-8 input.csv > output.csv

# Strip BOM if present
awk '{if(NR==1)sub(/^\xef\xbb\xbf/,"");print}' output.csv > clean.csv
```

In Python, use `encoding='utf-8-sig'` when reading to automatically strip the BOM:

```python
import csv

with open('input.csv', 'r', encoding='utf-8-sig') as f:
    reader = csv.DictReader(f)
    for row in reader:
        # row keys are now clean, BOM-free
        pass
```

> [!TIP]
> Always validate encoding *before* you run your transformation. Open a hex editor or run `xxd input.csv | head` to check the first bytes. If you see `ef bb bf`, you have a BOM. If you see characters above `7f` that don't form valid UTF-8 sequences, you have a legacy encoding.

For a deeper look at when CSVs work and when they fall apart in migrations, see our post on [using CSVs for SaaS data migrations](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

## Mapping CSV Columns to Ada's Data Model

Ada's Knowledge API expects a specific JSON structure. Your CSV columns won't map 1:1 — you'll need a transformation layer.

### Step 1: Create a Knowledge Source

Every article must belong to a source. Create the source *before* attempting any article upsert. If you reference a `source_id` that doesn't exist, the API returns `404`.

```python
import requests

ADA_BASE_URL = "https://yourinstance.ada.support/api/v2"
ADA_TOKEN = "your-platform-token"

headers = {
    "Authorization": f"Bearer {ADA_TOKEN}",
    "Content-Type": "application/json"
}

source_payload = {
    "id": "src-csv-import",
    "name": "CSV Import - Help Center",
    "description": "Knowledge articles imported from CSV export"
}

resp = requests.post(
    f"{ADA_BASE_URL}/knowledge/sources/",
    headers=headers,
    json=source_payload
)

if resp.status_code == 201:
    print(f"Source created: {resp.json()['id']}")
elif resp.status_code == 409:
    print("Source already exists — safe to proceed with article upsert")
else:
    raise Exception(f"Source creation failed: {resp.status_code} {resp.text}")
```

### Step 2: Knowledge Article Mapping

A typical CSV for knowledge articles might look like:

```
id,title,body,category,language,url
kb-001,How to reset password,"Click Settings > Security > Reset...",account,en,https://help.example.com/reset
kb-002,Billing FAQ,"We accept Visa, Mastercard...",billing,en,https://help.example.com/billing
```

Ada's bulk upsert endpoint expects this JSON structure:

```json
{
  "source_id": "src-csv-import",
  "articles": [
    {
      "id": "kb-001",
      "name": "How to reset password",
      "content": "Click Settings > Security > Reset...",
      "language": "en",
      "metadata": {
        "category": "account",
        "source_url": "https://help.example.com/reset"
      }
    }
  ]
}
```

Key mapping decisions:

- **`id`** must be unique and <= 160 characters. If your CSV uses numeric IDs, prefix them (e.g., `kb-001`) to avoid collisions with other sources.
- **`name`** maps to your title column. Max 255 characters.
- **`content`** is the article body. Max 100KB per article. If your CSV has articles exceeding this, you must split them before import.
- **`language`** must be one of the 9 supported languages. Articles in unsupported languages (e.g., Japanese, Korean, Polish) will be accepted by the API but will not generate accurate generative answers — Ada applies machine translation for unsupported languages, and quality varies significantly.
- **Every article must belong to a `source_id`.** Create the source first (see Step 1), then reference it in your bulk upsert.

### Step 3: End-User Mapping

A CSV of end users might look like:

```
email,name,language,plan_type,account_id,user_token
jane@example.com,Jane Smith,en,enterprise,acct-1234,token-abc
```

The End Users API expects:

```json
{
  "email": "jane@example.com",
  "name": "Jane Smith",
  "language": "en",
  "metadata": {
    "plan_type": "enterprise",
    "account_id": "acct-1234"
  },
  "sensitive_metadata": {
    "user_token": "token-abc"
  }
}
```

**`metadata` vs. `sensitive_metadata`:** Both accept arbitrary key-value pairs, but they differ in how Ada handles them:

- **`metadata`** is stored and accessible in the Ada dashboard, in conversation transcripts, and via API responses. Use it for non-sensitive CRM or account attributes (plan type, account tier, locale).
- **`sensitive_metadata`** is stored encrypted and is not surfaced in the Ada dashboard or conversation views. Use it for fields that should not be visible to Ada dashboard users — authentication tokens, PII, API keys, or any field your security policy requires to be restricted. The values are available to Generative Actions at runtime but are redacted in logs.

> [!CAUTION]
> **Critical timing constraint:** End users created via the API that are not associated with a conversation within 24 hours are automatically deleted by Ada. If you're pre-loading end-user profiles before go-live, time your import accordingly — or plan to re-import immediately before cutover.

## The Transformation Pipeline: CSV → JSON → Ada API

Here's a practical Python script skeleton for transforming CSV knowledge articles and pushing them to Ada's Knowledge API:

```python
import csv
import json
import requests
import time

ADA_BASE_URL = "https://yourinstance.ada.support/api/v2"
ADA_TOKEN = "your-platform-token"
SOURCE_ID = "src-csv-import"
BATCH_SIZE = 50  # articles per request; keep well under 10MB

def read_csv(filepath):
    articles = []
    with open(filepath, 'r', encoding='utf-8-sig') as f:
        reader = csv.DictReader(f)
        for row in reader:
            article = {
                "id": row["id"].strip(),
                "name": row["title"].strip()[:255],
                "content": row["body"].strip(),
                "language": row.get("language", "en").strip(),
                "metadata": {
                    k: v for k, v in row.items()
                    if k not in ("id", "title", "body", "language")
                }
            }
            # Validate article size
            if len(json.dumps(article).encode('utf-8')) > 102400:
                print(f"WARNING: Article {article['id']} exceeds 100KB — skipping")
                continue
            articles.append(article)
    return articles

def bulk_upsert(articles, source_id):
    headers = {
        "Authorization": f"Bearer {ADA_TOKEN}",
        "Content-Type": "application/json"
    }
    for i in range(0, len(articles), BATCH_SIZE):
        batch = articles[i:i + BATCH_SIZE]
        payload = {
            "source_id": source_id,
            "articles": batch
        }
        # Check payload size
        payload_bytes = len(json.dumps(payload).encode('utf-8'))
        if payload_bytes > 10_000_000:
            print(f"Payload exceeds 10MB at batch {i}. Reduce BATCH_SIZE.")
            continue

        resp = requests.put(
            f"{ADA_BASE_URL}/knowledge/bulk/articles/",
            headers=headers,
            json=payload
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            print(f"Rate limited. Waiting {retry_after}s...")
            time.sleep(retry_after)
            # Retry the same batch
            resp = requests.put(
                f"{ADA_BASE_URL}/knowledge/bulk/articles/",
                headers=headers,
                json=payload
            )
        if resp.status_code not in (200, 201):
            print(f"Error at batch {i}: {resp.status_code} {resp.text}")
        else:
            print(f"Upserted batch {i}–{i+len(batch)}")
        time.sleep(0.01)  # Stay under 200 req/sec

articles = read_csv("knowledge_articles.csv")
bulk_upsert(articles, SOURCE_ID)
```

This script handles the core path. In production, add:

- **Exponential backoff with jitter** for 429 responses (not just a flat `Retry-After`).
- **Idempotency:** Ada's bulk upsert is idempotent on the `id` field — re-running the script will update existing articles rather than create duplicates. This makes it safe to retry failed runs in full.
- **A dry-run mode** that validates all rows without sending API calls.
- **Logging** of every API response (status code, response body, batch index) for post-migration audit.

## Importing Tags from CSV

Tags are listed in Ada's Knowledge API under `POST /api/v2/knowledge/bulk/tags/`. The CSV-to-tag mapping is simpler than articles but follows the same bulk structure.

A typical tags CSV:

```
tag_id,tag_name,tag_color
tag-001,billing,blue
tag-002,account,green
tag-003,technical,red
```

Ada's bulk tag upsert payload:

```json
{
  "tags": [
    { "id": "tag-001", "name": "billing", "color": "blue" },
    { "id": "tag-002", "name": "account", "color": "green" },
    { "id": "tag-003", "name": "technical", "color": "red" }
  ]
}
```

Example tag import script:

```python
def import_tags(filepath):
    tags = []
    with open(filepath, 'r', encoding='utf-8-sig') as f:
        reader = csv.DictReader(f)
        for row in reader:
            tags.append({
                "id": row["tag_id"].strip(),
                "name": row["tag_name"].strip(),
                "color": row.get("tag_color", "").strip() or None
            })

    headers = {
        "Authorization": f"Bearer {ADA_TOKEN}",
        "Content-Type": "application/json"
    }
    resp = requests.post(
        f"{ADA_BASE_URL}/knowledge/bulk/tags/",
        headers=headers,
        json={"tags": tags}
    )
    if resp.status_code not in (200, 201):
        print(f"Tag import failed: {resp.status_code} {resp.text}")
    else:
        print(f"Imported {len(tags)} tags")

import_tags("tags.csv")
```

Tags must be imported before articles if you plan to associate tags with articles at upsert time. If you're adding tags to existing articles post-import, the article update endpoint (`PUT /api/v2/knowledge/articles/:id`) accepts a `tag_ids` array.

## Handling Knowledge Article Content from CSV

CSV is a poor format for rich text. If your knowledge base articles contain HTML, Markdown, or structured formatting, you'll hit these issues:

**Commas and newlines inside content fields:** A knowledge article body will almost certainly contain commas and line breaks. Your CSV must use proper quoting (double-quote delimiters with escaped inner quotes). Most CSV libraries handle this correctly, but manually edited CSVs often don't.

**HTML entities:** If your source system exported HTML content, it may contain entities like `&amp;`, `&lt;`, `&#x27;`. Decide whether to pass these through as-is (Ada will store them) or decode them before import.

**Content size:** Ada enforces a 100KB per-article limit. A single long FAQ document exported as one CSV row could exceed this. Check sizes programmatically and split oversized articles *before* attempting the upsert.

**Content language:** Ada's Knowledge API supports articles in 9 languages: English, Arabic, Chinese, Dutch, French, German, Italian, Portuguese, and Spanish. If your CSV includes articles in other languages, they'll be accepted by the API but may not generate accurate responses — Ada uses machine translation for unsupported languages, and quality varies.

## Migrating End Users from CSV to Ada

End-user imports from CSV are conceptually simple but operationally tricky because of the **24-hour auto-deletion rule**.

The workflow:

1. Parse your CSV, mapping each row to Ada's end-user schema (`email`, `name`, `language`, `metadata`, `sensitive_metadata`).
2. Call `POST /v2/end-users/` for each user. There is no bulk endpoint — it's one request per user.
3. Respect the 300 req/min and 60,000 req/day limits.

**Time estimate:** 60,000 end users = 1 full day at max throughput. 200,000 end users = 3–4 days minimum. 500,000+ end users = over a week of continuous API calls.

The 24-hour auto-deletion constraint means you should **not** pre-load end users weeks before go-live. Time your end-user import to complete within 24 hours of when those users will start conversations. If you're using Ada's embed or SDK, end users get created automatically on first interaction — making a bulk pre-load unnecessary for many use cases.

### Paginating GET Requests During Validation

After importing end users, validate by pulling samples via `GET /v2/end-users/`. This endpoint is paginated. Use `limit` and `offset` (or `cursor`-based pagination if supported by your Ada version) to walk through large result sets:

```python
def fetch_end_users(limit=100):
    headers = {"Authorization": f"Bearer {ADA_TOKEN}"}
    offset = 0
    all_users = []

    while True:
        resp = requests.get(
            f"{ADA_BASE_URL.replace('/api/v2', '')}/v2/end-users/",
            headers=headers,
            params={"limit": limit, "offset": offset}
        )
        data = resp.json()
        users = data.get("results", [])
        all_users.extend(users)
        if not data.get("next"):
            break
        offset += limit
        time.sleep(0.1)  # Respect rate limits on GET requests

    return all_users
```

Compare the returned count against your CSV row count (minus known skips) to confirm completeness.

## What About Conversation Data in Your CSV?

If you have conversation transcripts, chat logs, or ticket exports in CSV format, here's the definitive answer: **you cannot import them into Ada.**

Ada's Conversations API is designed for creating and managing live conversations. There is no historical import endpoint. The typical approach:

1. **Archive to a data warehouse.** Transform the CSV into a structured format (Parquet, BigQuery table, PostgreSQL) and keep it accessible for compliance and reference.
2. **Build a lookup integration.** If agents need to reference historical conversations during live Ada interactions, build a Generative Action that queries your archive and surfaces relevant history.
3. **Accept the break.** For reporting, treat the migration date as day zero. Historical metrics stay in the old system; new metrics start in Ada.

For a deeper walkthrough of conversation archival strategies, see our [help desk data mapping guide](https://clonepartner.com/blog/blog/your-ultimate-guide-to-data-mapping-for-a-flawless-help-desk-data-migration-with-csv-templates/).

## Validation Checklist: Before and After the Import

If you are migrating a public-facing help center, you also need to plan for URL redirects and SEO preservation. For a complete look at those requirements, see our [knowledge base migration checklist](https://clonepartner.com/blog/blog/the-ultimate-knowledge-base-migration-checklist-a-zero-downtime-plan/).

### Pre-Import Validation

- [ ] **Encoding confirmed as UTF-8** (no BOM, no Windows-1252 artifacts)
- [ ] **Delimiter verified** (comma vs. semicolon vs. tab)
- [ ] **No article exceeds 100KB** when serialized to JSON
- [ ] **All article IDs are unique** and <= 160 characters
- [ ] **All article names are <= 255 characters**
- [ ] **Every article has a valid `language` value** from the 9 supported languages
- [ ] **Knowledge source created in Ada** before bulk upsert (`POST /api/v2/knowledge/sources/`)
- [ ] **Tags imported** before articles if tag associations are needed at upsert time
- [ ] **Total article count is under 50,000** (or your plan's custom limit)
- [ ] **End-user email fields are valid** and deduplicated
- [ ] **`sensitive_metadata` fields identified** and separated from standard `metadata`
- [ ] **No conversation data in the import set** (it will fail silently or be misinterpreted)

### Post-Import Validation

- [ ] **Article count in Ada matches CSV row count** (minus skipped/errored rows)
- [ ] **Spot-check 5–10 articles** in Ada's dashboard for content accuracy, encoding, and formatting
- [ ] **Test generative answers** — ask the AI agent questions that should be answered by imported articles. Allow 5 minutes for indexing after the final upsert.
- [ ] **Verify end-user profiles** — pull a paginated sample via `GET /v2/end-users/` and compare against CSV source
- [ ] **Verify tags** — pull tag list via `GET /api/v2/knowledge/tags/` and compare against tags CSV
- [ ] **Check rate limit usage** in your logs to confirm no requests were silently dropped

## Common Failure Modes

| Failure | Cause | Fix |
|---|---|---|
| `400` on bulk upsert | Article limit exceeded (50,000 cap) | Check current article count; delete unused articles or request limit increase |
| `401 Unauthorized` | Missing or malformed auth token | Verify `Authorization: Bearer` header; regenerate token in Ada dashboard |
| `403 Forbidden` | Token lacks endpoint permissions | Check token scope in Ada dashboard Settings → Integrations |
| `404 Not Found` on article upsert | `source_id` doesn't exist | Create source via `POST /api/v2/knowledge/sources/` before upserting articles |
| `422 Unprocessable Entity` | Invalid field value | Check `language` is one of 9 supported codes; check `id` <= 160 chars |
| Garbled characters in article content | CSV encoding mismatch (Windows-1252 read as UTF-8) | Convert to UTF-8 before processing; use `encoding='utf-8-sig'` in Python |
| First article's `id` field has invisible prefix | BOM bytes prepended to first CSV cell | Strip BOM during preprocessing |
| End users disappearing after import | 24-hour auto-deletion for users without conversations | Time import closer to go-live; or rely on auto-creation via embed |
| `429 Too Many Requests` mid-import | Exceeded 60,000/day or 300/min rate limit | Implement backoff with jitter; spread import across multiple days |
| Articles not appearing in AI responses | Indexing latency (up to 5 minutes) | Wait at least 5 minutes after final upsert before testing |
| Entire row imported as single field | Semicolon delimiter from European CSV export | Explicitly set delimiter in your CSV reader: `csv.DictReader(f, delimiter=';')` |
| Tags not associated with articles | Tags imported after articles, or `tag_ids` not included in upsert payload | Import tags first; include `tag_ids` array in article upsert or update articles post-import |

## When CSV Is the Wrong Starting Point

CSV works for loading knowledge articles and end-user profiles into Ada — but it's the wrong tool in several scenarios:

- **You need to preserve relationships** between conversations, messages, and end users. CSVs flatten relational data. Use Ada's Data Export API (JSON) as your source instead.
- **Your data contains rich formatting, embedded images, or attachments.** CSV strips all of this. If your knowledge base has inline screenshots or downloadable PDFs, you need a different pipeline.
- **You're migrating from another platform with an API.** Pulling data via API and pushing to Ada's API directly (JSON-to-JSON) avoids the entire class of CSV encoding and parsing issues. It's more work to set up but dramatically more reliable at scale.
- **Your dataset exceeds 50,000 articles.** You'll need to coordinate with Ada's team for a higher limit, and at that volume, API-to-API is almost always faster and more reliable than CSV intermediation.

For a comparison of CSV vs. API vs. JSON export approaches, see [Using CSVs for SaaS Data Migrations](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

## Keeping Data in Sync After the Initial Import

A migration isn't done when the initial load completes. If your knowledge base is maintained in an external system (a CMS, a wiki, a shared drive of CSVs), you need an ongoing sync strategy.

Ada's Knowledge API supports full upsert semantics — if an article with the same `id` already exists, it gets updated. This means you can re-run your CSV-to-Ada pipeline on a schedule to pick up changes. But there are caveats:

**Deleted articles aren't handled by upsert.** If you remove a row from your CSV, the corresponding Ada article remains. You need an explicit deletion step that diffs your CSV against Ada's article list and calls `DELETE /api/v2/knowledge/articles/:id` for removed articles. Here's a working implementation:

```python
def sync_deletions(csv_filepath, source_id):
    """Remove Ada articles that no longer exist in the CSV."""
    # Build set of IDs from current CSV
    csv_ids = set()
    with open(csv_filepath, 'r', encoding='utf-8-sig') as f:
        reader = csv.DictReader(f)
        for row in reader:
            csv_ids.add(row["id"].strip())

    # Fetch all article IDs currently in Ada for this source
    headers = {"Authorization": f"Bearer {ADA_TOKEN}"}
    ada_ids = set()
    offset = 0
    limit = 1000

    while True:
        resp = requests.get(
            f"{ADA_BASE_URL}/knowledge/articles/",
            headers=headers,
            params={"source_id": source_id, "limit": limit, "offset": offset}
        )
        data = resp.json()
        articles = data.get("results", [])
        for article in articles:
            ada_ids.add(article["id"])
        if not data.get("next"):
            break
        offset += limit
        time.sleep(0.05)

    # Delete articles present in Ada but absent from CSV
    to_delete = ada_ids - csv_ids
    print(f"Articles to delete: {len(to_delete)}")

    for article_id in to_delete:
        resp = requests.delete(
            f"{ADA_BASE_URL}/knowledge/articles/{article_id}/",
            headers=headers
        )
        if resp.status_code == 204:
            print(f"Deleted: {article_id}")
        else:
            print(f"Failed to delete {article_id}: {resp.status_code} {resp.text}")
        time.sleep(0.01)  # Stay under rate limits

sync_deletions("knowledge_articles.csv", SOURCE_ID)
```

Additional sync caveats:

- **Indexing latency applies to every update.** Bulk-updated articles take up to 5 minutes to reflect in generative answers. If you're updating frequently, stagger updates to avoid serving stale content.
- **Consider Ada's native integrations first.** Ada has out-of-the-box integrations with Zendesk Guide, Salesforce Knowledge, Freshworks, Contentful, and others that handle sync automatically. If your knowledge lives in one of those systems, a direct integration is almost always preferable to a CSV pipeline.

## Planning a Zero-Downtime Cutover

If you're migrating to Ada while keeping an existing support system running, the knowledge import from CSV can happen entirely in the background — it doesn't affect live operations. End-user imports need more careful timing due to the 24-hour auto-deletion rule.

The real downtime risk comes from the *cutover*: the moment you switch live traffic to Ada. Plan for a brief parallel-run period where both systems are active. For detailed strategies on managing this, see our guide on [zero-downtime help desk migration](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/).

## When to Seek Additional Help

A CSV-to-Ada knowledge import with under 5,000 articles and clean, well-encoded data is a reasonable DIY project for a team with Python experience. Budget a day for scripting, a day for testing, and a half-day for validation.

The complexity increases significantly when:

- Your CSV has encoding problems or inconsistent formatting from multiple export sources — each source may require a separate transformation layer
- You're importing both knowledge articles and end users and need coordinated timing to avoid the 24-hour auto-deletion window
- Your article count approaches or exceeds the 50,000 cap and requires coordination with Ada's team
- You need an ongoing sync pipeline (not just a one-time load), including the deletion-diffing logic
- You have conversation data that needs archival alongside the Ada migration, requiring a separate data warehouse pipeline

ClonePartner has handled 1,500+ migrations including complex CSV-to-platform data loads with encoding normalization, field mapping, and API orchestration. If the scope is bigger than a weekend project, we can get it done in days.

> Need help with your CSV to Ada migration? Our engineers handle the encoding headaches, API rate limits, and data mapping so you don't have to. Book a free 30-minute migration consultation.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I import CSV data directly into Ada?

No. Ada has no native CSV import feature. You must convert CSV rows to JSON and use Ada's REST APIs — the Knowledge API for articles and the End Users API for user profiles. Each API call uses JSON payloads, not CSV.

### Can I import conversation history into Ada from a CSV?

No. Ada's Conversations API only creates new, live conversations. There is no endpoint that accepts historical conversation data. Conversation transcripts from CSVs must be archived externally (e.g., in a data warehouse).

### What are Ada's Knowledge API rate limits for bulk imports?

Ada's Knowledge API allows 60,000 requests per day, 1,000 per minute, and 200 per second. The max request payload is 10MB, each article can be up to 100KB, and the default article cap is 50,000 per instance.

### How do I handle CSV encoding issues when migrating to Ada?

Convert your CSV to UTF-8 without BOM before processing. Excel's default export uses Windows-1252, which corrupts accented characters. In Python, use encoding='utf-8-sig' to auto-strip the BOM. Verify encoding with `file -bi yourfile.csv` on the command line.

### How long does a CSV to Ada end-user migration take?

Ada's End Users API allows 60,000 requests per day with no bulk endpoint. Each user requires one API call. So 60,000 users take about 1 day, 200,000 users take 3-4 days, and 500,000+ users take over a week at continuous throughput.
