---
title: "Re:amaze to Tidio Migration: The Technical Guide"
slug: reamaze-to-tidio-migration-the-technical-guide
date: 2026-07-29
author: Roopi
categories: [Migration Guide, "Re:amaze", Tidio]
excerpt: "Technical guide to migrating from Re:amaze to Tidio. Covers data model mapping, API limits, JSONL import, edge cases, and step-by-step process."
tldr: "Re:amaze to Tidio migration requires API extraction and careful data model translation. Tidio has no Organization object, read-only operator APIs, and rate limits of 60–120 req/min."
canonical: https://clonepartner.com/blog/reamaze-to-tidio-migration-the-technical-guide/
---

# Re:amaze to Tidio Migration: The Technical Guide


# Re:amaze to Tidio Migration: The Technical Guide

Migrating from [Re:amaze](https://www.reamaze.com/) to [Tidio](https://www.tidio.com/) is a data model translation, not a drag-and-drop export. Re:amaze organizes support around continuous, reopenable Conversations tied to Contacts, Channels, and Brands. Tidio structures its world around Contacts, Conversations, and Tickets — with a flatter schema, no dedicated Organization object, and API endpoints that are still actively evolving.

The core challenge: Re:amaze allows complex custom attributes and continuous conversation threads across multiple channels. Tidio enforces a flatter hierarchy where organizational data must be mapped to contact-level properties, and infinite threads must be segmented into discrete tickets.

This guide covers every step of the move — extraction, transformation, loading — including what transfers cleanly, what requires transformation, and what you lose permanently.

## Why Teams Move from Re:amaze to Tidio

Teams don't switch helpdesks without a reason. Based on patterns across hundreds of helpdesk migrations:

- **AI-first automation.** Tidio's Lyro AI Agent handles multi-turn conversations autonomously, resolving up to 70% of routine queries without human intervention according to Tidio's published benchmarks. Re:amaze offers AI reply suggestions but lacks a comparable autonomous agent.
- **Pricing model shift.** Re:amaze uses per-agent pricing (starting at $29/agent/month on the Basic plan). Tidio offers per-conversation pricing with a free tier — a better fit for small teams with variable volume. The metering differs too: Re:amaze counts a conversation as active once there's a customer-facing reply from staff, a chatbot, or an autoresponder, while Tidio counts only conversations with a human agent reply and doesn't count unanswered spam.
- **Simpler interface.** Teams that found Re:amaze's tag-heavy navigation and multi-brand configuration complex often prefer Tidio's streamlined panel.
- **Chatbot and automation portability.** Tidio's Flows can be exported and imported as JSON between projects, making repeatable client rollouts easier for agencies and multi-site operators. ([help.tidio.com](https://help.tidio.com/hc/en-us/articles/10147126351516-Transfer-flows-between-projects))
- **Shopify ecosystem consolidation.** Both platforms integrate with Shopify, but teams already invested in Tidio's chatbot flows and Lyro AI for e-commerce may want to consolidate to a single platform.

The trade-offs are real. Re:amaze has deeper multi-channel support (VoIP, video calls, SMS alongside email and chat), a built-in FAQ/knowledge base with full API support, and a Status Page feature. Tidio has no knowledge base import pathway, no API-writable operator or department endpoints, and tighter rate limits. Know what you're giving up before you start.

## Core Architectural Differences

Understanding the structural gap between these platforms is the most important pre-migration step.

| Concept | Re:amaze | Tidio |
|---|---|---|
| Data model center | Conversations (continuous threads) | Contacts + Tickets + Conversations |
| Customer identity | Contacts with multiple Identities (email, social, phone) | Contacts identified by email, name, or phone |
| Organization/Company | No first-class object (handled via custom attributes) | No dedicated object (flatten to Contact Properties) |
| Channels | Email, chat, social, SMS, VoIP, video — all API-accessible | Live chat, email, social, Messenger — widget-centric |
| Knowledge base | Articles API (full CRUD) | No import API; manual rebuild only |
| Agent management | Staff API (read + create) | Operators API (read-only) |
| Departments/Groups | Channels with categories | Departments API (read-only) |
| Custom data | Custom attributes on Contacts and Conversations | Contact Properties (text, email, number, phone, url types) |
| Conversation statuses | Open, Responded, Done, Spam, Archived, On Hold, Auto-Done, AI-related states (8+ distinct values) | Open, Pending, Solved (3 values) |
| Auth model | HTTP Basic Auth (email:token), brand-scoped | API key pair (Client-Id + Client-Secret), project-scoped |
| Rate limits | Per-token, not publicly documented | Plus: 60 req/min; Premium: 120 req/min ([developers.tidio.com](https://developers.tidio.com/docs/openapi-rate-limiting)) |
| API access tier | Available on all paid plans | Plus and Premium plans only |

Tidio is the narrower model. Re:amaze documents more than eight conversation lifecycle states versus Tidio's three (open, pending, solved), which means information loss is unavoidable unless you preserve the source status value in a custom field or tag.

## Data Mapping Overview

Before writing any migration code, map every Re:amaze object to its Tidio equivalent — or acknowledge that no equivalent exists.

| Re:amaze Object | Tidio Object | Migration Notes |
|---|---|---|
| Contacts | Contacts | Map email, name, phone. Custom attributes → Contact Properties. |
| Contact Identities | Contact email/phone fields | Tidio contacts support one primary email; additional identities must be stored in custom properties or discarded. |
| Contact Notes | No direct equivalent | Store as Contact Properties (text) or archive externally. |
| Conversations | Tickets | Map subject, status, tags. Message history → Ticket replies. |
| Messages | Ticket Replies | Replay sequentially. Prefer JSONL import for `htmlContent` and `createdAt`. |
| Channels | Departments | Manual creation required — Departments API is read-only. |
| Staff | Operators | Manual creation required — Operators API is read-only. |
| Response Templates | No import API | Rebuild manually in Tidio's canned responses. |
| Articles (KB) | No equivalent | No knowledge base import in Tidio. Archive or rebuild manually. |
| Tags | Tags | Re:amaze conversation tags → Tidio ticket tags. Direct mapping. |
| Satisfaction Ratings | No import pathway | Export for archival; no Tidio import mechanism. |
| Status Page / Incidents | No equivalent | Not available in Tidio. |
| Custom Attributes (Conversations) | Contact Properties / Ticket tags | Lossy mapping — Tidio ticket-level custom fields are limited. |

If you have CRM-like entities mixed into agent workflows (accounts, companies, deals), those aren't native to either platform in the Salesforce/HubSpot sense. Keep the system of record in your CRM and expose only the context agents need — flattened into Tidio contact properties or ticket custom fields.

## Migration Approaches

There are five viable paths. The right one depends on your data volume, engineering capacity, and tolerance for data loss.

### 1. Native CSV Export + API Import

Re:amaze supports CSV export for contacts via the Contacts tab. ([support.reamaze.com](https://support.reamaze.com/kb/about-your-customers/exporting-re-amaze-customer-contacts)) You export, clean the file, and use Tidio's `POST /contacts` endpoint or CSV/TXT import to create contacts. ([help.tidio.com](https://help.tidio.com/hc/en-us/articles/5463416657052-Import-Contacts-Into-Tidio))

**When to use it:** Small datasets (under 1,000 contacts), contacts-only migrations, or when you don't need conversation history.

**Pros:** Low complexity, no API extraction code needed for contacts.
**Cons:** No conversation or ticket history. CSV export doesn't include message threads, attachments, or internal notes.
**Complexity:** Low

Note on Tidio CSV import deduplication: Tidio's CSV/TXT contact import can update existing contacts by matching on email address. This contrasts with the API's `POST /contacts` endpoint, which always creates new records. If you're doing a contacts-only migration via CSV, structure your file with `email` as the first column — Tidio uses this as the deduplication key during CSV imports.

### 2. API-to-API Migration

Extract all data from Re:amaze's REST API, transform it in-memory or via staging, and load it into Tidio via their OpenAPI endpoints.

**When to use it:** Any migration where you need conversation history, custom field data, and contact records with full programmatic control.

**Pros:** Highest control over field mapping and transformation. Good for delta sync during phased cutover.
**Cons:** Requires engineering time. Tidio's rate limits (60–120 req/min depending on plan) make large datasets slow. The create/reply ticket endpoints don't preserve original timestamps or HTML message bodies as cleanly as the JSONL import path.
**Complexity:** High

### 3. Hybrid: API Extract + JSONL Ticket Import

Extract Re:amaze data via API, then use Tidio's JSONL ticket importer for historical backfill and the OpenAPI for contacts and delta sync. Tidio's JSONL import supports `createdAt`, `htmlContent`, `attachments.publicUrl`, `operatorEmail`, and `departmentName` fields — making it the best option for preserving historical context. ([help.tidio.com](https://help.tidio.com/hc/en-us/articles/25897882005788-Import-Zendesk-tickets))

**When to use it:** Most real helpdesk migrations where timestamp fidelity, HTML email bodies, and attachment preservation matter.

**Pros:** Best balance of fidelity, repeatability, and cutover control. Preserves original timestamps and rich content.
**Cons:** Requires engineering time to build the JSONL file generation. The JSONL import capability is documented in the context of Tidio's Zendesk migration tooling — confirm availability for your specific use case with Tidio support.
**Complexity:** Medium-High

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

Both Re:amaze and Tidio have Zapier and Make integrations. You can build Zaps or Make scenarios that trigger on Re:amaze events and push data to Tidio.

**When to use it:** Ongoing sync between platforms during a phased migration, or for very small datasets where you want no-code simplicity.

**Pros:** No code. Good for real-time sync during transition periods.
**Cons:** Not designed for bulk historical migration. Per-task billing and operation quotas make large backfills impractical. Will timeout on large attachments. Limited transformation logic.
**Complexity:** Low

### 5. Custom ETL Pipeline

Build a dedicated extract-transform-load pipeline with staging databases, idempotent loads, error queues, and audit logging. This is the API approach wrapped in proper data engineering infrastructure.

**When to use it:** Enterprise migrations with 50,000+ records, complex custom attribute mappings, or compliance requirements that demand full audit trails.

**Pros:** Maximum control. Replayable. Full audit trail. Can handle deduplication and conflict resolution.
**Cons:** Highest engineering investment. Over-engineered for most use cases.
**Complexity:** High

### Approach Comparison

| Approach | Data Fidelity | Volume Capacity | Engineering Effort | Best For |
|---|---|---|---|---|
| CSV Export + API Import | Low (contacts only) | Small (<1K) | Low | Quick contact-only moves |
| API-to-API | High | Medium-Large (1K–50K) | High | Full migrations with programmatic control |
| Hybrid (API + JSONL) | Highest | Medium-Large (1K–50K) | Medium-High | Migrations needing timestamp/HTML fidelity |
| Middleware (Zapier/Make) | Low-Medium | Small (<500) | Low | Phased sync, small datasets |
| Custom ETL Pipeline | Highest | Enterprise (50K+) | Highest | Regulated or high-volume orgs |

### Recommendations

- **Small business, <1K contacts, no history needed:** CSV export + Tidio CSV import (leverages email-based deduplication).
- **Mid-size team, full history:** Hybrid API extract + JSONL ticket import.
- **Enterprise, 50K+ records, compliance requirements:** Custom ETL pipeline with audit trail support.
- **Phased migration with parallel operation:** Middleware for real-time sync during transition, with a historical backfill via API or JSONL.
- **Low engineering bandwidth:** Managed migration service. Don't underestimate the throwaway-code burden.

## Pre-Migration Planning

Skipping the audit phase is the most common cause of migration failures.

### Data Audit Checklist

Query Re:amaze to get exact counts before choosing a method:

- Total contact count (with and without email addresses)
- Merged identities per contact (query the identities sub-resource)
- Total conversation count and date range
- Message count per conversation (identify outliers — conversations with 100+ messages need special handling)
- Custom attributes in use (contact-level and conversation-level)
- Tags in use (active vs. stale)
- Knowledge base article count and category structure
- Response template count
- Staff/agent count and role assignments
- Channel configuration (email, chat, social, SMS, VoIP)
- Active automations and workflows
- Satisfaction ratings volume

Re:amaze's Advanced Search lets you scope conversations by assignee, message user, message type, status, tag, channel, and date range. Use it to estimate scope before extraction. ([support.reamaze.com](https://support.reamaze.com/kb/tips-tricks/advanced-search))

### Identify What to Exclude

Not everything deserves migration. Common candidates for exclusion:

- Conversations older than 24 months with no business value
- Test and spam contacts
- Unused custom attributes and stale tags
- Draft or unpublished KB articles
- Inactive staff accounts

### Choose a Migration Strategy

- **Big bang:** Migrate everything in a single cutover window. Works for small, single-brand environments. Risk: any failure means full rollback.
- **Phased:** Migrate contacts first, then conversations, then validate. Lower risk, longer timeline.
- **Incremental:** Run a historical backfill, then sync only changed records until switchover. Re:amaze's conversation endpoints support date filtering and `sort=changed` retrieval, making delta windows practical. This is the best default for mid-size and larger datasets. ([support.reamaze.com](https://support.reamaze.com/api/get_conversations))

### Multi-Brand Extraction

Re:amaze's API is brand-scoped: each brand operates under a separate subdomain (`brand1.reamaze.io`, `brand2.reamaze.io`) and may require separate API credentials. If you're migrating a multi-brand Re:amaze account, plan for:

- **Separate extraction runs per brand.** Each brand's contacts, conversations, and articles are isolated behind its own API base URL.
- **Credential management.** Each brand may have its own API token. Collect all tokens before starting.
- **Cross-brand deduplication.** The same customer may exist as separate contacts in multiple brands. Decide whether to merge them into a single Tidio contact (using email as the deduplication key) or keep them separate.
- **Department mapping.** Re:amaze channels-as-categories per brand may map to different Tidio departments. Build a brand→department mapping table.

For a three-brand account with 10,000 contacts per brand, expect three separate extraction passes and a merge/dedup phase before loading into Tidio.

### Risk Mitigation

- Keep the Re:amaze account active on a downgraded or read-only tier for 30 days post-migration.
- Freeze Re:amaze admin changes (tags, custom fields, routing rules) during the migration window.

## Migration Architecture

The data flow follows a standard ETL pattern with platform-specific constraints at each stage.

```
┌──────────────┐     ┌──────────────────┐     ┌─────────────┐
│  Re:amaze    │     │   Transform      │     │   Tidio     │
│  REST API    │────▶│   (Map/Clean)    │────▶│   OpenAPI   │
│  Extract     │     │   Stage          │     │   + JSONL   │
└──────────────┘     └──────────────────┘     └─────────────┘
       │                      │                       │
   Brand-scoped          Local disk /            Rate-limited
   Basic Auth            SQLite staging          60-120 req/min
```

### Extract: Re:amaze API

- **Base URL:** `https://{brand}.reamaze.io/api/v1/`
- **Auth:** HTTP Basic Auth with login email and API token
- **Pagination:** Page-based (default 30 records/page). Iterate until an empty array is returned.
- **Key endpoints:**
  - `GET /contacts` — all customer records
  - `GET /conversations` — all conversations with metadata (supports `sort=changed` and date filters for delta runs)
  - `GET /conversations/{slug}/messages` — message thread for a conversation (use `include=original_body` for HTML)
  - `GET /staff` — agent records
  - `GET /articles` — knowledge base content
  - `GET /response_templates` — canned responses

> [!WARNING]
> Re:amaze's API rate limit is enforced per token, but the exact requests-per-minute threshold is not publicly documented. Implement exponential backoff on HTTP 429 responses. Start conservatively at 30 requests/minute and adjust based on observed limits. In testing, we've observed thresholds in the range of 60–100 requests/minute, but this may vary by account and plan.

### Transform: Field Mapping and Cleaning

This is where most migration complexity lives.

**Contact identity consolidation:** Re:amaze contacts can have multiple identities (email, Twitter, phone). Tidio contacts have single email and phone fields. Choose primary identifiers and archive secondary identities as Contact Properties (e.g., `secondary_email`, `twitter_handle`).

**Name splitting:** Re:amaze stores a single `name` field. Tidio requires `first_name` and `last_name`. Split on the first space character. Handle edge cases: mononyms (map to `first_name` only), names with prefixes/suffixes, and empty strings.

**Phone normalization:** Normalize phone numbers to E.164 format before loading into Tidio. Tidio's `phone` property type validates format — malformed numbers will be rejected with a 400 error.

**Status mapping:**

| Re:amaze Status | Tidio Status | Notes |
|---|---|---|
| Open | Open | Direct map |
| Responded | Open | No direct equivalent; consider adding `legacy_status:responded` tag |
| Pending | Pending | Direct map |
| On Hold | Pending | No direct equivalent; add `legacy_status:on_hold` tag |
| Done / Archived | Solved | Direct map |
| Spam | — | Exclude or tag with `legacy_status:spam` |
| Auto-Done | Solved | Map or preserve as `legacy_status:auto_done` tag |
| AI-related states | — | Preserve as `legacy_status:{value}` tag |

For states without a Tidio equivalent, store the original value in a `legacy_status:{value}` tag. This preserves the information in a searchable format.

**Custom attributes → Contact Properties:** Pre-create all custom Contact Properties in Tidio (via Settings > Tags and Properties) before import. Tidio supports five property types: `text`, `email`, `number`, `phone`, and `url`. Passing a non-existing property name results in a `400 Bad Request` error with a response body like:

```json
{
  "error": "Validation failed",
  "details": [
    {
      "field": "properties.custom_field_name",
      "message": "Property 'custom_field_name' does not exist"
    }
  ]
}
```

**Tags:** Direct mapping — Re:amaze conversation tags transfer as Tidio ticket tags. Tags are created automatically on first use in Tidio; no pre-creation step needed.

### Load: Tidio OpenAPI and JSONL Import

- **Base URL:** `https://api.tidio.co`
- **Auth:** Custom headers `X-Tidio-Openapi-Client-Id` and `X-Tidio-Openapi-Client-Secret`
- **Plan requirement:** Plus or Premium plan required for API access
- **Key endpoints:**
  - `POST /contacts` — create contacts
  - `PATCH /contacts/{contactId}` — update contact properties
  - `POST /tickets` — create tickets
  - `POST /tickets/{ticketId}/replies` — add replies to tickets
  - `GET /operators` — retrieve operator info (read-only)
  - `GET /departments` — retrieve department info (read-only)

**Rate limits:** Plus plans allow 60 requests/minute; Premium allows 120 requests/minute. At these rates:

| Dataset Size | Plus Plan (60 req/min) | Premium Plan (120 req/min) |
|---|---|---|
| 1,000 contacts | ~17 minutes | ~8 minutes |
| 10,000 contacts | ~2.8 hours | ~1.4 hours |
| 50,000 contacts + tickets | ~14+ hours | ~7+ hours |

These estimates assume contacts-only creation. Ticket creation with message replay multiplies the API calls by the average messages per conversation.

**Batch operations:** Tidio batch operations are capped at 100 records and use an all-or-nothing strategy — if one record in the batch fails validation, the entire batch is rejected. ([developers.tidio.com](https://developers.tidio.com/reference/post_contacts))

> [!CAUTION]
> Tidio's `POST /contacts` endpoint always creates new contacts — it does not deduplicate by email or any other identifier. If you run the import twice, you'll get duplicate records. Build deduplication logic into your pipeline: query existing Tidio contacts via `GET /contacts` first, index by email, and use `PATCH /contacts/{contactId}` to update existing records. Tidio's CSV/TXT contact import, by contrast, deduplicates by email automatically.

> [!NOTE]
> If you need to preserve original timestamps, HTML email bodies, operator attribution, or attachment URLs in historical tickets, prefer **Tidio's JSONL ticket import** over the API ticket creation endpoints. The JSONL importer supports `createdAt`, `htmlContent`, `attachments.publicUrl`, `operatorEmail`, and `departmentName` fields. The API create/reply endpoints are better suited for new traffic and delta sync. ([help.tidio.com](https://help.tidio.com/hc/en-us/articles/25897882005788-Import-Zendesk-tickets))

## JSONL Import Schema and Sample Record

The JSONL importer is the recommended path for historical ticket backfill. Each line in the JSONL file represents one ticket with its full message history. Here's the schema and a sample record derived from a Re:amaze conversation:

```json
{
  "subject": "Order #12345 shipping delay",
  "status": "solved",
  "tags": ["shipping", "legacy_status:done"],
  "departmentName": "Support",
  "messages": [
    {
      "createdAt": "2024-01-15T10:30:00Z",
      "htmlContent": "<p>Hi, my order #12345 hasn't shipped yet. Can you check?</p>",
      "authorType": "contact",
      "contactEmail": "customer@example.com"
    },
    {
      "createdAt": "2024-01-15T11:45:00Z",
      "htmlContent": "<p>I've checked your order and it shipped this morning. Here's the tracking link.</p>",
      "authorType": "operator",
      "operatorEmail": "agent@yourcompany.com"
    },
    {
      "createdAt": "2024-01-15T12:00:00Z",
      "htmlContent": "<p>Thank you!</p>",
      "authorType": "contact",
      "contactEmail": "customer@example.com",
      "attachments": [
        {
          "publicUrl": "https://your-s3-bucket.s3.amazonaws.com/migrations/screenshot.png",
          "filename": "screenshot.png"
        }
      ]
    }
  ]
}
```

**Key fields:**

| Field | Type | Required | Notes |
|---|---|---|---|
| `subject` | string | Yes | Ticket subject line |
| `status` | string | Yes | `open`, `pending`, or `solved` |
| `tags` | array [string] | No | Ticket tags |
| `departmentName` | string | No | Must match an existing Tidio department name exactly |
| `messages [].createdAt` | ISO 8601 | Yes | Original message timestamp |
| `messages [].htmlContent` | string | Yes | HTML body of the message |
| `messages [].authorType` | string | Yes | `contact` or `operator` |
| `messages [].contactEmail` | string | Conditional | Required when `authorType` is `contact` |
| `messages [].operatorEmail` | string | Conditional | Required when `authorType` is `operator`; must match an existing Tidio operator email |
| `messages [].attachments` | array | No | Each item needs `publicUrl` (must be publicly accessible) and `filename` |

Write one JSON object per line (no trailing commas, no array wrapper). File extension should be `.jsonl`.

> [!WARNING]
> The JSONL import capability is documented in the context of Tidio's Zendesk migration tooling. Confirm availability and any schema differences for non-Zendesk sources with Tidio support before building your JSONL generation pipeline.

## Step-by-Step Migration Process

### Step 1: Set Up Tidio for Import

Ensure you're on a Tidio Plus or Premium plan for API access. Generate your OpenAPI Client-Id and Client-Secret. Manually create all operators, departments, and custom Contact Properties in Tidio's admin panel before migration. Collect the resulting IDs via the API — you'll need them for ticket assignment during import.

### Step 2: Extract Contacts from Re:amaze

```python
import requests
import time
import json
import os

REAMAZE_BRAND = "yourbrand"
REAMAZE_EMAIL = "you@example.com"
REAMAZE_TOKEN = "your-api-token"
OUTPUT_DIR = "./extraction"

os.makedirs(OUTPUT_DIR, exist_ok=True)

def extract_contacts():
    contacts = []
    page = 1
    while True:
        resp = requests.get(
            f"https://{REAMAZE_BRAND}.reamaze.io/api/v1/contacts",
            auth=(REAMAZE_EMAIL, REAMAZE_TOKEN),
            headers={"Accept": "application/json"},
            params={"page": page}
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 10))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        if not data.get("contacts"):
            break
        contacts.extend(data["contacts"])
        page += 1
        time.sleep(2)  # Conservative rate limit spacing
    
    # Write raw extraction to disk
    with open(f"{OUTPUT_DIR}/contacts_raw.json", "w") as f:
        json.dump(contacts, f, indent=2)
    
    print(f"Extracted {len(contacts)} contacts")
    return contacts
```

### Step 3: Extract Conversations and Messages

```python
def extract_conversations(since_date=None):
    """Extract conversations with optional delta filtering.
    
    Args:
        since_date: ISO 8601 date string for incremental extraction.
                    Uses sort=changed to get recently modified conversations.
    """
    conversations = []
    page = 1
    params = {"page": page, "sort": "changed"}
    if since_date:
        params["for"] = since_date  # Re:amaze date filter
    
    while True:
        params["page"] = page
        resp = requests.get(
            f"https://{REAMAZE_BRAND}.reamaze.io/api/v1/conversations",
            auth=(REAMAZE_EMAIL, REAMAZE_TOKEN),
            headers={"Accept": "application/json"},
            params=params
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 10))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        if not data.get("conversations"):
            break
        for convo in data["conversations"]:
            slug = convo["slug"]
            messages = extract_messages(slug)
            convo["_messages"] = messages
            conversations.append(convo)
        page += 1
        time.sleep(2)
    
    # Write raw extraction to disk
    with open(f"{OUTPUT_DIR}/conversations_raw.json", "w") as f:
        json.dump(conversations, f, indent=2)
    
    print(f"Extracted {len(conversations)} conversations")
    return conversations

def extract_messages(slug, max_retries=3):
    """Extract messages for a conversation, with retry logic."""
    for attempt in range(max_retries):
        resp = requests.get(
            f"https://{REAMAZE_BRAND}.reamaze.io/api/v1/conversations/{slug}/messages",
            auth=(REAMAZE_EMAIL, REAMAZE_TOKEN),
            headers={"Accept": "application/json"},
            params={"include": "original_body"}
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 10))
            time.sleep(retry_after * (attempt + 1))  # Exponential backoff
            continue
        resp.raise_for_status()
        return resp.json().get("messages", [])
    raise Exception(f"Failed to extract messages for {slug} after {max_retries} retries")
```

### Step 4: Transform Data

```python
import re

STATUS_MAP = {
    0: "open",       # Re:amaze: Open
    1: "open",       # Re:amaze: Responded
    2: "pending",    # Re:amaze: Pending
    3: "solved",     # Re:amaze: Done
    4: "solved",     # Re:amaze: Archived
}

# Re:amaze status names for tag preservation
STATUS_NAMES = {
    0: "open",
    1: "responded",
    2: "pending",
    3: "done",
    4: "archived",
    5: "spam",
    6: "on_hold",
    7: "auto_done",
}

def normalize_phone(phone):
    """Normalize phone number to E.164 format."""
    if not phone:
        return None
    digits = re.sub(r'[^\d+]', '', phone)
    if not digits.startswith('+'):
        # Assume US if no country code; adjust for your region
        if len(digits) == 10:
            digits = '+1' + digits
        elif len(digits) == 11 and digits.startswith('1'):
            digits = '+' + digits
    return digits if len(digits) >= 8 else None

def transform_contact(reamaze_contact):
    """Map Re:amaze contact to Tidio contact schema."""
    name = reamaze_contact.get("name", "") or ""
    parts = name.strip().split(" ", 1)
    
    # Build properties from custom attributes
    properties = {}
    for key, value in (reamaze_contact.get("data", {}) or {}).items():
        prop_name = key.lower().replace(" ", "_").replace("-", "_")
        if value is not None:
            properties[prop_name] = str(value)
    
    # Store secondary identities in properties
    identities = reamaze_contact.get("identities", [])
    for idx, identity in enumerate(identities[1:], start=2):
        id_type = identity.get("type", "unknown")
        id_value = identity.get("identifier", "")
        properties[f"secondary_identity_{idx}_{id_type}"] = id_value
    
    return {
        "email": reamaze_contact.get("email"),
        "first_name": parts[0] if parts[0] else None,
        "last_name": parts[1] if len(parts) > 1 else None,
        "phone": normalize_phone(reamaze_contact.get("mobile")),
        "properties": properties
    }

def transform_conversation_to_jsonl(convo):
    """Transform a Re:amaze conversation to Tidio JSONL ticket format."""
    status_int = convo.get("status", 0)
    tidio_status = STATUS_MAP.get(status_int, "open")
    
    tags = list(convo.get("tag_list", []))
    # Preserve original status as tag if lossy mapping
    if status_int not in (0, 2, 3):
        status_name = STATUS_NAMES.get(status_int, str(status_int))
        tags.append(f"legacy_status:{status_name}")
    
    # Preserve Re:amaze slug for cross-reference
    tags.append(f"reamaze_slug:{convo.get('slug', '')}")
    
    messages = []
    for msg in convo.get("_messages", []):
        is_staff = msg.get("user", {}).get("is_staff", False)
        message_entry = {
            "createdAt": msg.get("created_at"),
            "htmlContent": msg.get("body", ""),
            "authorType": "operator" if is_staff else "contact",
        }
        if is_staff:
            message_entry["operatorEmail"] = msg.get("user", {}).get("email")
        else:
            message_entry["contactEmail"] = msg.get("user", {}).get("email")
        
        # Handle attachments
        attachments = msg.get("attachments", [])
        if attachments:
            message_entry["attachments"] = [
                {"publicUrl": att.get("url"), "filename": att.get("filename", "attachment")}
                for att in attachments if att.get("url")
            ]
        
        messages.append(message_entry)
    
    return {
        "subject": convo.get("subject") or "(No subject)",
        "status": tidio_status,
        "tags": tags,
        "departmentName": convo.get("category", {}).get("name"),
        "messages": messages
    }
```

### Step 5: Handle Attachments

Re:amaze messages can contain file attachments. Tidio does not support direct file attachment uploads for historical imports via the API. Download attachments from Re:amaze and re-host them on external storage with publicly accessible URLs.

```python
import hashlib
import boto3
from urllib.parse import urlparse

s3_client = boto3.client('s3')
S3_BUCKET = "your-migration-bucket"
S3_PREFIX = "reamaze-attachments"

def download_and_rehost_attachment(attachment_url, conversation_slug):
    """Download an attachment from Re:amaze and upload to S3.
    
    Returns the public S3 URL for the JSONL attachments.publicUrl field.
    """
    if not attachment_url:
        return None
    
    resp = requests.get(attachment_url, stream=True)
    resp.raise_for_status()
    
    # Generate a unique filename
    url_hash = hashlib.md5(attachment_url.encode()).hexdigest()[:8]
    original_name = urlparse(attachment_url).path.split("/")[-1] or "attachment"
    s3_key = f"{S3_PREFIX}/{conversation_slug}/{url_hash}_{original_name}"
    
    s3_client.upload_fileobj(
        resp.raw,
        S3_BUCKET,
        s3_key,
        ExtraArgs={
            'ContentType': resp.headers.get('Content-Type', 'application/octet-stream'),
            'ACL': 'public-read'
        }
    )
    
    return f"https://{S3_BUCKET}.s3.amazonaws.com/{s3_key}"

def process_attachments_for_conversation(convo):
    """Download and re-host all attachments in a conversation."""
    slug = convo.get("slug", "unknown")
    for msg in convo.get("_messages", []):
        for att in msg.get("attachments", []):
            original_url = att.get("url")
            if original_url:
                new_url = download_and_rehost_attachment(original_url, slug)
                att["url"] = new_url  # Replace with S3 URL
```

### Step 6: Load Contacts into Tidio

```python
import sqlite3

TIDIO_CLIENT_ID = "your-client-id"
TIDIO_CLIENT_SECRET = "your-client-secret"
TIDIO_BASE = "https://api.tidio.co"

# Initialize ID mapping database
db = sqlite3.connect("migration_mapping.db")
db.execute("""
    CREATE TABLE IF NOT EXISTS contact_map (
        reamaze_email TEXT PRIMARY KEY,
        tidio_id TEXT,
        status TEXT,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")

def get_existing_tidio_contacts():
    """Fetch all existing Tidio contacts for deduplication."""
    contacts = {}
    page = 1
    while True:
        resp = requests.get(
            f"{TIDIO_BASE}/contacts",
            headers={
                "X-Tidio-Openapi-Client-Id": TIDIO_CLIENT_ID,
                "X-Tidio-Openapi-Client-Secret": TIDIO_CLIENT_SECRET,
            },
            params={"page": page}
        )
        if resp.status_code == 429:
            time.sleep(5)
            continue
        resp.raise_for_status()
        data = resp.json()
        if not data.get("data"):
            break
        for c in data["data"]:
            if c.get("email"):
                contacts[c["email"]] = c["id"]
        page += 1
        time.sleep(1)
    return contacts

def load_contact(contact_data, existing_contacts):
    """Create or update a contact in Tidio with deduplication."""
    email = contact_data.get("email")
    if not email:
        return None
    
    headers = {
        "X-Tidio-Openapi-Client-Id": TIDIO_CLIENT_ID,
        "X-Tidio-Openapi-Client-Secret": TIDIO_CLIENT_SECRET,
        "Content-Type": "application/json"
    }
    
    if email in existing_contacts:
        # Update existing contact
        contact_id = existing_contacts[email]
        resp = requests.patch(
            f"{TIDIO_BASE}/contacts/{contact_id}",
            headers=headers,
            json=contact_data
        )
    else:
        # Create new contact
        resp = requests.post(
            f"{TIDIO_BASE}/contacts",
            headers=headers,
            json=contact_data
        )
    
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 5))
        time.sleep(retry_after)
        return load_contact(contact_data, existing_contacts)
    
    if resp.status_code == 400:
        # Log the specific validation error
        error_detail = resp.json()
        logging.error(f"Validation error for {email}: {json.dumps(error_detail)}")
        db.execute(
            "INSERT OR REPLACE INTO contact_map VALUES (?, ?, ?, CURRENT_TIMESTAMP)",
            (email, None, "error")
        )
        db.commit()
        return None
    
    resp.raise_for_status()
    result = resp.json()
    contact_id = result.get("id")
    
    db.execute(
        "INSERT OR REPLACE INTO contact_map VALUES (?, ?, ?, CURRENT_TIMESTAMP)",
        (email, contact_id, "success")
    )
    db.commit()
    
    return result
```

### Step 7: Generate JSONL File for Ticket Import

```python
def generate_jsonl(conversations, output_path="tickets_import.jsonl"):
    """Generate a JSONL file for Tidio's ticket importer."""
    success_count = 0
    skip_count = 0
    
    with open(output_path, "w") as f:
        for convo in conversations:
            ticket_data = transform_conversation_to_jsonl(convo)
            
            # Skip conversations with no messages
            if not ticket_data.get("messages"):
                skip_count += 1
                continue
            
            f.write(json.dumps(ticket_data) + "\n")
            success_count += 1
    
    print(f"Generated {success_count} tickets in {output_path} (skipped {skip_count})")
    return output_path
```

After generating the JSONL file, upload it through Tidio's import interface. For API-based ticket creation (delta sync of new conversations), use the ticket endpoints directly.

### Step 8: Validate

```python
def validate_migration(reamaze_contacts, tidio_contacts):
    source_emails = {c["email"] for c in reamaze_contacts if c.get("email")}
    target_emails = {c["email"] for c in tidio_contacts if c.get("email")}
    
    missing = source_emails - target_emails
    extra = target_emails - source_emails
    duplicates = len(tidio_contacts) - len(target_emails)
    
    report = {
        "source_count": len(source_emails),
        "target_count": len(target_emails),
        "missing_count": len(missing),
        "extra_count": len(extra),
        "duplicate_count": duplicates,
        "missing_emails": list(missing)[:20],
    }
    
    print(f"Source contacts: {report['source_count']}")
    print(f"Target contacts: {report['target_count']}")
    print(f"Missing: {report['missing_count']}")
    print(f"Extra (not in source): {report['extra_count']}")
    print(f"Potential duplicates: {report['duplicate_count']}")
    
    if missing:
        print(f"Missing emails (first 20): {report['missing_emails']}")
    
    # Field-level sampling
    sample_size = min(50, len(reamaze_contacts))
    field_errors = validate_field_sample(reamaze_contacts[:sample_size])
    report["field_errors"] = field_errors
    
    return report

def validate_field_sample(source_contacts):
    """Spot-check field accuracy on a sample of migrated contacts."""
    errors = []
    for src in source_contacts:
        email = src.get("email")
        if not email:
            continue
        
        # Fetch from Tidio
        resp = requests.get(
            f"{TIDIO_BASE}/contacts",
            headers={
                "X-Tidio-Openapi-Client-Id": TIDIO_CLIENT_ID,
                "X-Tidio-Openapi-Client-Secret": TIDIO_CLIENT_SECRET,
            },
            params={"email": email}
        )
        if resp.status_code != 200:
            errors.append({"email": email, "error": f"HTTP {resp.status_code}"})
            continue
        
        data = resp.json().get("data", [])
        if not data:
            errors.append({"email": email, "error": "Not found in Tidio"})
            continue
        
        tidio_contact = data[0]
        name_parts = (src.get("name", "") or "").split(" ", 1)
        expected_first = name_parts[0] if name_parts else ""
        actual_first = tidio_contact.get("first_name", "")
        
        if expected_first and actual_first != expected_first:
            errors.append({
                "email": email,
                "error": f"Name mismatch: expected '{expected_first}', got '{actual_first}'"
            })
        
        time.sleep(1)
    
    return errors
```

> [!WARNING]
> Do not run extraction and loading simultaneously in a single loop. Decouple extraction from loading — write raw JSON extracts to disk first, then transform and load as separate steps. If the Tidio API goes down mid-load, you don't want to have to re-query Re:amaze. The staged approach also lets you inspect and validate transformed data before committing it.

## Edge Cases and Challenges

These are the issues that derail otherwise well-planned migrations.

### Duplicate Contacts

Tidio's `POST /contacts` endpoint does not check for existing records. Every call creates a new contact, even if the email already exists. You must build client-side deduplication: query existing Tidio contacts via `GET /contacts` first, index by email, and skip or update existing records using `PATCH /contacts/{contactId}`.

Re:amaze also supports profile merges and identity reassociation. If you ignore merged profiles during extraction, one Re:amaze contact can produce multiple Tidio contacts. Query the Re:amaze contact's `identities` array and consolidate before loading.

### Multi-Identity Contacts

Re:amaze contacts can have multiple identities — an email, a Twitter handle, a Facebook profile, and a phone number all linked to one contact record. Tidio contacts have flat fields: one email, one phone. Choose a primary identity (typically the email used in the most recent conversation) and store secondary identities in custom Contact Properties like `secondary_email`, `twitter_handle`, and `facebook_id`.

### Attachments

Re:amaze messages can contain file attachments. Tidio does not support direct file attachment uploads for historical imports via the API. The workaround: download attachments from Re:amaze, host them on external storage (S3, GCS, or any CDN with public URLs), and reference them via the `attachments.publicUrl` field in the JSONL importer. For API-based imports, embed download links directly in the ticket reply body HTML. See the attachment handling code in Step 5.

### Multi-Channel Threads

Re:amaze allows a conversation to switch from chat to email within the same thread. Tidio handles these as distinct channels. You may need to force historical multi-channel threads into a single ticket format to preserve readability. Add a `channel_switch:{channel_name}` tag or prepend `[Via: chat]` / `[Via: email]` to each message body to preserve channel context.

### Internal Notes vs. Public Messages

Re:amaze messages have a `visibility` field: `0` for regular (public) and `1` for internal notes. Tidio's ticket reply API does not currently expose an internal note / visibility parameter in its OpenAPI documentation. This means internal notes imported via the API will appear as public replies — potentially exposing internal discussions to customers.

**Mitigation strategies:**
1. **Exclude internal notes** from the migration entirely and archive them separately.
2. **Prefix internal notes** with a clear marker like `[INTERNAL NOTE - NOT VISIBLE TO CUSTOMER IN SOURCE SYSTEM]` in the message body.
3. **Store internal notes** in a separate JSONL file for internal reference, outside of Tidio.
4. **Test with a small batch first** — create 5 tickets with internal note messages and verify visibility in the customer-facing view.

### Timestamps

Messages created via the Tidio API carry the creation timestamp of the API call, not the original Re:amaze timestamp. If preserving original dates matters for compliance or reporting:

1. **Preferred:** Use the JSONL importer, which supports the `createdAt` field.
2. **Fallback:** Prepend the original timestamp to the message body: `[Original: 2024-01-15T10:30:00Z]`.
3. **Archival:** Keep the raw Re:amaze JSON extract as the compliance-ready source of truth.

### Conversation-Level Custom Attributes

Re:amaze supports custom data attributes on conversations. Tidio tickets have limited custom field support. Your options:
- Map to ticket tags (lossy, but searchable)
- Store in the ticket body as structured text
- Preserve the Re:amaze conversation `slug` in a Tidio tag (`reamaze_slug:{slug}`) for cross-reference
- Archive separately in external storage

### Common Tidio API Error Responses

| HTTP Status | Cause | Example Response | Resolution |
|---|---|---|---|
| 400 | Missing/invalid Contact Property | `{"error": "Validation failed", "details": [{"field": "properties.plan_type", "message": "Property 'plan_type' does not exist"}]}` | Pre-create the property in Tidio Settings |
| 400 | Invalid phone format | `{"error": "Validation failed", "details": [{"field": "phone", "message": "Invalid phone number format"}]}` | Normalize to E.164 before loading |
| 401 | Invalid API credentials | `{"error": "Unauthorized"}` | Verify Client-Id and Client-Secret |
| 429 | Rate limit exceeded | Headers include `Retry-After: 30` | Implement exponential backoff; read `Retry-After` header |
| 422 | Malformed request body | `{"error": "Unprocessable Entity"}` | Validate JSON structure; check for null values in required fields |

## Limitations and Constraints

Be honest with stakeholders about what this migration cannot preserve.

| What's Lost | Why |
|---|---|
| Knowledge base articles | Tidio has no KB import API or equivalent feature |
| Response templates | No Tidio import pathway; manual rebuild required |
| Satisfaction ratings (CSAT) | No Tidio import mechanism |
| Status page / incidents | Feature doesn't exist in Tidio |
| Agent/operator assignments | Operators API is read-only; manual setup required |
| Department structure | Departments API is read-only; manual setup required |
| Original message timestamps (API path) | API import uses current time; JSONL import preserves `createdAt` |
| Multi-identity contact links | Tidio supports single email + phone per contact |
| VoIP / video call history | No Tidio equivalent channel |
| Conversation-level automations | Must be rebuilt as Tidio Flows |
| Internal note visibility | Tidio reply API has no documented visibility flag |
| SLA policies and complex routing | Cannot be migrated programmatically; rebuild in Tidio's UI |

> [!NOTE]
> Tidio's OpenAPI is available exclusively on Plus and Premium plans. The Starter and Free plans do not have API access. Confirm your Tidio plan before investing in API-based migration development. ([developers.tidio.com](https://developers.tidio.com/docs/openapi-enable))

## Validation and Testing

Do not trust a 200 OK response blindly.

### Record Count Comparison

After migration, compare totals:
- Source contacts (Re:amaze) vs. target contacts (Tidio)
- Source conversations vs. target tickets
- Message count per conversation vs. reply count per ticket

### Field-Level Validation

Sample 5–10% of migrated records (minimum 50 records) and verify:
- Email, name, phone match the source
- Custom properties contain correct values and types
- Ticket subject lines match original conversation subjects
- Message body content is intact (check for encoding issues with HTML entities, special characters, and non-Latin scripts)
- Tags transferred correctly, including `legacy_status:*` tags
- Attachments are accessible via their new URLs

### UAT Process

1. Import a small batch (50–100 records) first, including edge cases: long threads (100+ messages), conversations with attachments, internal notes, merged contacts, and conversations in every status.
2. Have support agents review migrated tickets in Tidio's panel.
3. Verify that conversations are readable and linked to the correct contacts.
4. Confirm that internal notes are handled per your chosen strategy (excluded, prefixed, or archived).
5. Test search: can agents find historical tickets by tag, subject, and customer email?
6. Only proceed to full migration after UAT sign-off.

### Rollback Planning

Tidio does not have a bulk delete API. If the migration fails midway, you may need to manually clean up partial imports or request Tidio support to wipe imported data. Plan for this before you start — not after. Do not attempt to patch a broken migration on top of live data.

For a deeper validation checklist, see our [post-migration QA guide](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

## Post-Migration Tasks

Once data is validated, execute the operational cutover.

### Rebuild Automations: Flows vs. Workflows

Tidio has two distinct automation systems:

- **Flows** are the visual chatbot builder. They define customer-facing conversation trees with triggers (page visit, first message, specific keywords), conditions, and actions. Use Flows to replace Re:amaze's chatbot and auto-response rules. Flows can be exported and imported as JSON between Tidio projects. ([help.tidio.com](https://help.tidio.com/hc/en-us/articles/10147126351516-Transfer-flows-between-projects))
- **Workflows** are internal automation rules that operate on tickets and contacts: auto-assignment, tagging, status changes, and notifications based on ticket properties. Use Workflows to replace Re:amaze's routing rules, auto-tagging, and SLA-based escalations.

Map each Re:amaze automation rule to the appropriate Tidio system:

| Re:amaze Automation | Tidio Equivalent |
|---|---|
| Auto-responses to chat visitors | Flows (chatbot) |
| Intent-based routing | Flows (keyword triggers) → Workflows (assignment) |
| Auto-tagging by channel or content | Workflows |
| SLA reminders / escalations | Workflows |
| Welcome messages | Flows |
| Staff assignment rules | Workflows |

### Recreate Manual Assets

- Rebuild response templates as Tidio canned responses (manual process, no import API).
- Re:amaze FAQ articles can be converted to Tidio Lyro knowledge sources (CSV upload, PDF upload, or website URL scrape) or kept in an external help center. Tidio has no native KB comparable to Re:amaze's Articles feature.

### Update Routing and Channels

- Update DNS MX records and email forwarding rules to point from Re:amaze to Tidio.
- Reconfigure chat widget installations (replace Re:amaze embed snippet with Tidio's).
- Reconnect social channel integrations (Facebook Messenger, Instagram).
- Update any webhook URLs in third-party systems that pointed to Re:amaze.

### Train Your Team

Tidio's interface, terminology, and workflow differ from Re:amaze. Key areas to cover:

| Re:amaze Concept | Tidio Equivalent | Key Difference |
|---|---|---|
| Conversations | Conversations (live) + Tickets (async) | Tidio separates real-time chat from async tickets |
| Custom Attributes | Contact Properties | Must be pre-created; only 5 data types |
| Automation Rules | Workflows | Visual rule builder, not code |
| Chatbot | Flows | Visual flow editor with drag-and-drop |
| Channels | Departments | Read-only via API; manual setup |
| Brands | Projects | One Tidio project per site/brand |

### Monitor Post-Migration

Run daily spot checks for the first two weeks:
- Are new tickets being created correctly?
- Are contact properties populating as expected?
- Are duplicate contacts appearing? (Check daily contact count growth vs. expected new customer rate)
- Monitor API logs for delayed webhooks or sync failures for the first 48 hours.
- Verify Lyro AI Agent responses are contextually correct if using knowledge sources from the old KB.

## Best Practices

- **Back up everything first.** Run a full JSON export of your Re:amaze instance via API and store it in versioned, cold storage (S3 with versioning, GCS with object retention) before any destructive operations.
- **Run test migrations.** Migrate a small but representative sample (5% of data, minimum 50 records) into a Tidio sandbox. Include edge cases: attachments, internal notes, merged contacts, long threads, and every conversation status.
- **Pre-create all Contact Properties in Tidio.** Custom properties must exist in Tidio's Settings before you can assign them via API. Passing a non-existing property name results in a 400 validation error.
- **Build idempotent imports.** Track which records have been successfully imported (by source email/slug → target ID mapping in SQLite) so you can resume after failures without creating duplicates. The contact_map table in Step 6 is the minimal implementation.
- **Validate incrementally.** Validate data after the contact load, before moving on to the ticket load.
- **Log everything.** Every API call, response code, and payload should be logged. This is your audit trail and your debugging lifeline. Log to structured JSON for easy querying.
- **Respect rate limits aggressively.** Read the `Retry-After` header on 429 responses and implement exponential backoff (start at 2 seconds, double on each retry, cap at 60 seconds).
- **Keep source data immutable.** Never modify the raw extraction files. All transformations should produce new files, leaving the source JSON intact for re-processing.

For teams building in-house, structure your Python or Node.js application with these core modules:

1. **`extractor`**: Handles Re:amaze authentication, pagination, and per-brand scoping. Writes raw JSON to local disk, one file per entity type per brand.
2. **`transformer`**: Reads raw JSON, normalizes phones to E.164, splits names, maps statuses, strips unsupported HTML tags, and structures Tidio payloads (JSON for API, JSONL for import).
3. **`attachment_handler`**: Downloads Re:amaze attachments, re-hosts to S3/GCS, and returns public URLs.
4. **`loader`**: Handles Tidio API authentication, deduplication lookups, chunking/batching (100 records max), and exponential backoff for rate limits.
5. **`validator`**: Compares source and target counts, samples records for field-level checks, and generates a migration report.
6. **`logger`**: Records every successful ID mapping, flags any payload that returns a 400 or 500 error for manual review, and writes structured logs to a queryable format.

## Sample Field Mapping Table

| Re:amaze Field | Tidio Field | Type | Transformation |
|---|---|---|---|
| `contact.email` | `contact.email` | email | Direct map |
| `contact.name` | `contact.first_name` + `contact.last_name` | text | Split on first space; mononym → first_name only |
| `contact.mobile` | `contact.phone` | phone | Normalize to E.164 |
| `contact.data.{key}` | `contact.properties.{key}` | varies | Pre-create property; cast to Tidio type (text, email, number, phone, url) |
| `contact.identities [1+]` | `contact.properties.secondary_*` | text | Store non-primary identities as properties |
| `conversation.subject` | `ticket.subject` | text | Direct map; default to "(No subject)" if empty |
| `conversation.status` | `ticket.status` | enum | Map per status table; preserve lossy states as tags |
| `conversation.tag_list` | `ticket.tags` | array | Direct map; tags auto-created on first use |
| `conversation.category.name` | Department assignment | text | Map to pre-created Tidio department name |
| `conversation.slug` | `ticket.tags` (as `reamaze_slug:{slug}`) | text | Preserve for cross-reference |
| `message.body` | `ticket_reply.body` / JSONL `htmlContent` | html/text | Sanitize HTML; use JSONL for rich content |
| `message.visibility` | — | int | 0=public, 1=internal; no Tidio equivalent; exclude or prefix |
| `message.created_at` | JSONL `createdAt` / body text | datetime | Use JSONL importer; fallback: prepend to body |
| `message.user.email` | `contactEmail` / `operatorEmail` | email | Map to Tidio contact or operator based on `is_staff` flag |
| `message.attachments [].url` | `attachments [].publicUrl` | url | Re-host to S3/GCS; use public URL |

## When to Use a Managed Migration Service

Building an API migration pipeline from scratch typically requires 40–80 hours of engineering work for a mid-size dataset (5,000–20,000 contacts with conversation history) — based on our internal project tracking across similar helpdesk migrations. This estimate does not count debugging, validation iterations, and edge case handling, which can add another 20–40 hours.

The hidden costs aren't in the extraction code. They're in the three rounds of cleanup after go-live, the duplicate contacts discovered two weeks later, and the support tickets referencing conversations that didn't make it across.

Consider a managed service when:

- **Your engineering team is already at capacity.** Migration code is throwaway work that competes with product development.
- **You have complex custom attribute mappings** that require human judgment, not just field renaming.
- **Data integrity is non-negotiable.** A failed migration means lost customer history, broken ticket threads, or compliance gaps.
- **You're migrating multiple Re:amaze brands.** Re:amaze's brand-scoped API means multi-brand accounts require separate extraction runs per brand, cross-brand deduplication, and brand-to-project mapping — multiplying complexity.
- **You need attachment-preserving, timestamp-accurate migration** and don't want to build and maintain the S3 re-hosting and JSONL generation pipeline.

For related helpdesk migration guides, see our [Freshdesk to Tidio migration guide](https://clonepartner.com/blog/blog/freshdesk-to-tidio-migration-guide/), [Re:amaze to Zendesk guide](https://clonepartner.com/blog/blog/how-to-migrate-from-reamaze-to-zendesk-the-complete-technical-guide/), or [Zendesk to Tidio guide](https://clonepartner.com/blog/blog/zendesk-to-tidio-migration-guide/).

> Need help migrating from Re:amaze to Tidio? Our team handles the data extraction, transformation, and validation — with zero downtime and full audit trails. Book a 30-minute migration assessment.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I migrate conversation history from Re:amaze to Tidio?

Yes, but only via API extraction or JSONL ticket import — not CSV. Re:amaze's CSV export covers contacts only, not conversation threads. You need to extract conversations and messages from Re:amaze's REST API and recreate them as Tidio tickets with replies. For best timestamp and HTML fidelity, use Tidio's JSONL ticket importer rather than the API create/reply endpoints.

### What data is lost when migrating from Re:amaze to Tidio?

Knowledge base articles (Tidio has no KB import), response templates, satisfaction ratings, status page data, VoIP/video call history, original message timestamps (unless you use the JSONL importer), and multi-identity contact links (Tidio supports one email and one phone per contact). Agent assignments, departments, and automations must be manually rebuilt.

### Does Tidio deduplicate contacts on import?

No. Tidio's POST /contacts endpoint always creates new contacts regardless of whether the email already exists. You must build deduplication logic into your migration pipeline — query existing Tidio contacts first, index by email, and use PATCH to update existing records instead of creating duplicates. Tidio's CSV/TXT contact import, by contrast, can update existing contacts by email.

### How do I preserve original timestamps when migrating to Tidio?

Tidio's API create/reply endpoints stamp the import time as the creation time, not the original Re:amaze timestamp. To preserve original dates, use Tidio's JSONL ticket importer, which supports a createdAt field. As a fallback, prepend the original timestamp to the message body.

### Do I need Tidio Plus or Premium for an API-based migration?

Yes. Tidio's OpenAPI is available exclusively on Plus and Premium plans. Free and Starter plans do not have API access. Plus allows 60 requests/minute and Premium allows 120 requests/minute. Confirm your plan before investing in API-based migration development.
