---
title: "Dixa to Crisp Migration: The Complete Technical Guide"
slug: dixa-to-crisp-migration-the-complete-technical-guide
date: 2026-07-22
author: Rishabh
categories: [Migration Guide, Dixa]
excerpt: "A technical guide to migrating from Dixa to Crisp: API extraction, data mapping, custom attribute translation, rate limits, and step-by-step implementation."
tldr: "Dixa to Crisp migration is moderate complexity. Custom attribute translation, 31-day API export windows, and no native Dixa adapter in Crisp's import tool are the main challenges. Budget 2–4 weeks."
canonical: https://clonepartner.com/blog/dixa-to-crisp-migration-the-complete-technical-guide/
---

# Dixa to Crisp Migration: The Complete Technical Guide


## What Is a Dixa to Crisp Migration?

A **Dixa to Crisp migration** is the process of extracting conversations, messages, end users (contacts), tags, custom attributes, internal notes, CSAT ratings, and attachments from Dixa and loading them into Crisp's conversation and people data model. The goal is to preserve complete customer interaction history so agents in Crisp can see every previous exchange without context gaps.

The real question teams ask is rarely "How do I export CSVs?" It's "How do I keep enough operational history that my agents can work in Crisp on day one without losing context?"

### Why Teams Migrate from Dixa to Crisp

Teams switch from Dixa to Crisp for specific, measurable reasons:

1. **Cost structure.** Dixa starts at €89/user/month (Growth plan) with a minimum of 7 seats, and API access is only available on Ultimate (€139) or Prime (€179). Crisp uses flat-rate workspace pricing — the Plus plan at $295/month includes unlimited conversations and up to 20 agents. Worked example: a 15-agent team on Dixa Ultimate pays 15 × €139 = €2,085/month (≈$2,290). The same team on Crisp Plus pays $295/month — an 87% reduction in platform cost.
2. **Simplicity.** Crisp is a lighter platform: live chat, shared inbox, knowledge base, CRM contacts, campaigns, and an AI agent in a single workspace. Teams that don't need Dixa's native telephony, IVR, and advanced routing often find Crisp's simpler model faster to operate.
3. **Marketing automation.** Crisp includes campaign features (email, chat, in-app) natively, with targeting based on segments, events, and custom data. Dixa has no native outbound marketing layer.
4. **Agency and multi-brand use cases.** Crisp's per-workspace pricing makes it practical to run multiple brand workspaces without per-seat multiplication.

### Core Architectural Differences

Three architectural gaps drive the complexity of this migration:

| Dimension | Dixa | Crisp |
|---|---|---|
| **Routing model** | Offer-based push routing via Flow Builder | Inbox-based with Workflow automation |
| **Contact model** | End Users with typed custom attributes (text, dropdown, integer) | People profiles with freeform key-value custom data |
| **Channel architecture** | Native phone, email, chat, social in one seat | Chat, email, social via plugins; phone via third-party integrations (Ringover, Aircall) |
| **Custom attributes** | Schema-defined per entity type (conversation, end user) | Freeform `data` objects on sessions and contacts |
| **Tags** | Tags (string labels on conversations) | Segments (labels on conversations and contacts) |
| **Knowledge base** | Dixa Knowledge (categories, articles) | Crisp Helpdesk (categories, articles, markdown-based) |
| **Conversation threading** | Linked conversations via `link_type` and `linked_to` fields (parent/child, merged) | No native linked-conversation concept; threading is sequential within a single session |

The biggest data model gap: Dixa exports conversation records with queue, assignee, ratings, wrap-up notes, linked-conversation metadata, and telephony fields. Crisp's model centers on **people profiles**, **company info**, **custom data**, **conversations**, **messages**, and **inboxes**. Dixa's linked-conversation relationships (parent/child, merged) have no structural equivalent in Crisp — you must flatten these into notes or custom data fields that preserve the audit trail without the relational link. If you have CRM-style objects like accounts, leads, or opportunities, those should stay in your CRM (HubSpot, Salesforce, etc.) and sync into Crisp as enrichment — not be forced into the helpdesk.

