Skip to content

Freshchat to Groove Migration: A Technical Guide

Technical guide to migrating from Freshchat to Groove: data model mapping, API constraints, chat-to-ticket transformation, rate limits, and step-by-step process.

Wahab Wahab · · 26 min read
Freshchat to Groove Migration: A 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

Freshchat to Groove Migration: A Technical Guide

Migrating from Freshchat to Groove means translating a conversation-centric messaging platform into a ticket-centric shared inbox. There is no native migration path, no vendor-provided connector, and no push-button import between these two systems. Every record — users, conversations, messages, agents, groups, attachments — requires extraction from Freshchat's REST API v2, structural transformation to match Groove's data model, and loading through Groove's REST v1 or GraphQL v2 API.

This is not a CSV copy. It is a schema translation project where the fundamental unit of data changes shape: Freshchat stores a stream of messages inside a conversation object; Groove stores a ticket with threaded replies. Groove's official migration documentation points customers to Import2 or the API, and Freshchat is not listed in Groove's documented Import2 source list — so a direct, vendor-endorsed Freshchat-to-Groove path does not currently exist.

This guide covers data model translation, API constraints, extraction strategies, and the specific engineering challenges of converting continuous chat streams into readable email-style tickets.

Why Teams Move from Freshchat to Groove

Freshchat is Freshworks' omnichannel messaging platform for customer engagement across web chat, WhatsApp, Facebook Messenger, Instagram, Apple Business Chat, and email. Its data model is conversation-first: every interaction is a continuous stream of individual messages, each with its own sender, timestamp, and message type. Freshchat sits within the broader Freshworks ecosystem (Freshdesk, Freshsales, Freshservice) and includes features like bots, IntelliAssign, and WhatsApp templates. Pricing starts at $19/agent/month (Growth plan) and rises to $79/agent/month (Enterprise), with additional costs for bot sessions and WhatsApp message credits.

Groove (GrooveHQ) is a lightweight shared inbox and help desk built for small-to-mid-sized teams. It organizes customer communications into tickets across email, live chat, social channels, and phone — with a Gmail-like interface that prioritizes simplicity over feature density. Pricing runs $16–$56/user/month across Standard, Plus, and Pro tiers.

Common reasons teams migrate:

  • Simplicity over complexity. Teams that primarily need email-based support with light chat find Freshchat's omnichannel feature set — bot builders, WhatsApp template management, IntelliAssign routing — to be unnecessary overhead. Groove's intentionally constrained feature set reduces configuration burden.
  • Cost reduction. A 10-agent team on Freshchat Enterprise ($79/agent/month) pays $790/month before bot session add-ons. The same team on Groove Plus ($32/user/month) pays $320/month — a 59% reduction.
  • Consolidation away from Freshworks. Teams leaving the Freshworks ecosystem entirely no longer benefit from Freshchat's tight integration with Freshsales or Freshdesk, making it a standalone cost without ecosystem value.
  • Preference for email-first support. Groove's shared inbox model mirrors email workflows. Teams whose support volume is 70%+ email find Groove's paradigm more natural than Freshchat's chat-first interface.

The technical challenge lies in the paradigm mismatch: dumping raw chat logs into a ticketing system creates an unreadable, fragmented mess. The transformation layer is where this migration succeeds or fails.

Data Model Differences: Freshchat vs. Groove

Understanding the structural mismatch is the single most important step before writing any migration code.

Concept Freshchat Groove
Core unit Conversation (stream of messages) Ticket (threaded replies)
Customer record User (/v2/users) Customer
Support staff Agent (/v2/agents) Agent
Routing Groups + Channels (Topics) Mailboxes + Folders + Groups
Message types normal, private, system, bot Customer reply, agent reply, note
Custom fields Conversation properties (cf_ prefix) Custom ticket fields, tags
Satisfaction CSAT (1–5 rating per conversation) Satisfaction ratings
Attachments Files/Images (S3 bucket, 25 MB limit) Attachments on tickets (25 per message, 20 MB per file)
Statuses new, assigned, resolved, reopened open, pending, closed (varies by API version)
API style REST v2 (Bearer token) REST v1 (access token) + GraphQL v2
Warning

Key structural mismatch: A single Freshchat conversation may contain dozens of messages from users, agents, bots, and system events. In Groove, this must become a single ticket with ordered replies. Bot interactions, carousel cards, quick-reply buttons, and other rich message parts have no equivalent in Groove's ticket model — they must be flattened to plain text or dropped.

Migration Approaches

API-Based Migration (Freshchat REST v2 → Groove REST v1 / GraphQL v2)

How it works: Build a custom script that extracts all data from Freshchat's v2 API endpoints, transforms it to match Groove's data model, then loads it through Groove's API.

When to use it: Any migration involving more than a few hundred conversations, or where you need full control over data mapping, timestamps, and message ordering.

  • Pros: Full control over mapping logic. Can preserve timestamps, relationships, and message ordering. Handles custom fields and conversation properties.
  • Cons: Requires development effort (Python/Node.js). Rate limits on both sides constrain throughput. Groove's REST v1 is no longer in active development, and the GraphQL v2 Inbox APIs are still being built.
  • Complexity: High

Native Export / Manual Import (CSV)

How it works: Export Freshchat data as CSV via POST /reports/raw (event/report oriented), flatten conversations into rows, and attempt to import into Groove.

