Skip to content

Re:amaze to Tidio Migration: The Technical Guide

Technical guide to migrating from Re:amaze to Tidio. Covers data model mapping, API limits, JSONL import, edge cases, and step-by-step process.

Roopi Roopi · · 30 min read
Re:amaze to Tidio Migration: The Technical Guide
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Re:amaze to Tidio Migration: The Technical Guide

Migrating from Re:amaze to Tidio 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)
  • 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)
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) You export, clean the file, and use Tidio's POST /contacts endpoint or CSV/TXT import to create contacts. (help.tidio.com)

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)

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)

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)

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:

{
  "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)

Danger

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.

Info

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)

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:

{
  "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

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

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

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.

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

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

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

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
Info

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)

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.

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)
  • 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, Re:amaze to Zendesk guide, or Zendesk to Tidio guide.

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.

More from our Blog