For related Dixa migration guides, see [How to Export Data from Dixa: Methods, API Limits & Data Mapping](https://clonepartner.com/blog/blog/how-to-export-data-from-dixa-methods-api-limits-data-mapping/) and [Dixa to Freshdesk Migration: The Complete Technical Guide](https://clonepartner.com/blog/blog/dixa-to-freshdesk-migration-the-complete-technical-guide/).

## Migration Approaches: Which Method Fits Your Team

### 1. Crisp's Official Import Tool (Custom Adapter)

**How it works:** Crisp maintains an open-source Node.js tool called [`crisp-import-conversations`](https://github.com/crisp-im/crisp-import-conversations) that reads exported conversation files (JSON, JSONL, CSV, XML) and writes them into Crisp via API. It ships with adapters for Zendesk, Gorgias, Help Scout, Tidio, GrooveHQ, and WHMCS — but **no Dixa adapter exists**. You'd export Dixa data via the Exports API, then write a custom adapter to transform Dixa's format into Crisp's expected schema.

**When to use it:** Mid-complexity migrations where you're comfortable writing a Node.js adapter (~200–400 lines). The tool handles API authentication, rate-limit retries, resume-on-failure, and conversation creation.

**Pros:** Built-in resume capability (skips previously imported conversations), handles message ordering and timestamp preservation, works on all Crisp plans.

**Cons:** No Dixa adapter — you build it yourself. Adapter documentation is sparse. Provided adapters "may break anytime" per the repo's own warning. The tool does not handle contact creation — people profiles must be imported separately.

**Complexity:** Medium

### 2. API-Based Custom Migration (Dixa API → Crisp API)

**How it works:** Write a custom ETL script that calls the Dixa Exports API (`https://exports.dixa.io/v1/`) to extract conversations and messages, calls the Dixa API (`https://dev.dixa.io/v1/`) to extract end users, tags, and custom attributes, transforms the data, and loads it into Crisp using the REST API. The Crisp Python SDK ([`python-crisp-api`](https://pypi.org/project/crisp-api/)) or Node.js SDK can be used for the load phase. ([docs.dixa.io](https://docs.dixa.io/docs/exports-api-guide))

**When to use it:** Full-control migrations where you need contacts, custom data, segments, and conversation history in a single coordinated pipeline.

**Pros:** Total control over data transformation and error handling. Can handle contacts and conversations in one pipeline. Preserves original timestamps on Crisp messages.

**Cons:** Dixa Exports API queries are limited to 31-day windows — extracting years of history requires iterating month by month. Significant development effort (120–200 engineer-hours for a full implementation). Both platforms impose rate limits that require careful handling.

**Complexity:** High

### 3. CSV Export + Crisp Contact Import (Partial Migration)

**How it works:** Export contacts from Dixa as CSV via the admin UI or Exports API, and import them into Crisp via the built-in CSV import. Crisp's import supports email, full name, company, segments, created dates, and custom data mapping. ([help.crisp.chat](https://help.crisp.chat/en/article/how-to-import-contact-profiles-dcukz8/))

**When to use it:** When you only need to carry over contact data and are willing to leave historical conversations behind.

**Pros:** No code required. Fastest path to get contacts into Crisp. Crisp CSV import supports custom data mapping.

**Cons:** No conversation history, message, attachment, or note migration. Dixa CSV exports don't include message bodies. Large CSVs may need splitting below 10 MB.

**Complexity:** Low

### 4. Middleware Platforms (Zapier, Make, n8n)

**How it works:** Build automation flows that pull data from Dixa and push it to Crisp using pre-built connectors or HTTP request modules.

**When to use it:** Ongoing, real-time sync between systems after the historical migration is complete. Not practical for bulk historical migration.

**Pros:** No-code setup for simple triggers. Good for post-cutover contact sync.

**Cons:** Not designed for bulk historical data. Task limits and API rate limits make large migrations cost-prohibitive. Dixa's Zapier integration has limited trigger/action coverage. Hard to make idempotent at scale.

**Complexity:** Low (for sync), impractical (for bulk migration)

### 5. Managed Migration Service

**How it works:** A migration partner handles the full pipeline — extraction, transformation, loading, validation — using custom scripts tuned to both platforms' APIs and edge cases.

**When to use it:** When you have 50K+ conversations, complex custom attributes, limited engineering bandwidth, or zero tolerance for data loss.

**Pros:** Handles API quota negotiation, rate limiting, error recovery, and edge cases (orphaned messages, missing end users, attachment re-hosting). Includes validation and rollback planning.

**Cons:** External cost. Requires sharing API credentials with the partner.

**Complexity:** Low (for your team)

### Approach Comparison

| Approach | Conversations | Contacts | Custom Data | Complexity | Best For |
|---|---|---|---|---|---|
| Crisp Import Tool (custom adapter) | ✅ | ❌ (separate step) | Partial | Medium | 10K–200K conversations, dev team available |
| Custom API Pipeline | ✅ | ✅ | ✅ | High | 200K+ conversations, full data fidelity |
| CSV Export + Import | ❌ | ✅ | Partial | Low | Contact-only migration |
| Middleware (Zapier/Make) | ❌ (bulk) | ✅ (trickle) | ❌ | Low | Post-migration ongoing sync |
| Managed Service | ✅ | ✅ | ✅ | Low (your side) | 50K+, limited eng bandwidth |

### Recommendations by Scenario

- **Small team, <10K conversations, dev available:** Crisp import tool with a custom Dixa adapter. Budget 40–80 hours.
- **Mid-market, 10K–200K conversations:** Custom API pipeline or managed service. The Dixa Exports API's 31-day window constraint and Crisp's daily quotas add real complexity.
- **Enterprise, 200K+ conversations:** Managed migration service. The API coordination alone justifies the cost.
- **Contacts only, no history needed:** CSV export from Dixa → CSV import into Crisp. Done in an afternoon.
- **Ongoing sync post-cutover:** Zapier or Make for new conversation notifications or contact sync.

The summary: **CSV for contacts only, API or ETL for history, middleware for ongoing sync, managed service when you cannot afford mistakes.** Neither platform exposes a one-click Dixa-to-Crisp translator.

## When to Use a Managed Migration Service

Building a migration script in-house always looks like a two-day project on a Jira board. In reality, the hidden costs stack up:

- **API quota coordination.** Crisp's plugin token quotas start at approximately 5,000 requests/day. Large imports require requesting increases through the Marketplace, which adds days of back-and-forth.
- **31-day export windows.** Dixa's Exports API limits queries to 31-day ranges. Three years of history means 36+ sequential API calls just for extraction, each needing error handling.
- **Attachment re-hosting.** Dixa attachment URLs may expire or require authentication. Each file needs to be downloaded and re-uploaded to Crisp.
- **Custom attribute translation.** Dixa uses typed, schema-defined attributes (dropdown, integer, text) mapped by UUID. Crisp uses freeform key-value pairs. The translation logic is not trivial.
- **Internal notes extraction.** Dixa's `message_export` endpoint does not include internal notes. You must separately extract `conversation_wrapup_notes` from the conversation export or the Dixa API conversation endpoint. Miss this and you lose all internal notes. ([docs.dixa.io](https://docs.dixa.io/docs/exports-api-guide))
- **Email suppression.** Crisp recommends contacting support before importing to block outbound emails from imported conversations. Miss this step and your customers get unexpected messages.
- **Webhook migration.** If your Dixa instance pushes events to downstream systems (CRMs, analytics, warehouses) via webhooks, those configurations must be recreated in Crisp's webhook system or via plugin integrations. This is often overlooked until post-cutover when downstream systems stop receiving events.

When you build in-house, your engineers absorb all of this operational risk. If historical tickets map to the wrong Crisp profiles, your support team loses context and customers get a worse experience.

## Pre-Migration Planning

Do not touch an API key until you've audited your Dixa environment.

### Data Audit Checklist

| Data Object | Where to Find It | Migration Priority |
|---|---|---|
| Conversations | Exports API (`/v1/conversation_export`) | High — core history |
| Messages | Exports API (`/v1/message_export`) | High — conversation content |
| End Users | Dixa API (`/v1/endusers`) | High — maps to Crisp people |
| Internal / Wrap-up Notes | Dixa API (conversation `conversation_wrapup_notes`) | Medium — not in message export |
| Tags | Dixa API (`/v1/tags`) | High — maps to Crisp segments |
| Custom Attributes | Dixa API (`/v1/custom-attributes`) | Medium–High |
| CSAT Ratings | Dixa API (`/v1/conversations/{id}/ratings`) | Medium |
| Agents | Dixa API (`/v1/agents`) | Low (recreate manually in Crisp) |
| Queues | Dixa API (`/v1/queues`) | Low (rebuild as Crisp Inboxes) |
| Flows | No API export | N/A (rebuild manually) |
| Knowledge Base | Dixa Knowledge (no bulk API export) | Medium |
| Attachments / Recordings | Dixa Files API | Medium–High |
| Webhook configurations | Dixa admin UI (Settings → Webhooks) | Medium — rebuild in Crisp |

### Identify What Not to Migrate

- **Closed spam conversations** — filter these out during extraction
- **Bot-only conversations** with no human reply — often noise
- **Test/sandbox data** — exclude agent test conversations
- **Conversations older than 3 years** — truncating old data speeds up the migration and reduces API load; decide based on actual agent usage patterns
- **Deactivated agent records** — Dixa's `GET /v1/agents` only returns active agents by default; pass `?active=false` to include deactivated ones

### GDPR and Data Residency Considerations

Both Dixa and Crisp serve EU customers and store data in the EU (Dixa in AWS EU regions, Crisp in EU data centers). During migration, data transits through your extraction infrastructure. Ensure:

- Your extraction scripts run on infrastructure within an approved data processing region
- Temporary data stores (JSON files, databases) used during transformation are encrypted at rest
- Your Data Processing Agreement (DPA) with Crisp is executed before importing personal data
- If using a managed migration service, verify the partner has an appropriate DPA and processes data within compliant jurisdictions
- After migration, confirm with Dixa their data retention and deletion policies for your account — Dixa's standard terms may retain data for a period after account closure, which affects your GDPR obligations

### Dixa Data Retention After Account Closure

Before canceling your Dixa subscription, confirm in writing how long Dixa retains your data post-cancellation. Dixa's standard terms typically allow data retrieval for 30 days after contract end, after which data may be permanently deleted. This creates a hard deadline for your extraction phase.

### Migration Strategy

Three options:

- **Big bang:** Extract everything, import in one batch, cut over. Works well for <50K conversations. Run a full historical migration, pause Dixa, run a delta migration for the final few hours of tickets, and route DNS/email forwarding to Crisp.
- **Phased:** Migrate in time-based batches (last 6 months first, then older history). Reduces risk and allows validation between phases. Crisp's own migration guides recommend stabilizing the new workflow before loading historical conversations. ([help.crisp.chat](https://help.crisp.chat/en/article/moving-from-zendesk-13w918w/))
- **Incremental:** Migrate historical data, then run a delta sync for conversations created during the migration window. Requires timestamp tracking.

### Timeline Estimates by Phase

| Phase | Scope: <10K convos | Scope: 50K–150K convos | Scope: 200K+ convos |
|---|---|---|---|
| Data audit & planning | 1–2 days | 2–3 days | 3–5 days |
| Extraction from Dixa | 2–4 hours | 1–3 days | 3–7 days |
| Transformation | 1–2 days | 3–5 days | 5–10 days |
| Loading into Crisp | 4–8 hours | 2–5 days | 5–14 days |
| Validation & UAT | 1–2 days | 2–3 days | 3–5 days |
| Cutover & monitoring | 1 day | 1–2 days | 2–3 days |
| **Total** | **1–2 weeks** | **2–4 weeks** | **3–6 weeks** |

Extraction time is dominated by the Dixa Exports API rate limits (10 conversation export requests/minute, 3 message export requests/minute) and the 31-day windowing constraint. Loading time depends on Crisp plugin token daily quotas and any quota increases obtained from the Marketplace.

## Data Model & Object Mapping

This is where most Dixa-to-Crisp migrations succeed or fail. The platforms share a conversation-centric model, but the details diverge.

### Object-Level Mapping

| Dixa Object | Crisp Equivalent | Notes |
|---|---|---|
| End User | People Profile | Match on email. Crisp enforces email uniqueness. |
| Conversation | Conversation (Session) | 1:1 mapping. Both use conversation as the core unit. |
| Message | Message | Crisp supports types: `text`, `note`, `file`, `audio`, `event`. |
| Internal Note / Wrap-up Note | Message (type: `note`) | Dixa stores wrap-up notes separately; Crisp treats all notes as message type. Both types must be extracted and imported. |
| Tag | Segment | Dixa tags are conversation-level; Crisp segments apply to both conversations and contacts. |
| Custom Attribute (End User) | People Custom Data | Dixa: typed schema (UUID-keyed). Crisp: freeform key-value JSON in `data` object. |
| Custom Attribute (Conversation) | Session Data | Same transformation challenge. |
| Queue | Inbox | Rebuild manually; routing logic differs fundamentally. |
| Flow | Workflow | No migration path; rebuild in Crisp's visual builder. |
| CSAT Rating | No direct structural equivalent | Store as conversation metadata/custom data or re-implement using Crisp's native satisfaction survey. Historical ratings import as `data.csat_score` and `data.csat_comment`. |
| Agent | Operator | Recreate manually in Crisp. |
| Linked Conversations | No equivalent | Dixa's `link_type` (merged, parent/child) has no Crisp analog. Preserve as `data.dixa_linked_to` and `data.dixa_link_type` custom fields on the session. |
| Webhooks | Plugin webhooks or Crisp hooks | Must be rebuilt; endpoint configurations and event types differ. |

### Field-Level Mapping: End Users → People Profiles

| Dixa Field | Crisp Field | Transform |
|---|---|---|
| `email` | `email` | Direct |
| `displayName` | `person.nickname` | Direct |
| `phoneNumber` | `phone` | Normalize to E.164 |
| `additionalEmails` | Not natively supported | Store in custom data as `data.additional_emails` (JSON array) |
| `additionalPhoneNumbers` | Not natively supported | Store in custom data as `data.additional_phones` (JSON array) |
| `customAttributes` (typed, UUID-keyed) | `data` (freeform key-value) | Flatten UUID keys to human-readable names using attribute definitions endpoint |
| `externalId` | `data.dixa_external_id` | Store as custom data |

### Field-Level Mapping: Conversations → Sessions

| Dixa Field | Crisp Field | Transform |
|---|---|---|
| `csid` (conversation ID) | `session_id` | Generate new UUID or use mapping table |
| `channel` (email, chat, phone) | `origin` | Map to Crisp origin URN |
| `requesterId` | People profile link | Resolve via email match |
| `assignedAgentId` | Conversation participant (operator) | Map agent email |
| `state` (open, closed) | Conversation state | Direct |
| `tags []` | `segments []` | Lowercase, underscore-delimited |
| `createdAt` | `created_at` | Timestamp in milliseconds |
| `customAttributes` | `meta.data` | Flatten UUID keys to readable names |
| `conversation_wrapup_notes` | Message (type: `note`) | Flatten to note message; preserve author and timestamp |
| `recording_url` / `voicemail_url` | Custom data or archived link note | Fetch, store, re-link; often better archived than replayed |
| `link_type` / `linked_to` | Custom data (`data.dixa_linked_to`, `data.dixa_link_type`) | Preserve for audit trail; no relational equivalent in Crisp |

### Handling Custom Attributes

Dixa's custom attributes use a schema with explicit types. Each attribute has a UUID, a `displayName`, an `entityType` (conversation or end_user), and an `inputDefinition` that specifies the data format (text, dropdown, integer, etc.).

Crisp's custom data is freeform — any JSON key-value pair. This means:

- **Dropdown values** in Dixa become plain strings in Crisp (validation is lost)
- **Integer fields** become JSON numbers (or strings if you're not careful — always cast explicitly)
- **UUID attribute keys** must be resolved to human-readable names using the custom attributes definition endpoint
- **Multi-select fields** often need to become arrays or delimited strings
- **Boolean fields** should remain JSON booleans, not strings like `"true"`

> [!WARNING]
> **Edge case:** Dixa's `GET /v1/endusers` list endpoint does not include custom attributes in the response. You must call `GET /v1/endusers/{userId}` individually for each user to retrieve their custom attributes. For 100K end users at 10 req/sec, that's ~2.8 hours of extraction time just for custom data.

## Migration Architecture

### Data Flow: Extract → Transform → Load

```
┌──────────┐     ┌──────────────┐     ┌──────────┐
│  DIXA    │────▶│  TRANSFORM   │────▶│  CRISP   │
│          │     │              │     │          │
│ Exports  │     │ • Flatten    │     │ People   │
│ API      │     │   attributes │     │ API      │
│ (convos) │     │ • Map tags   │     │          │
│          │     │   → segments │     │ Messages │
│ Dixa API │     │ • Normalize  │     │ API      │
│ (users,  │     │   phone #s   │     │          │
│  attrs,  │     │ • Re-host    │     │ Import   │
│  notes)  │     │   files      │     │ Tool     │
└──────────┘     └──────────────┘     └──────────┘
```

### Dixa Extraction Details

**Exports API** (`https://exports.dixa.io/v1/`):
- Uses bearer token authentication
- Returns streamed JSON arrays
- Queries are time-bounded: `created_after` and `created_before` parameters, max 31-day span
- Two endpoints: `conversation_export` (conversation metadata, wrap-up notes) and `message_export` (message bodies)
- Rate limit: 10 conversation export requests/minute and 3 message export requests/minute ([docs.dixa.io](https://docs.dixa.io/docs/api-standards-rules))

**Dixa API** (`https://dev.dixa.io/v1/`):
- End users: `GET /v1/endusers` (paginated, `pageKey` + `pageLimit`)
- Individual user with custom attrs: `GET /v1/endusers/{userId}`
- Tags: `GET /v1/tags`
- Custom attribute definitions: `GET /v1/custom-attributes`
- Ratings: `GET /v1/conversations/{conversationId}/ratings`
- Rate limit: 10 req/sec per token, burst of 4, daily ceiling of 864,000
- No `Retry-After` header on 429 responses — implement exponential backoff

> [!CAUTION]
> **Data loss risk:** Dixa's Exports API message endpoint does not return internal notes. If you only use the Exports API for messages, you'll lose all internal notes. You must also extract `conversation_wrapup_notes` from the conversation export endpoint or use the Dixa API v1 conversation endpoint.

### Crisp Loading Details

**Crisp REST API** (`https://api.crisp.chat/v1/`):
- **Plugin tokens** are recommended for migrations — they're exempt from per-route rate limits and only subject to daily quotas. Website tokens are limited to ~10,000 requests/day; plugin tokens start at ~5,000/day but can be increased by requesting through the Marketplace. ([docs.crisp.chat](https://docs.crisp.chat/references/rest-api/v1/))
- `POST /v1/website/{website_id}/conversation/{session_id}/message` — supports a `timestamp` field for ingesting older messages with original timestamps
- People profiles: `POST` to create, `PATCH` to update via `/v1/website/{website_id}/people/profile`
- Custom data: `PUT /v1/website/{website_id}/people/data/{people_id}`
- Segments: set via conversation meta update or people profile update
- Authentication: Basic Auth with `identifier` and `key` generated from a custom plugin in the Crisp Marketplace. Do not use personal user tokens for migrations.
- **429 handling:** Crisp returns HTTP 429 with a JSON body including `"reason": "too_many_requests"`. Unlike some APIs, Crisp does not include a `Retry-After` header on 429 responses. Implement exponential backoff starting at 2 seconds, doubling per retry, capping at 120 seconds.
- **Python SDK:** The [`crisp-api`](https://pypi.org/project/crisp-api/) package (version 5.x as of mid-2025) wraps the REST API. Pin the version in your `requirements.txt` to avoid breaking changes during migration.

> [!TIP]
> **Critical step:** Before starting any conversation import, contact Crisp support and ask them to block outbound emails for your workspace. Imported conversations can trigger email notifications to customers if this isn't disabled. Re-enable after the import is complete and validated.

### Authentication & Rate Limits Summary

| Platform | Auth Method | Rate Limit | Daily Limit |
|---|---|---|---|
| Dixa Exports API | Bearer token | 10 conv req/min, 3 msg req/min | Shared with Dixa API |
| Dixa API v1 | Bearer token | 10 req/sec per token, burst of 4 | 864,000 req/token |
| Crisp REST API (plugin) | Identifier + Key (Basic Auth) | Not subject to per-route limits | Daily quota (~5,000 default; requestable increase) |
| Crisp REST API (website) | Identifier + Key (Basic Auth) | API Global + API Route limits | ~10,000 req/day |

## Step-by-Step Migration Process

### Step 1: Extract Conversations and Messages from Dixa

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

DIXA_TOKEN = "your_dixa_api_token"
EXPORTS_URL = "https://exports.dixa.io/v1"

def extract_conversations(start_date, end_date):
    """Extract conversations in 31-day chunks."""
    all_conversations = []
    current = start_date
    
    while current < end_date:
        chunk_end = min(current + timedelta(days=31), end_date)
        
        response = requests.get(
            f"{EXPORTS_URL}/conversation_export",
            params={
                "created_after": current.strftime("%Y-%m-%d"),
                "created_before": chunk_end.strftime("%Y-%m-%d")
            },
            headers={"Authorization": f"Bearer {DIXA_TOKEN}"}
        )
        
        if response.status_code == 200:
            all_conversations.extend(response.json())
        elif response.status_code == 429:
            # No Retry-After header — exponential backoff
            time.sleep(60)
            continue
        else:
            raise Exception(f"Extraction failed: {response.status_code} — {response.text}")
        
        current = chunk_end
    
    return all_conversations
```

### Step 2: Extract End Users and Custom Attribute Definitions

```python
def extract_end_users():
    """Paginate through all end users."""
    users = []
    page_key = None
    
    while True:
        params = {"pageLimit": 100}
        if page_key:
            params["pageKey"] = page_key
        
        response = requests.get(
            "https://dev.dixa.io/v1/endusers",
            params=params,
            headers={"Authorization": f"Bearer {DIXA_TOKEN}"}
        )
        
        if response.status_code == 429:
            time.sleep(1)
            continue
        
        data = response.json()
        users.extend(data.get("data", []))
        
        page_key = data.get("nextPageKey")
        if not page_key:
            break
    
    return users

def get_user_custom_attributes(user_id):
    """Fetch individual user to get custom attributes (not included in list endpoint)."""
    response = requests.get(
        f"https://dev.dixa.io/v1/endusers/{user_id}",
        headers={"Authorization": f"Bearer {DIXA_TOKEN}"}
    )
    if response.status_code == 200:
        return response.json().get("data", {}).get("customAttributes", {})
    return {}

def get_attribute_definitions():
    """Fetch custom attribute definitions to map UUIDs to names."""
    response = requests.get(
        "https://dev.dixa.io/v1/custom-attributes",
        headers={"Authorization": f"Bearer {DIXA_TOKEN}"}
    )
    definitions = response.json().get("data", [])
    return {attr["id"]: attr["displayName"] for attr in definitions}
```

### Step 3: Transform Data to Crisp Format

```python
import re

def normalize_phone_e164(phone, default_country_code="+1"):
    """Normalize phone number to E.164 format."""
    if not phone:
        return ""
    digits = re.sub(r'[^\d+]', '', phone)
    if not digits.startswith('+'):
        digits = default_country_code + digits
    return digits

def transform_end_user(dixa_user, attr_map):
    """Transform a Dixa end user into Crisp people profile format."""
    custom_data = {}
    for attr_id, value in dixa_user.get("customAttributes", {}).items():
        readable_name = attr_map.get(attr_id, attr_id)
        key = readable_name.lower().replace(" ", "_")
        # Preserve type: integers stay integers, booleans stay booleans
        custom_data[key] = value
    
    if dixa_user.get("externalId"):
        custom_data["dixa_external_id"] = dixa_user["externalId"]
    
    if dixa_user.get("additionalEmails"):
        custom_data["additional_emails"] = dixa_user["additionalEmails"]
    
    if dixa_user.get("additionalPhoneNumbers"):
        custom_data["additional_phones"] = dixa_user["additionalPhoneNumbers"]
    
    return {
        "email": dixa_user.get("email"),
        "person": {"nickname": dixa_user.get("displayName", "")},
        "phone": normalize_phone_e164(dixa_user.get("phoneNumber", "")),
        "data": custom_data
    }

def transform_tag_to_segment(tag):
    """Dixa tags map to Crisp segments. Lowercase, underscore-delimited."""
    return tag.lower().replace(" ", "_")
```

### Step 4: Load Contacts into Crisp

Create people profiles first — you'll need the Dixa user ID → Crisp people ID mapping to link conversations later.

```python
from crisp_api import Crisp

client = Crisp()
client.set_tier("plugin")
client.authenticate(CRISP_IDENTIFIER, CRISP_KEY)

WEBSITE_ID = "your_website_id"

# Persistent ID map — use a database or JSON file in production
id_map = {}  # {dixa_user_id: crisp_people_id}

def import_people_profile(dixa_user_id, profile_data):
    """Create or update a Crisp people profile."""
    try:
        result = client.website.add_new_people_profile(
            WEBSITE_ID,
            {"email": profile_data["email"],
             "person": profile_data["person"]}
        )
        people_id = result["people_id"]
        
        if profile_data.get("data"):
            client.website.save_people_data(
                WEBSITE_ID, people_id,
                {"data": profile_data["data"]}
            )
        
        id_map[dixa_user_id] = people_id
        return people_id
    except Exception as e:
        log_error("people_import", profile_data.get("email"), str(e))
        return None
```

### Step 5: Create Conversations and Replay Messages

In Crisp, you first create a conversation session, then append messages to it. A key implementation detail: **a newly created conversation is not visible in the Crisp Inbox until a message is sent with a `user` sender.** Your loader should emit the first customer-authored message before internal notes or agent replies.

```python
import uuid

def import_conversation(dixa_conversation, messages, agent_email_map):
    """Replay messages into a Crisp conversation with original timestamps."""
    session_id = str(uuid.uuid4())
    
    # Create the conversation session
    client.website.create_a_new_conversation(
        WEBSITE_ID,
        {"session_id": session_id}
    )
    
    # Sort messages chronologically
    messages.sort(key=lambda m: m["original_timestamp_ms"])
    
    # Ensure first message is from a user (required for Inbox visibility)
    user_messages = [m for m in messages if m["from"] == "user"]
    if user_messages:
        first_user = user_messages[0]
        messages.remove(first_user)
        messages.insert(0, first_user)
    
    for message in messages:
        try:
            client.website.send_message_in_conversation(
                WEBSITE_ID,
                session_id,
                {
                    "type": message["type"],  # "text", "note", "file"
                    "content": message["content"],
                    "from": message["from"],  # "user" or "operator"
                    "origin": "chat",
                    "timestamp": message["original_timestamp_ms"],
                    "stealth": True  # Don't notify the other party
                }
            )
        except Exception as e:
            log_error("message_import", session_id, str(e))
    
    # Store mapping for delta sync and rollback
    id_map[dixa_conversation["csid"]] = session_id
    return session_id
```

In production, add **idempotency keys**, **source-to-target ID maps** (persisted to disk or database), **exponential backoff on 429**, and a **dead-letter queue** for failed attachments. Do not let the script guess on duplicates; choose a deterministic match order such as `email → external_id → phone → manual review`.

### Step 6: Validate and Cut Over

After import, run validation checks:

- **Record count comparison:** Total conversations in Dixa export vs. total sessions in Crisp. Accept <1% variance for filtered spam/bot conversations.
- **Message count per conversation:** Spot-check 50–100 conversations for message count parity.
- **Contact count:** Dixa end users vs. Crisp people profiles. Account for deduplication reducing the Crisp count.
- **Segment coverage:** Verify that all Dixa tags appear as Crisp segments by querying `GET /v1/website/{website_id}/conversations/list` and checking segment values.
- **Custom data integrity:** Sample 20 contacts and compare custom data values side-by-side between your Dixa export JSON and Crisp's people data endpoint.
- **Timestamp integrity:** Verify that message timestamps in Crisp match the original Dixa timestamps, not the import time.

Only switch mail forwarding, chat widgets, or phone routing after counts and agent UAT pass.

For a deeper validation framework, see [Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

## Edge Cases & Challenges

### Duplicate Contacts

Dixa allows multiple end users with the same email (rare but possible through API creation or different channels). Crisp uses email as the de facto unique identifier for people profiles. Your transform layer must deduplicate by email before importing, merging custom data from duplicates. Strategy: sort duplicates by `updatedAt` descending, take the most recent profile as the primary, and merge custom attributes from older profiles (newer values win on conflict).

### Missing End Users

Some Dixa conversations may reference a `requesterId` that no longer exists (deleted or anonymized users). Your script must handle orphaned conversations gracefully — create a placeholder contact with email `unknown+{requesterId}@placeholder.local` or import the conversation with available metadata only. Log all orphaned conversations for manual review.

### Phone-Only Conversations

Dixa's native telephony creates conversations with voice recordings and call metadata. Crisp has no native phone channel — phone integrations come through third-party plugins (Ringover, Aircall). Phone conversation history can be imported as text-type messages with a note indicating the original channel, but actual call recordings need to be stored externally (S3, GCS) and linked via URLs in custom data. Example note format: `[Phone call — Duration: 4m 23s — Recording: https://your-cdn.com/recordings/{id}.mp3]`.

### Attachments and Inline Images

Dixa attachment URLs are authenticated and may expire. Each attachment must be:

1. Downloaded from Dixa during extraction (store locally or in cloud storage)
2. Re-uploaded to Crisp's bucket endpoint (`POST /v1/website/{website_id}/bucket/url`) or hosted on your own CDN
3. Referenced in the Crisp message as a `file` type message

Images pasted directly into Dixa emails are often base64 encoded or hosted on Dixa's CDN. These will break in Crisp unless parsed, downloaded, and re-uploaded. Budget ~0.5 seconds per attachment for download+upload at scale — for 50K conversations averaging 1.5 attachments each, that's approximately 10 hours of attachment processing.

### Internal Notes vs. Wrap-Up Notes

Dixa has two distinct note types: in-conversation internal notes (visible to agents during the conversation) and wrap-up notes (added at conversation close, stored in `conversation_wrapup_notes`). Crisp treats all notes as messages of type `note`. Both must be extracted and imported — but they come from different API endpoints. The wrap-up notes are in the conversation export; inline notes may require separate API calls. Prefix wrap-up notes with `[Wrap-up Note]` to preserve the distinction in Crisp.

### Imported History Visibility

Imported conversation history in Crisp is visible to operators in the Inbox, but **customers do not automatically see past imported exchanges in the chatbox widget.** If your requirement is customer-visible transcript replay inside the widget, test this in a sandbox before promising parity to stakeholders.

### Multi-Language Knowledge Base

If your Dixa Knowledge base is multi-language, Crisp's helpdesk import requires a separate URL per language variant. Your Dixa KB must be publicly accessible during import.

## Crisp Limitations vs. Dixa

Be honest with your stakeholders about what does not migrate cleanly:

| Capability | Dixa | Crisp | Impact |
|---|---|---|---|
| Native phone/IVR | ✅ Built-in | ❌ Third-party plugins | Phone history loses native context |
| Typed custom attributes | ✅ Schema-defined | ❌ Freeform key-value | Dropdown validation lost |
| Routing flows | ✅ Visual Flow Builder | ✅ Workflows (different paradigm) | Must be rebuilt manually |
| CSAT ratings | ✅ Native with API | ✅ Native surveys | Re-implement triggers; historical ratings migrate as custom data |
| SLA policies | ✅ | Partial | Must be rebuilt |
| Linked conversations | ✅ Parent/child, merged | ❌ No equivalent | Flatten to custom data |
| Tables in KB articles | Supported | Not supported | KB articles with tables need reformatting |
| Video in KB articles | Supported | Not imported | Videos must be re-added manually |
| Contact profile limits | No published limit | Plan-dependent caps | May need higher Crisp plan for large databases |
| API access by plan | Ultimate/Prime only (€139–€179/user/mo) | All plans | Crisp is more accessible for API-based work |
| Webhook integrations | Configurable via admin UI | Plugin-based webhooks | Must be rebuilt; different event taxonomy |

## Validation & Testing

### Validation Strategy

1. **Pre-migration snapshot:** Export full record counts from Dixa — total conversations, messages, end users, tags. Store these counts in a validation spreadsheet.
2. **Test migration:** Run the full pipeline against a subset (e.g., last 30 days of data) into a Crisp test workspace. Always validate in a sandbox before touching production.
3. **Record count comparison:** Dixa totals vs. Crisp totals. Accept <1% variance for edge cases (anonymized users, spam).
4. **Field-level sampling:** Pick 50 random conversations. Verify message count, content, timestamps, and participant mapping.
5. **Custom data spot-check:** Compare 20 contacts side-by-side between Dixa and Crisp.
6. **Segment verification:** Export the full list of Dixa tags and verify each appears as a Crisp segment.

### UAT Process

Have 2–3 agents open Crisp and search for recent customers they handled in Dixa:

- Verify conversation history is complete and readable
- Confirm segments (tags) are visible and filterable
- Check that contact custom data appears correctly in the people profile sidebar
- Verify they can route the next reply correctly
- Confirm that wrap-up notes are distinguishable from inline notes

### Rollback Planning

Crisp does not offer a one-click "undo import." Your rollback plan:

1. **Use a test workspace** for the first full import. Only import into production after validation passes.
2. **Track imported session IDs** in your persistent ID map so you can programmatically delete them via `DELETE /v1/website/{website_id}/conversation/{session_id}` if needed. For 100K conversations at Crisp's daily API quota, a full rollback would take approximately 2–5 days depending on your quota tier.
3. **Back up everything** from Dixa before starting. Keep the raw JSON exports in versioned cloud storage (S3, GCS) with at least 90-day retention.

## Post-Migration Tasks

### Rebuild Workflows and Automations

Dixa's Flow Builder logic — routing, IVR trees, auto-replies, escalation rules — has no migration path. Rebuild in Crisp's Workflow builder. The paradigm differs but covers similar use cases: auto-tagging, routing to inboxes, bot responses.

### Rebuild Webhook Integrations

If your Dixa instance sent webhook events to downstream systems (CRMs, analytics pipelines, data warehouses, Slack channels), catalog every active webhook and its downstream consumer. Recreate equivalent event subscriptions in Crisp's plugin webhook system. Test each webhook end-to-end before cutover.

### Rebuild Knowledge Base

Crisp can auto-import KB content from a public URL via the helpdesk import feature. Your Dixa KB must be publicly accessible during import. Tables are not supported and videos are not imported. Manual formatting review is required after import.

### Agent Onboarding

- Crisp uses **Inboxes** (not queues) for conversation routing
- **Segments** replace tags — same concept, different terminology
- Custom data appears in the conversation sidebar, not in dedicated fields
- Keyboard shortcuts and conversation actions differ from Dixa
- Internal notes are added as message type `note` in the conversation, not via a separate notes panel

### Post-Cutover Monitoring

In the first 2 weeks:

- Watch for duplicate contacts created by the live chat widget (new visitors who already exist as imported contacts)
- Monitor Crisp's people profile count against plan limits
- Check that Workflow automations fire correctly on new conversations
- Watch error logs for the first 48 hours for webhook failures or routing misconfigurations
- Verify downstream systems that depended on Dixa webhooks are receiving events from Crisp

## Best Practices

- **Back up before migration.** Export all Dixa data to JSON files and store in versioned cloud storage (S3, GCS) with 90-day retention before touching anything in Crisp.
- **Run a sandbox migration first.** Move 1,000 tickets to a Crisp test workspace to test your mapping logic before touching production. Validate after each phase, not just at the end.
- **Block outbound emails.** Contact Crisp support to disable email notifications before importing conversations. Re-enable after the import is complete and validated.
- **Use plugin tokens.** Plugin tokens in Crisp are exempt from per-route rate limits — significantly better for bulk operations. Request quota increases through the Marketplace before starting. Allow 3–5 business days for quota increase requests.
- **Track the ID map.** Maintain a persistent mapping of Dixa conversation IDs → Crisp session IDs and Dixa user IDs → Crisp people IDs. Store in a database or versioned JSON file. You'll need this for debugging, delta syncs, rollback, and post-migration auditing.
- **Handle rate limits gracefully.** Neither Dixa nor Crisp returns a `Retry-After` header on 429 responses. Implement exponential backoff starting at 1–2 seconds, doubling per retry, capping at 60–120 seconds.
- **Log everything.** If a single message fails to push, your script should log the Dixa ID, error message, and timestamp so you can retry it later without restarting the entire batch. Use a dead-letter queue for failed attachments.
- **Execute DPA with Crisp.** Ensure your Data Processing Agreement is signed before importing any personal data, especially if migrating EU customer records.
- **Do not delete Dixa immediately.** Keep it in a read-only state for at least 30 days post-migration as a fail-safe. Confirm Dixa's post-cancellation data retention timeline in writing.

For general migration best practices, see [Best Practices for a Successful Help Desk Data Migration](https://clonepartner.com/blog/blog/best-practices-for-help-desk-data-migration/).

## Making the Decision

A Dixa-to-Crisp migration is moderate complexity. The data layer is manageable — both platforms are conversation-centric, and Crisp's API supports timestamp-preserving message ingestion. The hard parts are the workflow rebuild (Dixa Flows → Crisp Workflows), the custom attribute translation (typed schema → freeform key-value), the extraction constraints (31-day API windows, separate endpoints for notes), and the webhook/integration rebuild.

If your team has JavaScript or Python experience and your dataset is under 50K conversations, a DIY migration using Crisp's import tool with a custom adapter is realistic. Budget 2–3 weeks.

If you're dealing with 100K+ conversations, complex custom attributes, phone history, or a tight cutover window, the engineering overhead and risk of data loss typically justify a managed migration.

## Frequently asked questions

### Can I migrate conversation history from Dixa to Crisp?

Yes, but not through CSV import alone. Crisp's native import is contact-focused. Historical conversations require Crisp's crisp-import-conversations tool with a custom Dixa adapter, or direct API calls using the message endpoint with original timestamps. Internal notes require a separate extraction step from Dixa's conversation export — the message export endpoint does not include them.

### Does Crisp have a built-in Dixa migration tool?

Not directly. Crisp's open-source crisp-import-conversations tool supports adapters for Zendesk, Gorgias, Help Scout, Tidio, GrooveHQ, and WHMCS — but no Dixa adapter is included. You need to write a custom adapter or use the raw Crisp API for data loading.

### What Dixa plan do I need for API-based migration?

Dixa's API access is only available on the Ultimate and Prime plans (€139–€179/user/month). The Growth plan does not include API access. Without API access, you're limited to CSV exports, which don't include message bodies or threaded conversation history.

### How long does a Dixa to Crisp migration take?

For 50,000–150,000 conversations, expect 2–4 weeks including data extraction, transformation script development, test migration, validation, and cutover. Smaller datasets (under 10K conversations) can be completed in 1–2 weeks. The 31-day API export window constraint and Crisp's daily plugin quotas are the main time factors.

### Will my customers receive emails during the Crisp migration?

They can if you don't take precautions. Contact Crisp support before importing and ask them to block outbound emails for your workspace. Imported conversations can trigger email notifications to customers if this isn't disabled. Re-enable notifications after the import is complete and validated.