When to use it: Very small datasets where you only need contact lists or low-fidelity conversation history for reference.

  • Pros: No code required for extraction. Low technical barrier.
  • Cons: Freshchat's CSV exports are flat transcript logs, not structured ticket data. Groove has no documented bulk CSV ticket import tool. Loses relationships between users, agents, and conversations. Cannot reconstruct message threads, attachments, or chronological agent replies.
  • Complexity: Medium (deceptively simple — the transformation work is significant)

Middleware / Integration Platforms (Zapier, Make)

How it works: Use Zapier or Make to connect Freshchat triggers to Groove actions, forwarding new conversations as they arrive. Make has Freshchat modules, and Groove has an official Zapier integration.

When to use it: For ongoing, real-time sync during a phased transition. Not suitable for historical data migration.

  • Pros: No-code setup. Good for bridging during a transition period.
  • Cons: Cannot migrate historical data. Limited field mapping. Per-task pricing gets expensive at volume (Make charges per operation; 10,000 conversations × ~5 operations each = 50,000 operations). Cannot handle complex message threading or message grouping.
  • Complexity: Low (for forward-only sync)

Third-Party Migration Services

How it works: Managed migration services handle extraction, transformation, and loading as a project.

When to use it: When engineering bandwidth is limited, data volume is high, or the migration is business-critical with no room for error.

  • Pros: Minimal internal engineering effort. Handles edge cases (duplicates, attachments, relationship mapping). Faster time-to-completion.
  • Cons: External cost. Less control over transformation logic (though good vendors provide visibility).
  • Complexity: Low (for your team)

Custom ETL Pipeline

How it works: Build a full extract-transform-load pipeline using tools like dlt, Apache Airflow, or custom scripts with staging databases.

When to use it: Enterprise-scale migrations with tens of thousands of conversations, complex business rules, or regulatory requirements.

  • Pros: Full audit trail. Replayable and testable. Can handle incremental/delta migrations.
  • Cons: Highest engineering investment. Requires infrastructure (staging DB, job scheduling).
  • Complexity: High

Approach Comparison

Method Historical Data Ongoing Sync Attachments Complexity Best For
API-based script Manual re-run High Most migrations
CSV export/import Partial Medium Contacts-only, tiny datasets
Zapier/Make Limited Low Transition bridging
Third-party service Varies Low (your team) Time-constrained teams
Custom ETL High Enterprise scale

Recommendations by Scenario

  • Small business, low engineering bandwidth: Third-party migration service or accept a fresh start, keeping Freshchat open as a read-only archive.
  • Small business, dev-capable team: API-based script.
  • Enterprise / high volume (>25K conversations): Custom ETL pipeline or managed service. The data transformation logic for chat-to-email mapping is too complex for basic tools.
  • Transition period: Zapier/Make for forward flow + API script or managed service for historical backfill.
  • Low engineering bandwidth: Do not attempt a custom build. The hidden costs of API rate limit management, schema mapping, and attachment handling will derail your sprint cycle.

Pre-Migration Planning

Start with a data audit, not a script.

Data Audit Checklist

Before extracting anything, inventory what you have in Freshchat:

  • Users (customers): Total count, custom properties, email coverage (what percentage have email addresses vs. anonymous?)
  • Agents: Active vs. deactivated, group memberships
  • Conversations: Total count by status (new, assigned, resolved, reopened)
  • Messages per conversation: Average depth, max depth (critical for estimating API call volume)
  • Groups and Channels (Topics): Mapping to Groove mailboxes/folders
  • Custom conversation properties: Field types, picklist values (only available for Freshchat accounts within the Freshsales Suite)
  • CSAT ratings: Volume. Note: Groove does not expose a programmatic CSAT import endpoint in its current API documentation — raw ratings may need to be stored as tags or custom fields rather than native satisfaction records.
  • Attachments and files: Total volume, file types, sizes
  • Bot interactions: Volume of bot-handled conversations (percentage of total)
  • Outbound WhatsApp messages: These have no Groove equivalent

Define Migration Scope

Not everything should move. Consider excluding:

  • Bot-only conversations with no human agent involvement
  • System messages (auto-assignment notifications, IntelliAssign events)
  • Resolved conversations older than X months if they have no business value
  • Outbound WhatsApp message templates (no Groove equivalent)

Filtering these out reduces API call volume — which directly impacts migration duration given Freshchat's rate limits — and keeps Groove tickets clean.

Migration Strategy

Strategy When to Use
Big bang Small dataset (<5K conversations), short cutover window acceptable
Phased Large dataset, migrate by date range or channel
Parallel run Risk-averse teams, run both platforms during transition

Freshchat's updated_from filters and message from_time pagination support incremental extraction, which enables phased and delta migration strategies.

Data Mapping: Freshchat Objects → Groove Objects

Core Object Mapping

