---
title: "Tidio to Groove Migration: A Technical Guide"
slug: tidio-to-groove-migration-a-technical-guide
date: 2026-08-11
author: Rishabh
categories: [Tidio, Groove, Migration Guide]
excerpt: "Technical guide to migrating from Tidio to Groove. Covers API constraints, data model mapping, chat sessionization, rate limits, edge cases, and cutover planning."
tldr: "No native Tidio-to-Groove importer exists. The migration requires API extraction (gated to Tidio's $749/mo Plus plan), schema translation from contact-centric to ticket-centric, and careful handling of Tidio's 60 req/min rate limit."
canonical: https://clonepartner.com/blog/tidio-to-groove-migration-a-technical-guide/
---

# Tidio to Groove Migration: A Technical Guide


# Tidio to Groove Migration: A Technical Guide

Migrating from Tidio to Groove means converting a **conversation-and-contact-centric platform** into a **ticket-centric shared inbox**. There is no native migration path, no vendor-provided import tool, and no push-button connector between these two systems. Groove's official migration flow points customers to [Import2 or the API](https://help.groovehq.com/help/how-to-migrate-to-groove-and-import-your-tickets), and Tidio is not on Groove's published Import2 source list. Every record — contacts, operators, conversations, messages, tickets, tags, attachments — must be extracted from the [Tidio OpenAPI](https://developers.tidio.com/reference), transformed to match Groove's data model, and loaded through Groove's API.

This is a schema translation project. Tidio stores data around the **Contact** object, with conversations and tickets as child entities. Groove stores everything around the **Ticket** (conversation) object with messages threaded inside it. Tidio's chat history is a continuous stream of messages, bot interactions, and system events. Groove expects discrete tickets with subjects, threaded replies, and clear state transitions.

This guide covers the full data model mapping, API constraints on both sides, extraction strategy, loading sequence, idempotency handling, and the specific edge cases that surface during this migration.

> [!NOTE]
> **Short answer:** There is no documented native Tidio-to-Groove importer. Budget for an ETL project, not a CSV upload.

## Why Teams Move from Tidio to Groove

**Tidio** is a customer service and conversational AI platform built for SMBs and e-commerce. It merges live chat, chatbots (Flows), AI agents (Lyro), and a Help Desk ticketing system into one dashboard. Channels include web chat widget, email, Facebook Messenger, Instagram, and WhatsApp. Pricing is conversation-based: Starter at $29/mo, Growth at $59/mo, Plus from $749/mo, and Premium from ~$2,999/mo. All self-serve plans cap at 10 operator seats.

**Groove** (GrooveHQ) is a shared inbox and helpdesk for small-to-mid-sized teams that prefer simplicity over enterprise complexity. It combines email, live chat, social channels, and a knowledge base into a unified agent workspace with collision detection, automations, SLA tracking, and reporting. Groove uses per-user pricing: Standard at ~$20/user/month, Plus at ~$45/user/month, and Pro at ~$70/user/month.

Common reasons for the switch:

- **Pricing predictability.** Tidio's three-meter billing model (billable conversations, Lyro AI conversations, Flow triggers) makes costs hard to forecast. Groove's flat per-user pricing is simpler.
- **Ticket-centric workflow.** Teams handling primarily email-based support find Groove's shared inbox model more natural than Tidio's chat-first interface.
- **Avoiding the $59-to-$749 cliff.** Tidio jumps from Growth ($59/mo) directly to Plus ($749/mo) with no mid-tier option — a 12× price increase that pushes growing teams elsewhere.
- **API access costs.** Tidio's full OpenAPI access requires the Plus plan ($749/mo minimum). Groove includes API access on all paid plans.

## Data Model Mapping: Tidio → Groove

The foundational difference: Tidio organizes data around **Contacts** (visitors who've interacted with the widget), while Groove organizes data around **Tickets** (conversations in a shared inbox).

| Tidio Object | Groove Object | API Surface | Notes |
|---|---|---|---|
| Contacts | Customers | REST `/v1/customers` or GraphQL `createContact` | Match on email; Groove auto-creates customers on ticket creation |
| Operators | Agents | Manual or API | Agents must exist before ticket assignment |
| Departments | Groups | REST or GraphQL | Tidio departments are operator teams; Groove separates team structure (`groups`) from routing channels (`mailboxes`) |
| Conversations (chat messages) | Tickets + Messages | REST `/v1/tickets` + `/v1/tickets/{id}/messages` or GraphQL | Each Tidio conversation becomes one Groove ticket with threaded replies |
| Tickets (Help Desk) | Tickets + Messages | Same as above | More direct mapping; preserve status and assignee |
| Tags | Tags | REST or GraphQL | Tidio uses `tag_ids`; Groove expects tag names. Build a tag dictionary before loading. |
| Contact Properties (custom) | Custom fields, tags, or notes | Varies | Groove's custom field support is limited; some properties may become tags or structured notes |
| Attachments | Attachments | REST uploader or GraphQL | Must be downloaded from Tidio and re-uploaded to Groove |
| Viewed Pages | Not supported | — | Groove has no page-visit tracking; archive externally or drop |
| Flows / Bots | Not supported | — | Groove has basic automation rules, not visual flow builders |
| Lyro AI data | Not supported | — | No migration path for AI training data |

A few mapping details that trip people up:

- **Status mapping is not 1:1.** Tidio tickets support `open`, `pending`, and `solved`. Groove supports `unread`, `opened`, `pending`, `closed`, and `spam`. In most projects: `solved → closed`, `pending → pending`, `open → opened`. Consider setting the state to `unread` when the last historical message came from the customer.
- **Tags need translation, not passthrough.** Tidio ticket APIs use `tag_ids`; Groove expects tag names when creating or updating tags on a ticket. Build a tag dictionary during extraction.
- **Custom fields may need a second pass.** Tidio supports contact properties and ticket custom fields. Groove supports conversation custom fields in the product and GraphQL schema, but the published REST v1 ticket-create docs don't expose conversation custom-field parameters. The safer path: create the ticket first, then update conversation custom fields via Groove's GraphQL `customFieldValuesUpdate` mutation.
- **Tidio has two inboxes.** Tidio's **live chat inbox** and **Help Desk ticketing system** are [separate modules with different API endpoints](https://clonepartner.com/blog/blog/livechat-to-tidio-migration-the-technical-guide/). Chat messages come from `/contacts/{contactId}/messages`; tickets come from `/tickets`. Make sure your extraction covers both.

> [!WARNING]
> **What you will lose:** Tidio's Viewed Pages history, Flow/bot configurations, Lyro AI training data, pre-chat survey metadata, conversation ratings, and WhatsApp/Instagram/Messenger channel configs have no Groove equivalents. Archive these externally or deliberately drop them. Be explicit with stakeholders about what's lost before the project starts.

## Tidio API: Extraction Constraints

The [Tidio OpenAPI](https://developers.tidio.com/reference) is your extraction path.

### Authentication

Tidio uses a paired header approach. Every request requires two headers:

```
X-Tidio-Openapi-Client-Id: <your-client-id>
X-Tidio-Openapi-Client-Secret: <your-client-secret>
```

Credentials are generated in the Tidio Panel under **Settings → Developer → OpenAPI**.

### Plan Gating

This is the most common blocker teams hit. Tidio's OpenAPI endpoints (outside the Product Recommendation area) are available only on the **Plus plan ($749/mo) and above**. Free, Starter ($29/mo), and Growth ($59/mo) tiers have no access to the endpoints needed for migration. If you're on a lower-tier plan, you'll need to upgrade — even temporarily — to extract your data via API.

### Rate Limits

Tidio enforces rate limits per project:

- **Plus plan:** 60 requests per minute
- **Premium plan:** 120 requests per minute

The API returns `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers on every response. Build your extraction script to read these headers and throttle proactively — don't just catch 429s reactively. Every response also includes a `trace-id` header, worth storing for support cases.

A Tidio 429 response looks like this:

```json
{
  "status": 429,
  "message": "Too Many Requests",
  "retryAfter": 30
}
```

The `Retry-After` header accompanies the 429 and specifies the wait in seconds. Store the `trace-id` from every failed response — Tidio support requires it for rate-limit investigations.

At 60 req/min on Plus, a dataset with 10,000 contacts and 2,000 tickets needs at least 12,000 detail requests just for per-contact chat history and per-ticket message bodies — before retries, attachments, or delta sync. That's roughly 200 minutes of source-side API time at best.

### Pagination

Tidio uses **cursor-based pagination**. The response includes a cursor value; pass it in the next request's `cursor` query parameter. Omit the parameter for the first page. There's no documented page-size override — you get whatever the API returns per page and iterate until no cursor is returned.

### Delta Sync Filtering

Tidio's `/tickets` endpoint accepts `created_from` and `created_to` query parameters (ISO 8601 timestamps) for filtering by creation date. Use these for delta sync: record the timestamp when your initial extraction starts, then re-run with `created_from={extraction_start_timestamp}` to capture new records created during the migration window.

For chat messages via `/contacts/{contactId}/messages`, there is no server-side timestamp filter — you must pull all messages per contact and filter client-side by comparing against your recorded extraction timestamp. This means delta sync for chat history is O(contacts), not O(new messages). For large contact volumes, minimize the delta window to reduce redundant pulls.

> [!TIP]
> **CSV alternative for contacts:** Tidio allows CSV export of contacts from the Contacts List UI (Settings → Customers). This export includes only the properties currently displayed in your contacts list. If you need a quick contacts-only dump and don't need conversation history, the CSV export can supplement the API approach — but it won't include messages.

### Key Extraction Endpoints

| Endpoint | Method | Purpose |
|---|---|---|
| `/contacts` | GET | List all contacts (paginated by cursor) |
| `/contacts/{contactId}` | GET | Single contact with properties |
| `/contacts/{contactId}/messages` | GET | All messages for a contact (chat history) |
| `/contacts/{contactId}/viewed-pages` | GET | Page visit history (no Groove equivalent) |
| `/contact-properties` | GET | Custom property definitions |
| `/operators` | GET | List all operators |
| `/departments` | GET | List departments |
| `/tickets` | GET | List all Help Desk tickets (without messages); supports `created_from`, `created_to` filters |
| `/tickets/{ticketId}` | GET | Single ticket with details and messages |
| `/tickets/tags` | GET | All ticket tags |
| `/tickets/custom-fields` | GET | Custom field definitions |

Note that Tidio's ticket list endpoint returns tickets without messages — you need a second call per ticket to pull the full thread.

## Groove API: Loading Constraints

Groove offers two APIs. Understanding the state of each matters for your migration.

### REST v1 API

The REST API base URL is `https://api.groovehq.com/v1`. Authentication uses a Bearer access token generated from **Settings → Developer → API**. This API is marked as no longer in active development, but it remains functional and is the more battle-tested option for ticket imports.

Key capabilities for migration:

- Backdated timestamps via `sent_at` on both tickets and messages
- Private notes via `note: true`
- `skip_notifications: true` to suppress agent email alerts during import
- `skip_unread_ticket: true` to prevent historical replies from reopening queue state
- Pagination at a maximum of 50 records per page

Groove's REST API returns an empty response body for most error codes. The exception is **422 Unprocessable Entity**, which returns field-level JSON:

```json
{
  "errors": {
    "body": ["can't be blank"],
    "mailbox": ["is not valid"]
  }
}
```

All other error codes (400, 401, 403, 404, 500) return empty bodies. This means your loader must log the full request payload alongside the HTTP status — there is no server-provided error message to log for most failures.

### GraphQL v2 API

The GraphQL endpoint is `https://api.groovehq.com/v2/graphql`, also using a Bearer API key. Key mutations for migration:

```graphql
# Create a conversation (ticket)
mutation CreateConversation($input: CreateConversationInput!) {
  createConversation(input: $input) {
    conversation {
      id
      number
    }
    errors {
      message
      path
    }
  }
}

# Add a reply to an existing conversation
mutation AddReply($input: AddReplyInput!) {
  addReply(input: $input) {
    message {
      id
    }
    errors {
      message
    }
  }
}

# Update conversation custom field values
mutation UpdateCustomFields($input: CustomFieldValuesUpdateInput!) {
  customFieldValuesUpdate(input: $input) {
    customFieldValues {
      id
      value
    }
    errors {
      message
    }
  }
}

# Create a contact
mutation CreateContact($input: CreateContactInput!) {
  createContact(input: $input) {
    contact {
      id
      email
    }
    errors {
      message
    }
  }
}
```

GraphQL errors surface in the `errors` array on the mutation response, not as HTTP error codes — your error handling must check both the HTTP status and the response body's `errors` field. A 200 response can contain a failed mutation.

The GraphQL API covers conversations, messages, agents, contacts, mailboxes, tags, and knowledge base articles. However, some areas are still being built, so verify endpoint coverage against current documentation before committing to GraphQL exclusively.

### Rate Limits

Groove's API limits by plan: **200 calls/min on Standard**, **400 calls/min on Plus**, and **800 calls/min on Pro**. The source platform (Tidio) is almost always the extraction bottleneck, not Groove.

### Groove-Specific Constraints

- **No custom object system.** Groove is a flat shared inbox. Structured data in Tidio custom properties must be flattened into tags, custom fields, or notes.
- **Ticket numbers are auto-generated.** You cannot set or preserve Tidio ticket IDs. Store a mapping table (`tidio_ticket_id → groove_ticket_number`) for post-migration auditing.
- **Mailbox vs. inbox terminology.** Groove's UI refers to "inboxes" but the REST API object is `mailbox`.
- **Groove does not expose an `imported` flag or conversation source field** on ticket records. Use a dedicated import tag (e.g., `tidio-import`) on every migrated ticket to distinguish imported records from organic conversations in reporting and validation.

> [!CAUTION]
> Set `skip_notifications: true` and keep customer email delivery off during historical replay. The fastest way to turn a clean migration into an incident is to email customers from years-old imported threads.

## Idempotency: Handling Crashes and Reruns

This is the most operationally dangerous gap in DIY migrations. If your migration script crashes mid-run and you restart it without idempotency logic, you will create duplicate tickets in Groove. Groove's ticket-create endpoint has no native deduplication — it creates a new ticket on every POST.

**The solution: a local state database.**

Before loading any record, write its Tidio ID and status (`pending`) to a local SQLite or Redis store. After successful creation in Groove, update the record with the Groove ticket number and status (`complete`). On rerun, skip any record with status `complete`.

```python
import sqlite3

def init_state_db(db_path="migration_state.db"):
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS ticket_map (
            tidio_id TEXT PRIMARY KEY,
            groove_ticket_number TEXT,
            status TEXT DEFAULT 'pending',
            error TEXT,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)
    conn.commit()
    return conn

def is_already_migrated(conn, tidio_id):
    row = conn.execute(
        "SELECT status FROM ticket_map WHERE tidio_id = ? AND status = 'complete'",
        (tidio_id,)
    ).fetchone()
    return row is not None

def record_success(conn, tidio_id, groove_ticket_number):
    conn.execute(
        "INSERT OR REPLACE INTO ticket_map (tidio_id, groove_ticket_number, status) VALUES (?, ?, 'complete')",
        (tidio_id, groove_ticket_number)
    )
    conn.commit()

def record_failure(conn, tidio_id, error_message):
    conn.execute(
        "INSERT OR REPLACE INTO ticket_map (tidio_id, groove_ticket_number, status, error) VALUES (?, NULL, 'failed', ?)",
        (tidio_id, error_message)
    )
    conn.commit()
```

Apply the same pattern for contacts and messages. For messages, the composite key is `(tidio_ticket_id, message_index)` or `(tidio_ticket_id, message_timestamp, sender_id)` — whatever uniquely identifies a message in the source data.

This state table also becomes your reconciliation artifact post-migration: every Tidio ID maps to a Groove ticket number, with a status and error log for failures.

## Step-by-Step Migration Process

### Step 1: Audit and Define Scope

Before extracting anything, decide what moves:

- **Must migrate:** Contacts with email addresses, active/recent tickets, associated messages and replies, operator mappings, tags
- **Should migrate:** Closed/archived tickets with historical value, custom contact properties
- **Cannot migrate:** Flows, Lyro training data, Viewed Pages, pre-chat survey configs, widget customizations
- **Consider dropping:** Bot-generated conversations with no human interaction, spam contacts, anonymous visitor sessions with no email

Reducing scope directly reduces migration time and cost. At 60 requests/minute, every unnecessary API call adds up.

### Step 2: Extract from Tidio

Extract in dependency order:

1. **Operators** (`GET /operators`) — You need operator IDs to map conversation assignments later.
2. **Departments** (`GET /departments`) — Map these to Groove groups.
3. **Contact properties** (`GET /contact-properties`) — Understand your custom field schema before extracting contacts.
4. **Ticket tags** (`GET /tickets/tags`) and **ticket custom fields** (`GET /tickets/custom-fields`) — Capture field definitions.
5. **Contacts** (`GET /contacts`, paginated) — For each contact, fetch messages (`GET /contacts/{contactId}/messages`) for chat history.
6. **Tickets** (`GET /tickets`, paginated) — For each ticket, fetch details (`GET /tickets/{ticketId}`) for replies and metadata.

Store everything as JSON files on disk. Don't stream directly from Tidio to Groove — if anything fails mid-run, you want to resume from your local cache, not re-extract against those rate limits.

```python
import requests
import time
import json

TIDIO_BASE = "https://api.tidio.com"
HEADERS = {
    "X-Tidio-Openapi-Client-Id": "YOUR_CLIENT_ID",
    "X-Tidio-Openapi-Client-Secret": "YOUR_CLIENT_SECRET"
}

def extract_paginated(endpoint, output_file):
    results = []
    cursor = None
    while True:
        params = {}
        if cursor:
            params["cursor"] = cursor
        resp = requests.get(f"{TIDIO_BASE}{endpoint}", headers=HEADERS, params=params)
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 60))
            time.sleep(wait)
            continue
        resp.raise_for_status()
        data = resp.json()
        results.extend(data.get("data", []))
        cursor = data.get("cursor")
        if not cursor:
            break
        # Respect rate limits proactively
        remaining = int(resp.headers.get("x-ratelimit-remaining", 1))
        if remaining < 5:
            time.sleep(2)
    with open(output_file, "w") as f:
        json.dump(results, f)
    return results
```

### Step 3: Create Target Structure in Groove

Before loading any conversations, set up Groove:

1. **Create agents** — Every Tidio operator assigned to a ticket must exist as a Groove agent. Groove's own [migration guide](https://help.groovehq.com/help/how-to-migrate-to-groove-and-import-your-tickets) warns that users should be set up before tickets are imported, or tickets can collapse onto one user.
2. **Create mailboxes** — Map Tidio channels to Groove mailboxes. One primary support channel typically means one Groove mailbox. Example REST call to verify mailboxes:
   ```
   GET https://api.groovehq.com/v1/mailboxes
   Authorization: Bearer {token}
   ```
   The response returns mailbox slugs (e.g., `"slug": "support"`) — use the slug, not the display name, when assigning tickets.
3. **Create groups** — Map Tidio departments to Groove groups.
4. **Create tags** — Extract all tags from Tidio and pre-create them in Groove. This avoids tag-creation race conditions during parallel ticket loading.
5. **Set up custom fields** — If you need conversation-level custom fields, create them before loading. Note their field IDs from the Groove GraphQL schema — you'll need those IDs for the `customFieldValuesUpdate` mutation.

For operators who have left your company and you don't want to consume a Groove seat license: map their historical Tidio messages to a generic "Legacy Agent" in Groove, or provision them temporarily, run the migration, and immediately suspend the account.

### Step 4: Handle Anonymous Contacts

Tidio allows visitors to initiate chats without providing an email address. They're tracked by `visitor_id` or browser session. Groove's Customer object requires an email address (a common target constraint also detailed in our [Tidio to Enchant migration guide](https://clonepartner.com/blog/blog/tidio-to-enchant-migration-guide/)) — you cannot create a customer with just a name or phone number.

Your options:

1. **Generate placeholder emails** using the visitor ID (e.g., `visitor_{id}@anonymous.local`). This preserves the data in Groove but can pollute CRM, automations, and reporting if you don't isolate these records.
2. **Drop anonymous conversations** if they have no historical value.
3. **Archive externally** and keep them out of Groove entirely.

```javascript
// Deterministic placeholder email for anonymous Tidio contacts
function formatGrooveCustomer(tidioContact) {
  const email = tidioContact.email || `visitor_${tidioContact.id}@anonymous.local`;
  return {
    email: email,
    name: tidioContact.name || "Anonymous Visitor",
    about: `Imported from Tidio. Original ID: ${tidioContact.id}`
  };
}
```

**Contact deduplication before load**

Tidio's create-contact behavior always adds a new contact and never overwrites on matching email or `distinct_id`. Years of chatbot activity frequently produce duplicate contacts for the same email address. Before loading into Groove, deduplicate your extracted contact set using the following approach:

1. **Normalize emails:** lowercase, strip whitespace, resolve common aliasing (e.g., `+` suffixes if your domain uses them).
2. **Group by normalized email.** For duplicate groups, select the contact with the most recent `last_seen` timestamp as canonical, and merge all conversation and ticket references to that canonical ID.
3. **For contacts without email:** treat each `visitor_id` as a distinct identity — do not attempt to merge anonymous contacts.
4. **Log all merged IDs** in your state database so the post-migration mapping table remains accurate.

Skipping this step causes Groove history to fragment across multiple customer records for the same person.

### Step 5: Transform Chat Conversations into Tickets

This is where the paradigm shift hits hardest. Tidio chat history is a flat stream of messages per contact. Groove expects discrete tickets with subjects and threaded replies.

**Sessionization: Reconstructing ticket boundaries from chat streams**

For Tidio Help Desk tickets, the boundary already exists — each ticket is a discrete unit. For chat message history (retrieved via `/contacts/{contactId}/messages`), you need rules to split the stream into ticket-sized chunks. Common split points:

- A solved/closed event
- A long inactivity gap (e.g., 30+ minutes with no messages)
- A department change
- A first operator reply after a previously solved state

Keep the rule set simple and document it. Aggressive auto-merging makes validation nearly impossible. If a chat never had a subject, generate one deterministically from channel plus date plus contact name — not from a random message snippet.

**Message grouping for readability**

In Tidio, a user might send five separate messages in ten seconds:

1. "Hi"
2. "I have a problem"
3. "With my order"
4. "Order #12345"
5. "It hasn't arrived"

If you migrate these as five separate replies on a Groove ticket, the UI becomes cluttered. Groove is designed for paragraph-length email replies, not rapid-fire chat bubbles. Two approaches:

1. **Group consecutive messages** from the same sender within a time window (e.g., 5 minutes) into a single reply.
2. **Compile the entire transcript** into a single HTML body for the Groove ticket. This is often the best approach for historical chat data — it preserves readability without generating hundreds of thousands of individual message records.

```json
{
  "body": "<strong>Chat transcript imported from Tidio</strong><br><br><strong>User (10:01 AM):</strong> Hi<br><strong>User (10:01 AM):</strong> I have a problem with my order #12345<br><strong>Agent (10:05 AM):</strong> I can help with that. Let me check the status.",
  "from": { "email": "customer@example.com", "name": "Jane Doe" },
  "to": "support@yourcompany.com",
  "mailbox": "support",
  "subject": "Chat - 2024-04-09 - Jane Doe",
  "sent_at": "Tue, 09 Apr 2024 10:01:00 +0000",
  "state": "closed",
  "assignee": "agent@yourcompany.com",
  "tags": ["tidio-import"],
  "skip_notifications": true
}
```

**Filtering bot-only conversations**

If you used Tidio's Lyro AI or decision-tree bots, your data likely contains abandoned bot flows — "Welcome! How can I help you?" followed by the user leaving. Migrating these to Groove wastes API calls and pollutes your inbox. Filter out conversations with zero messages from a human operator and zero meaningful user input.

**Preserving public vs. internal messages**

Tidio ticket payloads distinguish `message_type: public` and `message_type: internal`. Groove uses normal messages vs `note: true`. Preserve this split or your agents will lose internal context.

### Step 6: Re-host Attachments

Attachments cannot be passed as URL references. Tidio attachment URLs may require authentication or expire after your Tidio account is closed. You must physically move the files.

For each attachment:

1. **Download** the file binary from the Tidio attachment URL.
2. **Check size** — Groove limits attachments to [20 MB per file and 25 files per message](https://doc.groovehq.com/attachments). Files exceeding the limit should be uploaded to external storage (e.g., S3) with the link inserted into the message body instead.
3. **Upload** to Groove's attachment endpoint.
4. **Associate** the returned attachment ID with the specific message payload.

Log failures. If an attachment can't be transferred, annotate the Groove message body or add a private note documenting the missing file.

### Step 7: Load into Groove

Use a queuing system (BullMQ for Node.js, Celery for Python) to manage API calls. This gives you automatic retries, concurrency control, and detailed logging. A simple `for` loop will not handle rate limiting, retries, or partial failures gracefully.

When creating historical tickets:

- Set `state` to `closed` (or appropriate historical state) to prevent triggering Groove's auto-responders or SLA alerts
- Set `skip_notifications: true` to suppress agent email alerts
- Set `skip_unread_ticket: true` on historical replies to prevent them from reopening queue state
- Keep `send_copy_to_customer` off unless you intentionally want customers emailed from imported history
- Apply the `tidio-import` tag on every ticket so imported records are identifiable in reporting and validation queries

```json
{
  "body": "Internal note migrated from Tidio",
  "author": "agent@yourcompany.com",
  "sent_at": "Tue, 09 Apr 2024 10:20:00 +0000",
  "note": true,
  "skip_notifications": true,
  "skip_unread_ticket": true
}
```

**Logging requirements for every API call:**

Store the following for each record attempt: Tidio source ID, Groove response HTTP status, Groove returned ticket number (on success), full request payload, full response body, Tidio `trace-id` header (extraction), and wall-clock timestamp. For Groove REST errors other than 422, the response body is empty — the request payload log is your only diagnostic artifact.

### Step 8: Validate

After loading, run automated validation — not just manual spot checks. For migrations over 1,000 tickets, manual review alone is insufficient.

**Automated checks:**

- **Count verification:** Compare total contacts from Tidio vs. customers in Groove. Compare total conversations + tickets vs. Groove conversations. Flag any delta greater than 1% for investigation.
- **Message counts:** Spot-check 20–30 tickets. Compare message counts and ordering between source and target.
- **Assignment integrity:** Verify tickets are assigned to the correct Groove agents.
- **Tag mapping:** Confirm all tags transferred correctly. Query Groove's tag list and diff against Tidio's extracted tag set.
- **Attachments:** Click through attachments in Groove to verify files aren't corrupted.
- **Timestamps:** Verify the oldest and newest tickets have correct dates, not the import date.
- **Import tag coverage:** Confirm 100% of imported tickets carry the `tidio-import` tag. Any ticket missing it may have been created organically during the migration window — investigate before assuming it's a valid import.

**Edge case spot-checks — verify these specifically:**

- Long threads (50+ messages)
- Internal notes (confirm `note: true` records appear as notes, not public replies)
- Multi-attachment tickets
- Anonymous visitor records (confirm placeholder emails are consistent)
- Records that changed departments mid-conversation
- Duplicate-contact cases (confirm merged records appear under one Groove customer)

**Use Groove's webhook stream for real-time validation (optional):**

Subscribe to Groove's `conversation_created` webhook during the load phase. Each event returns the conversation number and mailbox. Stream these into a counter and compare against your expected load count in near-real-time. This catches load failures faster than a post-hoc count query.

### Step 9: Delta Sync and Cutover

During the initial migration, your team is still using Tidio. New conversations and tickets are being created.

1. Record the ISO 8601 timestamp when your initial extraction started (e.g., `2024-04-09T00:00:00Z`).
2. After the initial load completes, re-extract Tidio tickets using `created_from=2024-04-09T00:00:00Z` on the `/tickets` endpoint.
3. For chat messages (which lack server-side timestamp filtering): re-pull `/contacts/{contactId}/messages` for any contact whose `last_seen` value is newer than the extraction start timestamp. Filter messages client-side by timestamp.
4. Load the delta into Groove. Your idempotency state database will skip any ticket already marked `complete` — only net-new and updated records load.
5. **Cutover:** Switch your live chat widget, email forwarding, and channel integrations from Tidio to Groove. Update DNS/MX records if applicable.
6. Run one final delta sync to catch stragglers created between the delta load and cutover.

Tidio publishes ticket and conversation webhooks (`ticket.created`, `ticket.replied`, `conversation.operator_replied`, and solve events), which makes short delta windows possible. One caveat: Tidio notes that changes made by the OpenAPI **do not** trigger webhooks, so don't expect webhook echo from API-driven test writes.

## Edge Cases That Break DIY Migrations

- **Duplicate source contacts.** Tidio's create-contact behavior always adds a new contact and never overwrites on matching email or `distinct_id`. If your Tidio tenant has years of chatbot or integration activity, you may have duplicate contacts. Dedupe before import or Groove history will fragment across multiple customers.
- **Rich messages and cards.** Tidio supports interactive bot messages (carousels, buttons, quick replies). These have no Groove equivalent. Convert them to plain-text or HTML representations in the message body.
- **Conversation-level custom fields via REST.** Groove supports conversation custom fields in the product and GraphQL schema, but the REST v1 ticket-create docs don't expose them. Plan a second GraphQL pass using `customFieldValuesUpdate` or flatten values into tags and notes.
- **GraphQL errors on 200 responses.** Groove's GraphQL endpoint returns HTTP 200 even for failed mutations. Always check the `errors` array in the response body — a successful HTTP status does not mean the record was created.
- **Source throttling beats target parallelism.** Tidio tops out at 60–120 req/min. Groove allows 200–800 req/min depending on plan. Throwing more workers at the loader doesn't fix a slow extractor.
- **Weak error visibility on Groove REST.** Groove REST errors other than 422 return empty response bodies. Tidio returns a `trace-id` header on every response. Store both platform IDs, HTTP statuses, request payloads, and trace IDs for every failed record.
- **No idempotency on Groove ticket creation.** Every POST to `/v1/tickets` creates a new ticket regardless of payload content. Without a local state database tracking completed records, a script crash creates duplicates on rerun.

> [!TIP]
> **Dry run strategy:** Migrate a small but ugly sample first — one anonymous chat, one ticket with internal notes, one thread with attachments, one record that changed departments, and one duplicate-contact case. That sample reveals more than a 100-record happy-path batch.

## What Cannot Be Migrated

| Tidio Feature | Why It Can't Move |
|---|---|
| Flows (chatbot automation) | Groove uses basic rules, not visual flow builders |
| Lyro AI training data | Proprietary to Tidio's AI engine |
| Viewed Pages history | No equivalent tracking in Groove |
| Pre-chat survey config | Groove uses its own widget config |
| Conversation ratings | No standardized import path |
| WhatsApp/Instagram/Messenger channel config | Must be reconfigured in Groove natively |
| Widget appearance customization | Groove has its own widget system |

## Migration Timeline Estimates

| Volume | Estimated Time | Notes |
|---|---|---|
| < 1,000 contacts, < 5,000 messages | 1–2 days | Extraction is the bottleneck at 60 req/min |
| 1,000–10,000 contacts | 3–5 days | Includes transform, load, and validation |
| 10,000+ contacts with full history | 1–2 weeks | Tidio rate limiting is the primary constraint |

The biggest variable is Tidio's extraction rate limit. At 60 req/min on Plus, extracting 10,000 contacts plus their messages requires thousands of API calls that take hours even with perfect throttling. Budget additional time if your contact set includes significant anonymous visitor volume — those contacts require client-side deduplication and decision logic that adds processing overhead.

## Choosing Your Migration Approach

**DIY scripting** works if your team has an engineer comfortable with API integrations and you have fewer than a few thousand contacts. The Tidio and Groove APIs are documented well enough for a competent developer to build a migration pipeline in a few days.

**Third-party tools** like Help Desk Migration (helpdeskmigration.com) advertise Tidio support, but verify they cover both chat conversations *and* Help Desk tickets, and check whether they support Groove as a target. Confirm the tool handles anonymous contacts, attachment re-hosting, and idempotency before committing.

**Managed migration** makes sense when volume is high, when you need zero downtime, or when the engineering team shouldn't be pulled off product work for a one-time infrastructure task.

For related migrations involving either platform, see our guides on [Freshchat to Groove](https://clonepartner.com/blog/blog/freshchat-to-groove-migration-a-technical-guide/), [Tidio to Zendesk](https://clonepartner.com/blog/blog/tidio-to-zendesk-migration-guide/), and [Kustomer to Groove](https://clonepartner.com/blog/blog/kustomer-to-groove-migration-the-complete-technical-guide/).

## Technical Checklist

Before you start, make sure you've addressed every item:

- [ ] Confirm Tidio plan has OpenAPI access (Plus or Premium)
- [ ] Generate Tidio API credentials (Client ID + Secret)
- [ ] Generate Groove API token
- [ ] Define migration scope (which contacts, conversations, tickets)
- [ ] Decide handling strategy for anonymous visitors
- [ ] Decide whether to migrate bot-only conversations
- [ ] Map Tidio departments → Groove groups
- [ ] Map Tidio operators → Groove agents (create accounts)
- [ ] Create mailboxes in Groove; record mailbox slugs for ticket assignment
- [ ] Pre-create tags in Groove
- [ ] Record Groove custom field IDs for `customFieldValuesUpdate` mutations
- [ ] Initialize local idempotency state database before first load run
- [ ] Build extraction script with rate limit handling and `trace-id` logging
- [ ] Build deduplication step for contacts (normalize emails, merge by `last_seen`)
- [ ] Build transform layer (sessionization, message grouping, schema mapping)
- [ ] Build load script with error handling, retry logic, and `skip_notifications`
- [ ] Apply `tidio-import` tag on all migrated tickets
- [ ] Build validation script (count checks, spot checks, timestamp verification, import tag coverage)
- [ ] Plan delta sync window: record extraction start timestamp, use `created_from` filter on `/tickets`, client-side filter on chat messages
- [ ] Plan cutover procedure (widget swap, email forwarding, DNS/MX)
- [ ] Run full pipeline on ugly sample set before running at scale
- [ ] Archive Tidio data externally (full JSON dump) as a safety net

> Migrating from Tidio to Groove and want to get it right the first time? Our engineers will review your setup, map the data model, and handle the entire migration — including delta sync and validation. Book a free 30-minute call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Does Groove have a native Tidio importer?

No. Groove's documented Import2 source list does not include Tidio, so Tidio-to-Groove migrations require API-based scripting, a third-party migration tool that supports both platforms, or a managed migration service.

### What Tidio plan do I need to access the API for migration?

Tidio's OpenAPI endpoints for contacts, conversations, and tickets are available only on the Plus plan ($749/mo) and Premium plan (~$2,999/mo). Lower-tier plans do not have backend API access for data extraction.

### What data is lost when migrating from Tidio to Groove?

Tidio Flows (chatbot automation), Lyro AI training data, Viewed Pages history, pre-chat survey configurations, conversation ratings, and channel configurations (WhatsApp, Instagram, Messenger) have no equivalents in Groove and cannot be migrated.

### How do I handle anonymous Tidio visitors in Groove?

Groove requires an email address for all Customer records. For anonymous Tidio visitors, you can generate deterministic placeholder emails (e.g., visitor_{id}@anonymous.local), drop the conversations, or archive them externally. Placeholder emails work technically but can pollute CRM and reporting if not isolated.

### How long does a Tidio to Groove migration take?

For under 1,000 contacts, expect 1–2 days. For 1,000–10,000 contacts with conversation history, plan for 3–5 days. For 10,000+ contacts with full history, expect 1–2 weeks. The primary bottleneck is Tidio's 60 requests-per-minute rate limit on the Plus plan.