Freshchat Object Freshchat Endpoint Groove Equivalent Notes
User /v2/users Customer Map email, name, phone. Custom properties → customer custom fields or tags
Agent /v2/agents Agent Must be manually created in Groove first; map by email
Conversation /v2/conversations/{id} Ticket One conversation = one ticket
Message (normal, actor_type=user) /v2/conversations/{id}/messages Customer reply Flatten message_parts to text
Message (normal, actor_type=agent) /v2/conversations/{id}/messages Agent reply Flatten message_parts to text
Message (private) /v2/conversations/{id}/messages Internal note
Message (system) /v2/conversations/{id}/messages Drop or append as note No direct equivalent
Group /v2/groups Group / Folder
Channel (Topic) /v2/channels Mailbox or Tag Depends on Groove structure
Conversation Property /conversations/fields Custom ticket field or Tag Limited by Groove's field types
CSAT Rating /csat/{id} Tag or custom field No confirmed programmatic CSAT import in Groove's current API
File/Image attachment Message parts Ticket attachment Download from S3 URL, re-upload to Groove

Status Mapping

Freshchat Status Groove Status
new Open
assigned Open (with assignee)
resolved Closed
reopened Open
Warning

Status mapping caveat: Groove's current help docs describe Open/Snoozed/Closed operational states, while the legacy REST v1 API exposes unread, opened, pending, closed, and spam as valid state values. These are two different status systems depending on which API version you target. Test your exact target account behavior before hard-coding status transforms — create a test ticket via API and verify the returned state values match your mapping.

Field-Level Mapping

Freshchat Field Type Groove Field Transformation
user.email string customer.email Direct map, lowercase + dedupe
user.first_name + user.last_name string customer.name Concatenate
user.phone string customer.phone Direct map
user.properties [] array Tags or custom fields Iterate and map
conversation.status enum ticket.state Value map (see above)
conversation.properties.priority enum Tag or custom field Groove's POST /v1/tickets does not accept a priority parameter; use tags (e.g., priority:high)
conversation.assigned_agent_id UUID ticket.assignee Resolve agent email → Groove agent ID
conversation.channel_id UUID ticket.mailbox or tag Resolve channel name → Groove mailbox
message.message_parts [].text.content string Reply body Concatenate all text parts
message.message_parts [].file object Attachment Download URL, re-upload
message.created_time UTC timestamp Reply timestamp Direct map

The Chat-to-Ticket Transformation Challenge

In Freshchat, a user might send five distinct messages in 30 seconds:

  1. "Hi"
  2. "I have a problem"
  3. "My order didn't arrive"
  4. "Order #12345"
  5. "Can you help?"

If you migrate these as five separate replies into a Groove ticket, the UI becomes cluttered and difficult to read — each message occupies its own reply card with header, timestamp, and avatar, turning a brief exchange into a visually overwhelming wall. Your transformation layer should group messages from the same sender that occur within a short time window (e.g., 2 minutes) into a single HTML text block before pushing to Groove. This preserves readability in an email-style interface.

Bot interactions present a separate challenge. Freshchat bot messages can contain carousels, quick-reply buttons, dropdowns, and text inputs — none of which have equivalents in Groove's plain text/HTML reply model. Flatten these to descriptive text (e.g., [Bot: presented options — Option A, Option B, Option C. User selected: Option B]) or drop them entirely. Document your choice so agents reviewing historical tickets understand what was lost.

Migration Architecture

The data flow follows a standard ETL pattern:

Freshchat API (v2)  →  Staging Layer  →  Groove API (v1 / GraphQL v2)
     Extract              Transform              Load

Never stream directly from Freshchat to Groove. Always extract to a staging database first (SQLite for small datasets, PostgreSQL for >10K conversations). This protects you against network failures, rate limits, and expiring attachment URLs, and it lets you re-run transformation logic without re-querying the source API.

Extract Phase: Freshchat API

Authentication: Bearer token in Authorization header. Generate from Admin > Configure > API Tokens in the Freshchat dashboard.

Base URL: https://<account>.freshchat.com/v2/

Key endpoints:

  • GET /v2/agents — all agents (paginated)
  • GET /v2/users — all users (filterable by created_from/created_to date range)
  • GET /v2/groups — all groups
  • GET /v2/channels — all channels/topics
  • GET /v2/conversations/{id} — single conversation metadata
  • GET /v2/conversations/{id}/messages — messages in a conversation (max 50 per page)
  • GET /v2/users/{user_id}/conversations — all conversation IDs for a user
  • POST /reports/raw — bulk report extraction (CSV)

Rate limits: Per-account, varies by plan. The Reports API has a documented limit of 100 calls per minute. Monitor X-RateLimit-Remaining and X-RateLimitReset response headers. HTTP 429 returned when exceeded. Implement exponential backoff starting at 1 second, doubling per retry, with a maximum of 5 retries.

Pagination: All list endpoints use page and items_per_page query parameters. Messages endpoint caps at 50 items per page. Follow links.next_page.href until the next_page key is absent from the response.

Info

Extraction bottleneck: There is no bulk "list all conversations" endpoint in Freshchat's public API. You must either: (a) iterate through users and call /v2/users/{user_id}/conversations for each, or (b) use the Reports API (POST /reports/raw with event Chat-Transcript) to get conversation IDs in bulk, then fetch details per conversation. The Reports API is limited to 24-hour windows for transcript data and 1-month windows for other event types, meaning a 1-year backfill requires at minimum 365 sequential API calls just for conversation discovery.

Estimated Extraction Duration

To help scope your project: extracting 10,000 conversations with an average of 15 messages each requires approximately:

  • Conversation discovery: 10,000 user-level queries (or ~365 Reports API calls for a 1-year window)
  • Message extraction: 10,000 conversations × ~1 page of messages each = 10,000 calls (more if conversations exceed 50 messages)
  • At Freshchat's 100-calls/minute rate limit: ~200 minutes (3.3 hours) for message extraction alone, assuming no 429 retries

Plan for 6–12 hours of wall-clock extraction time for a 10K-conversation dataset, including retries and attachment downloads.

Transform Phase

This is where most migrations break. Key transformations:

  1. Flatten message streams into ticket replies. Each Freshchat message becomes a Groove reply. Concatenate all message_parts [].text.content values into a single reply body. Drop or annotate rich parts (carousels, quick replies) as descriptive text.

  2. Group rapid sequential messages. Messages from the same sender within a configurable time window (recommended: 120 seconds) should be combined into a single reply. In testing, this typically reduces reply count per ticket by 30–50% for chat-heavy conversations.

  3. Resolve actor types. Map actor_type: "user" → customer reply, actor_type: "agent" → agent reply, actor_type: "bot" → note or drop, message_type: "private" → internal note.

  4. Download and stage attachments. Freshchat file URLs are pre-signed S3 links that expire (typically within hours to days depending on the signing configuration). Download all attachments during extraction and store locally or in cloud storage before loading into Groove. Verify that file MIME types are supported by Groove — standard image formats (JPEG, PNG, GIF), PDFs, and common document types are accepted, but test any unusual file types against Groove's upload endpoint.

  5. Build an agent lookup table. Freshchat uses UUIDs for agent IDs. Groove uses its own integer ID system. Build a mapping table keyed by agent email address. Extract all agents from both systems first and match by email.

  6. Map conversation properties to tags or custom fields. Freshchat custom properties use cf_ prefix naming. Map these to Groove tags or custom fields based on your Groove configuration. Note that cf_ properties are only available for Freshchat accounts within the Freshsales Suite; standalone Freshchat accounts use the standard properties array on user objects.

  7. Handle the restore_id deduplication problem. Freshchat's restore_id field is a client-side identifier used to associate anonymous sessions with authenticated users. When the same person starts multiple chat sessions before authenticating, Freshchat may create multiple user records with different restore_id values but the same email. Your deduplication logic must group by email, not by Freshchat user ID, and merge conversation histories from all matching user records into a single Groove customer.

Load Phase: Groove API

REST v1: Base URL https://api.groovehq.com/v1. Authentication via access token (Bearer header). JSON request/response bodies.

GraphQL v2: Offers more flexibility for contact and company operations. However, Inbox and Knowledge Base APIs are still being built — use REST v1 for ticket creation and message operations.

Key REST v1 endpoints:

  • POST /v1/tickets — create a ticket
  • POST /v1/tickets/{id}/messages — add a reply to a ticket
  • GET /v1/customers — list customers
  • POST /v1/customers — create a customer

Customer deduplication behavior: When you POST /v1/customers with an email that already exists in Groove, test whether the API returns the existing record or creates a duplicate. Groove's documentation does not explicitly state the behavior. In practice, implement a check-then-create pattern: GET /v1/customers?email={email} before creating, and use the existing record if found. This is critical for idempotent re-runs.

Attachment constraints: Groove limits attachments to 25 per message and 20 MB per file.

Rate limits: Groove's public documentation does not publish numeric rate limits. In empirical testing, sustained rates of approximately 5–10 requests per second are generally stable, but this varies by account and endpoint. Implement adaptive backoff: start with 200ms delays between requests, and on receiving any 429 response, back off exponentially starting at 2 seconds. Log all 429 responses with timestamps to establish your account's actual limit.

Warning

Groove API caveat: Groove's REST v1 API is no longer in active development — no new features or endpoints will be added. The GraphQL API (v2) is the recommended path forward, but its Inbox endpoints are still being built. Check the Groove developer documentation for current endpoint availability before starting development. Plan for the possibility that you may need to mix REST v1 (for tickets and messages) with GraphQL v2 (for contacts and companies).

Step-by-Step Migration Process

Step 1: Extract Reference Data

Pull agents, groups, and channels first. These are small datasets and serve as lookup tables for all subsequent operations.

import requests
import time
from datetime import datetime
 
FRESHCHAT_URL = "https://yourco.freshchat.com/v2"
HEADERS = {
    "Authorization": "Bearer YOUR_FRESHCHAT_API_KEY",
    "Accept": "application/json"
}
 
def fetch_all_pages(endpoint, key):
    results = []
    page = 1
    while True:
        resp = requests.get(
            f"{FRESHCHAT_URL}/{endpoint}",
            headers=HEADERS,
            params={"page": page, "items_per_page": 100}
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            print(f"Rate limited. Waiting {retry_after}s...")
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        data = resp.json()
        results.extend(data.get(key, []))
        if not data.get("links", {}).get("next_page"):
            break
        page += 1
    return results
 
agents = fetch_all_pages("agents", "agents")
groups = fetch_all_pages("groups", "groups")
channels = fetch_all_pages("channels", "channels")
 
# Build agent lookup: freshchat_id -> {email, name}
agent_lookup = {
    a["id"]: {"email": a.get("email"), "name": f"{a.get('first_name','')} {a.get('last_name','')}".strip()}
    for a in agents
}

Step 2: Extract Users and Conversations

# Extract users by date range
users = fetch_all_pages(
    "users?created_from=2020-01-01T00:00:00.000Z", "users"
)
 
# Deduplicate users by email (handles restore_id duplicates)
seen_emails = {}
for user in users:
    email = (user.get("email") or "").lower().strip()
    if email and email in seen_emails:
        # Merge conversation lists from duplicate user records
        seen_emails[email]["duplicate_ids"].append(user["id"])
    elif email:
        seen_emails[email] = {**user, "duplicate_ids": [user["id"]]}
    else:
        # Anonymous user — no email
        seen_emails[user["id"]] = {**user, "duplicate_ids": [user["id"]]}
 
# For each user (and duplicates), get conversation IDs
for key, user_data in seen_emails.items():
    all_conv_ids = []
    for uid in user_data["duplicate_ids"]:
        resp = requests.get(
            f"{FRESHCHAT_URL}/users/{uid}/conversations",
            headers=HEADERS
        )
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 60)))
            continue
        conv_ids = [c["id"] for c in resp.json().get("conversations", [])]
        all_conv_ids.extend(conv_ids)
    user_data["conversation_ids"] = list(set(all_conv_ids))

Step 3: Extract Messages and Attachments per Conversation

import os
 
def fetch_messages(conversation_id):
    messages = []
    page = 1
    while True:
        resp = requests.get(
            f"{FRESHCHAT_URL}/conversations/{conversation_id}/messages",
            headers=HEADERS,
            params={"page": page, "items_per_page": 50}
        )
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 60)))
            continue
        resp.raise_for_status()
        data = resp.json()
        messages.extend(data.get("messages", []))
        if not data.get("link"):
            break
        page += 1
    return sorted(messages, key=lambda m: m.get("created_time", ""))
 
def download_attachment(url, conversation_id, filename):
    """Download attachment from pre-signed S3 URL before it expires."""
    os.makedirs(f"attachments/{conversation_id}", exist_ok=True)
    filepath = f"attachments/{conversation_id}/{filename}"
    if os.path.exists(filepath):
        return filepath  # Idempotent: skip if already downloaded
    resp = requests.get(url, stream=True)
    resp.raise_for_status()
    with open(filepath, "wb") as f:
        for chunk in resp.iter_content(8192):
            f.write(chunk)
    return filepath

Download all file attachments from pre-signed S3 URLs immediately during extraction. These URLs expire — if you extract conversation data but delay loading by more than a few hours, the URLs will return 403 errors.

Step 4: Transform and Group Messages

from datetime import datetime, timezone
 
def parse_time(time_str):
    """Parse Freshchat UTC timestamp to datetime."""
    # Freshchat uses ISO 8601 format: 2024-01-15T10:30:00.000Z
    return datetime.fromisoformat(time_str.replace("Z", "+00:00"))
 
def flatten_message_parts(message):
    """Extract text from Freshchat message_parts."""
    parts = []
    for part in message.get("message_parts", []):
        if "text" in part:
            parts.append(part["text"]["content"])
        elif "file" in part:
            parts.append(f"[Attachment: {part['file'].get('name', 'file')}]")
        elif "image" in part:
            parts.append(f"[Image: {part['image'].get('url', '')}]")
        elif "quick_reply_options" in part:
            options = [o.get("label", "") for o in part.get("quick_reply_options", [])]
            parts.append(f"[Bot options: {', '.join(options)}]")
        elif "carousel" in part:
            cards = part["carousel"].get("cards", [])
            card_titles = [c.get("title", "Untitled") for c in cards]
            parts.append(f"[Bot carousel: {', '.join(card_titles)}]")
    return "\n".join(parts) if parts else "[No text content]"
 
def group_messages(messages, window_seconds=120):
    """Group rapid sequential messages from the same sender."""
    grouped = []
    buffer = None
    for msg in messages:
        if buffer and msg["actor_type"] == buffer["actor_type"]:
            time_diff = parse_time(msg["created_time"]) - parse_time(buffer["created_time"])
            if time_diff.total_seconds() <= window_seconds:
                buffer["body"] += "\n" + flatten_message_parts(msg)
                # Collect any attachments from grouped messages
                buffer["attachments"].extend(msg.get("_downloaded_attachments", []))
                continue
        if buffer:
            grouped.append(buffer)
        buffer = {
            "actor_type": msg["actor_type"],
            "actor_id": msg.get("actor_id"),
            "created_time": msg["created_time"],
            "body": flatten_message_parts(msg),
            "message_type": msg.get("message_type", "normal"),
            "attachments": list(msg.get("_downloaded_attachments", []))
        }
    if buffer:
        grouped.append(buffer)
    return grouped

Apply status mapping, resolve agent IDs to Groove agent IDs via your lookup table, and convert grouped message bodies to HTML (wrap in <p> tags, convert newlines to <br>).

Step 5: Load into Groove

Create customers first, then tickets, then replay messages in chronological order.

GROOVE_URL = "https://api.groovehq.com/v1"
GROOVE_HEADERS = {
    "Authorization": "Bearer YOUR_GROOVE_ACCESS_TOKEN",
    "Content-Type": "application/json"
}
 
STATUS_MAP = {
    "new": "opened",
    "assigned": "opened",
    "resolved": "closed",
    "reopened": "opened"
}
 
def find_or_create_groove_customer(email, name):
    """Check-then-create pattern to avoid duplicates."""
    resp = requests.get(
        f"{GROOVE_URL}/customers",
        headers=GROOVE_HEADERS,
        params={"email": email}
    )
    customers = resp.json().get("customers", [])
    if customers:
        return customers[0]
 
    resp = requests.post(
        f"{GROOVE_URL}/customers",
        headers=GROOVE_HEADERS,
        json={"email": email, "name": name}
    )
    return resp.json()
 
def create_groove_ticket(conversation, grouped_messages, agent_map, user_email):
    first_msg = grouped_messages[0] if grouped_messages else None
    body = first_msg["body"] if first_msg else ""
 
    # Generate subject from first message content
    subject_text = body.replace("\n", " ")[:80]
    if len(body) > 80:
        subject_text += "..."
 
    ticket_payload = {
        "body": body,
        "from": user_email,
        "to": "support@yourco.com",
        "subject": subject_text,
        "assignee": agent_map.get(conversation.get("assigned_agent_id")),
        "state": STATUS_MAP.get(conversation.get("status"), "opened"),
        "tags": [f"freshchat-import", f"channel:{conversation.get('channel_name', 'unknown')}"]
    }
 
    resp = requests.post(
        f"{GROOVE_URL}/tickets",
        headers=GROOVE_HEADERS,
        json=ticket_payload
    )
    if resp.status_code == 429:
        time.sleep(2)
        return create_groove_ticket(conversation, grouped_messages, agent_map, user_email)
 
    ticket = resp.json()
    ticket_number = ticket.get("ticket", {}).get("number") or ticket.get("number")
 
    # Add subsequent messages as replies in chronological order
    for msg in grouped_messages[1:]:
        msg_payload = {
            "body": msg["body"],
            "note": msg["message_type"] == "private"
        }
        requests.post(
            f"{GROOVE_URL}/tickets/{ticket_number}/messages",
            headers=GROOVE_HEADERS,
            json=msg_payload
        )
        time.sleep(0.2)  # Throttle to stay under rate limits
 
    return ticket

Timestamp preservation: When creating tickets with historical data, Groove's REST v1 POST /v1/tickets does not document a timestamp override parameter in its public API reference. Test whether passing created_at or sent_at in the request body is honored. If not, tickets will be stamped with the migration date. As a workaround, prepend the original timestamp to the ticket body: [Originally sent: 2024-01-15T10:30:00Z].

Step 6: Upload Attachments

Upload temporarily stored files to Groove and associate them with the respective messages. If any conversation exceeds Groove's 25-attachments-per-message limit, split across multiple replies. Track upload success/failure per file in your migration log.

Step 7: Validate

Run validation checks before going live (see Validation section below).

Edge Cases and Challenges

Anonymous Users

Freshchat allows chats without email addresses. Groove requires an email to create a customer. Generate placeholder emails (e.g., anon-{freshchat_user_id}@yourdomain.com) and tag these customers with anonymous-freshchat-user to make them searchable later. Append the Freshchat user ID as a custom field to maintain traceability.

Duplicate Users

Freshchat can have multiple user records for the same person — this occurs when different restore_id values are generated across sessions before the user authenticates. The same email may appear across 2–5 user records. Deduplicate by lowercase email before creating Groove customers, and merge all conversation histories from duplicate records into the single Groove customer.

Expired Attachment URLs

Freshchat serves file attachments via pre-signed S3 URLs that expire. Download all attachments during extraction and store them locally or in cloud storage (S3, GCS). If you extract conversation data but delay loading by more than a few hours, the URLs will return 403 Forbidden errors and the files will be unrecoverable without re-extracting from the API.

Inline Images

Freshchat messages often contain inline base64 images or S3 links within HTML content. Your script must detect <img> tags in message body HTML, download the source file (handling both data: URIs and HTTPS URLs), upload it to Groove as an attachment, and rewrite the src attribute in the HTML payload to reference the Groove-hosted URL.

Multi-Interaction Conversations

A single Freshchat conversation can be resolved and reopened multiple times, generating new interaction_id values each time. All interactions belong to the same conversation_id. In Groove, this maps to a single ticket — preserve the full message history chronologically. Optionally, insert a separator note (e.g., [Conversation reopened: 2024-03-15T14:00:00Z]) between interaction boundaries for readability.

Private Notes vs. Bot Private Notes

Freshchat distinguishes between agent private notes (message_type: "private") and bot private notes (botsPrivateNote: true). Both should become Groove internal notes, but bot private notes are typically data-collection artifacts (e.g., "User selected language: English") and may not be worth migrating. If you include them, prefix with [Bot Note] to differentiate from agent notes.

Bot Conversations

Decide upfront whether to migrate interactions handled entirely by Freshchat bots. In many accounts, 30–60% of conversations are bot-only. Filtering these out can halve your API call volume and significantly reduce clutter in Groove.

Deactivated Agents

Conversations assigned to now-deactivated Freshchat agents need a fallback owner in Groove. Options: map to a default "Migration Import" agent account, or set to unassigned. Document the mapping in your migration log so agents can investigate if needed.

WhatsApp Outbound Messages

Freshchat's outbound WhatsApp message templates (/v2/outbound-messages/whatsapp) have no Groove equivalent. These cannot be migrated. If WhatsApp conversation history is business-critical, archive it separately (e.g., export via Reports API and store as CSV).

Freshchat Contacts API

Freshchat accounts within the Freshworks Suite (Freshsales + Freshchat) may expose customer data through /v2/contacts rather than /v2/users. If your account uses the Freshsales Suite, check both endpoints — contacts may contain richer profile data including custom properties, lifecycle stage, and deal associations that are not present on the users endpoint.

Limitations and Constraints

Groove-Side Limitations

  • No true custom objects. Groove's data model is flat: tickets, customers, agents. If you rely on Freshchat's conversation properties heavily, you'll need to map them to tags or limited custom fields.
  • GraphQL v2 Inbox APIs are incomplete. Groove recommends REST v1 for Inbox operations, but REST v1 is no longer in active development. No new features or endpoints will be added.
  • Simplified reporting. Groove's reporting is more limited than Freshchat's granular metrics API. Historical volume trends, first-response-time distributions, and agent performance breakdowns may not be reproducible.
  • Rate limits not published. Public Groove docs do not publish numeric rate limits. Empirically, 5–10 requests/second appears stable, but implement adaptive backoff from the start.
  • Attachment limits. 25 attachments per message, 20 MB per file.
  • No programmatic CSAT import. Groove's current API does not expose an endpoint for importing historical satisfaction ratings. Store them as tags or custom fields.

Freshchat-Side Limitations

  • No bulk conversation list endpoint. You must discover conversations via user-level queries or the Reports API.
  • Rate limits are plan-dependent. The Reports API documents 100 calls per minute. Other endpoint limits vary by plan and are not fully documented. Monitor X-RateLimit-Remaining headers.
  • Reports API time windows. Chat-Transcript reports are limited to 24-hour extraction windows. For a 1-year backfill, this means a minimum of 365 sequential API calls just for conversation discovery.
  • Messages pagination caps at 50 per page. For conversations with hundreds of messages, expect many paginated calls per conversation.

Data That Cannot Be Migrated

  • Bot flows and automation rules (must be rebuilt manually in Groove)
  • IntelliAssign / Omniroute configuration
  • WhatsApp message templates
  • CSAT survey configuration (only raw ratings can move, as tags/custom fields)
  • Agent availability/status history
  • Dashboard metric history and historical reporting data

Validation and Testing

Never migrate directly to production. Always run a test migration on a separate Groove account first. Groove does not offer a sandbox environment, so create a dedicated test account for trial runs.

Record Count Comparison

Object Freshchat Count (Source) Groove Count (Target) Delta Acceptable?
Users / Customers 0% delta
Agents Manual — verify all mapped
Conversations / Tickets 0% for in-scope conversations
Total Messages / Replies <5% acceptable (grouped messages reduce count)
Attachments 0% delta

A delta in messages/replies is expected due to message grouping. Any delta in customers, tickets, or attachments means records were dropped. Investigate before going live.

Sampling Strategy

Pull 50 records — intentionally pick edge cases:

  • Oldest conversation in the system
  • Conversation with the most messages (test pagination completeness)
  • Conversation with attachments (verify files are accessible)
  • Conversation that was resolved and reopened multiple times (verify chronological integrity)
  • Conversation with bot interactions (verify flattening logic)
  • Conversation assigned to a now-deactivated agent (verify fallback assignment)
  • Conversation from an anonymous user (verify placeholder email)

For each, verify: correct customer association, correct agent assignment, message ordering matches source, all attachments are downloadable, status is correctly mapped, and any custom field values are present.

Agent UAT

Have actual support agents (not just admins or engineers) run scripted checks:

  1. Find a specific migrated ticket by customer email
  2. Verify message chronology matches the original Freshchat conversation
  3. Open each attachment and confirm it renders correctly
  4. Confirm the customer profile shows correct contact information
  5. Reply to the migrated ticket and verify the thread is not broken

Rollback Planning

Groove does not support bulk delete via API. If the migration produces unacceptable results:

  1. Best option: Use a separate test Groove account for all trial runs (strongly recommended — eliminates rollback risk entirely)
  2. Contact Groove support for a data reset on your production account. Response time varies; plan for 1–3 business days.
  3. API deletion: Delete tickets one by one via DELETE /v1/tickets/{number}. At ~5 requests/second, deleting 10,000 tickets takes approximately 33 minutes.

Post-Migration Tasks

Once the final delta sync completes and your support channels are pointed to Groove:

  • Rebuild automations. Freshchat assignment rules, auto-resolve rules, and bot flows do not migrate. Recreate equivalent rules in Groove's automation builder. Common mappings: IntelliAssign → Groove round-robin assignment; auto-resolve after X hours → Groove auto-close rules.
  • Configure Groove integrations. Connect Slack, Shopify, Stripe, Salesforce, or other tools your team relies on. Verify webhooks are pointing to Groove, not Freshchat.
  • Update customer-facing widgets. Swap the Freshchat widget JavaScript snippet for Groove's support widget on your website. Update any mobile SDK references.
  • Train your team. Groove's interface differs from Freshchat. Key differences agents should understand: folders replace channels/topics, tags replace conversation properties, and internal notes replace private messages.
  • Monitor for data inconsistencies. Watch for customer complaints about missing history or misattributed conversations in the first two weeks. Set up a #migration-issues Slack channel for agents to report problems.
  • Keep Freshchat in read-only mode. Do not decommission Freshchat until you've confirmed all data is validated in Groove. Keep it as a read-only archive for 30–90 days as a safety net. Freshchat does not have a native "read-only" mode; instead, disable all channels and remove the widget from your site.

Best Practices

  1. Decouple the pipeline. Never stream directly from Freshchat to Groove. Extract to a staging database first. This protects you against network failures, rate limits, and expiring S3 URLs, and enables re-runs without re-querying the source.
  2. Log everything. Persist every source ID, target ID, HTTP status code, attempt count, and last error. Maintain a strict mapping table of freshchat_conversation_id → groove_ticket_number. This is essential for delta syncs, troubleshooting, and post-migration audits.
  3. Run at least two test migrations before the production run. The first will expose mapping bugs; the second validates your fixes.
  4. Validate incrementally — check the first 100 tickets as they load, don't wait until all 10,000 are complete to start checking.
  5. Throttle API calls conservatively. Stay well under rate limits, especially on Freshchat's side. A 429 lockout mid-migration wastes hours and complicates checkpointing.
  6. Map agents before anything else. Agent resolution errors cascade into every ticket — a missing agent mapping means every ticket assigned to that agent will fail or be unassigned.
  7. Handle attachments in a separate pass. Download all files during extraction, store locally with a naming convention that maps back to conversation/message IDs, then upload during loading. Never stream directly from Freshchat S3 URLs to Groove — the URLs will expire mid-migration.
  8. Build idempotent scripts. Use your freshchat_conversation_id → groove_ticket_number mapping table to skip already-migrated records on re-run. If reruns create duplicates, the script is not finished. Checkpoint every page and every ticket so reruns resume cleanly.
  9. Run a delta sync. Perform the bulk migration over several days. On the day of cutover, run a delta sync using Freshchat's updated_from filter to catch only conversations created or updated during the migration window.
  10. Tag all migrated tickets. Apply a consistent tag (e.g., freshchat-import) to every migrated ticket. This makes it easy to identify, filter, and if necessary bulk-delete migrated records.

Sample Data Mapping Table

Freshchat Field Groove Field Transformation
user.id customer custom field (legacy_freshchat_user_id) Direct copy for traceability
user.email customer.email Lowercase + dedupe across restore_id duplicates
first_name + last_name customer.name Concatenate with space separator
conversation.id Ticket tag or custom field (legacy_freshchat_conversation_id) Direct copy for traceability
conversation.status ticket.state Mapped per status matrix above
conversation.assigned_agent_id ticket.assignee Agent lookup by email; fallback to unassigned
channel_id / channel name Mailbox or tag Routing choice per your Groove structure
message.actor_type Reply vs. note user → customer reply, agent → agent reply, system/bot → note or drop
message.message_parts [].text.content message.body Concatenate parts, group by 120-second time window
message.created_time message.sent_at or body prefix Preserve chronology; test API timestamp acceptance
Attachments Attachments on message Download during extraction, re-upload during load (split if >25/message)
conversation.properties.cf_* Tags or custom fields Map per property; only available in Freshsales Suite accounts
CSAT rating Tag (e.g., csat:4) No native CSAT import in Groove API

Frequently Asked Questions

Does Groove have a native Freshchat importer?
No. Groove's documented migration path points customers to Import2 or the API, and Freshchat is not listed in Groove's documented Import2 source list. You must extract data via Freshchat's REST API v2, transform conversations into Groove's ticket format, and load through Groove's REST v1 or GraphQL v2 API — using custom scripts, third-party services, or a managed migration provider.
How long does a Freshchat to Groove migration take?
It depends on data volume and API rate limits. A small account with under 5,000 conversations can be migrated in 2–5 days including testing. Larger accounts with 20,000+ conversations, heavy attachments, and complex bot interactions may take 1–3 weeks due to rate limits and the per-conversation message extraction required by Freshchat's API.
How do you handle chat messages migrating into an email ticketing system?
Build a transformation layer that groups rapid, sequential chat messages from the same sender within a time window (e.g., 2 minutes) into a single consolidated HTML block before pushing to Groove. Without grouping, a 30-second chat exchange becomes five separate ticket replies, cluttering the Groove UI.
What data can't be migrated from Freshchat to Groove?
Bot flows, automation rules, IntelliAssign configurations, WhatsApp outbound message templates, CSAT survey settings (raw ratings can move, configuration cannot), and agent availability history cannot be migrated. Rich bot message parts like carousels and quick-reply buttons must be flattened to text or dropped.
What happens to Freshchat attachments during the migration?
Attachments must be downloaded during extraction because Freshchat serves files via pre-signed S3 URLs that expire. Store them locally or in cloud storage, then upload to Groove's API. Groove limits attachments to 25 per message and 20 MB per file, so large threads may need attachments split across multiple replies.

More from our Blog

Help Desk Data Migration Playbook: What Data to Move and What to Leave Behind
Help Desk

Help Desk Data Migration Playbook: What Data to Move and What to Leave Behind

This definitive playbook answers the single most critical question: "What data do we actually need to move?". This strategic guide helps you declutter and decide what's precious and what's junk. We provide a clear breakdown of the non-negotiable Tier 1 data, like tickets , knowledge bases , and user profiles, versus the Tier 2 data that provides rich context, like automations and organizations. Use this as your strategic checklist to avoid common mistakes and ensure a flawless, functional new help desk.

Raaj Raaj · · 7 min read