---
title: "Groove to Gorgias Migration: A Technical Guide"
slug: groove-to-gorgias-migration-a-technical-guide
date: 2026-08-13
author: Rishabh
categories: [Groove, Gorgias, Migration Guide]
excerpt: "How to migrate from Groove to Gorgias — API constraints, data mapping, attachment handling, and the step-by-step process to avoid data loss."
tldr: "Groove to Gorgias has no native import path. Extract via Groove JSON export, transform the data model, and import via the Gorgias REST API — plan 1–4 weeks depending on volume."
canonical: https://clonepartner.com/blog/groove-to-gorgias-migration-a-technical-guide/
---

# Groove to Gorgias Migration: A Technical Guide


# Groove to Gorgias Migration: A Technical Guide

> [!NOTE]
> **TL;DR:** A Groove to Gorgias migration requires an API-led approach because Gorgias offers no native Groove importer. The primary engineering challenges are mapping Groove's multi-state ticket model to Gorgias's binary open/closed system, handling Groove's authenticated attachments, and managing Gorgias's strict API rate limits (40 requests per 20 seconds on API key auth, ~120 requests/minute effective throughput). At realistic throughput of 30–60 tickets per minute, a 10,000-ticket migration takes 3–6 hours of continuous import time; a 50,000-ticket migration takes multiple days. Plan 1–4 weeks total depending on volume, attachments, and data complexity.

A **Groove to Gorgias migration** moves conversation history, customer records, tags, agent assignments, and attachments from Groove's flat shared inbox into Gorgias's e-commerce-native helpdesk. There is no native import path — Gorgias offers a first-party historical importer for Zendesk and an email-history import for connected Gmail/Outlook addresses, but Groove is not on either list. Every Groove-to-Gorgias migration is API-driven, export-driven, or handled by a third-party tool. ([docs.gorgias.com](https://docs.gorgias.com/en-US/import-zendesk-data-into-gorgias-214412))

The core data maps reasonably well. Both platforms are conversation-based inboxes with tags, assignments, and customer records. The hard parts are Groove's transitional API state (REST v1 is deprecated, GraphQL v2 is incomplete for Inbox data), Gorgias's tight API rate limits, and the status-model mismatch between the two systems. This guide covers the real constraints on both sides, field-level data mapping, every realistic migration method, and the edge cases that cause silent data loss.

For getting data out of Groove, start with [How to Export Data from Groove](https://clonepartner.com/blog/blog/how-to-export-data-from-groove-methods-api-limits-portability/). If you're evaluating Gorgias migrations from other source platforms, [Zendesk to Gorgias Migration: The Complete Technical Guide](https://clonepartner.com/blog/blog/zendesk-to-gorgias-migration-the-complete-technical-guide/) covers the native importer in depth. If you are still evaluating target platforms, see our [Groove to Zendesk Migration Guide](https://clonepartner.com/blog/blog/groove-to-zendesk-migration-data-mapping-apis-rate-limits/). For target-side rollback planning, see [How to Export Data from Gorgias](https://clonepartner.com/blog/blog/how-to-export-data-from-gorgias-methods-api-limits-formats/).

> [!WARNING]
> **Scope check:** This guide covers **GrooveHQ shared inbox data moving into Gorgias**. Knowledge Base articles, widget configurations, and CRM-specific entities must be migrated separately — Gorgias does not accept KB imports from Groove via the ticket API.

## Why Teams Move from Groove to Gorgias

The switch is almost always driven by e-commerce needs that Groove was not built for:

- **Native Shopify integration.** Gorgias surfaces order data, refund actions, and shipping status inside the ticket sidebar. Agents can issue refunds, duplicate orders, and edit shipping without leaving the conversation. Groove has a Shopify integration, but it does not offer the same depth of in-ticket actions.
- **Ticket-based pricing with unlimited agents.** Gorgias charges by ticket volume, not by seat. For teams scaling headcount quickly, this shifts costs from per-agent to per-interaction.
- **AI Agent for autonomous resolution.** Gorgias's AI Agent can auto-resolve tickets end-to-end on Shopify stores. Groove's AI features cover summarization and writing suggestions but not autonomous resolution.
- **Multi-channel consolidation.** Gorgias natively handles email, chat, social (Instagram, Facebook, TikTok Shop), SMS, and voice in a single queue. Groove covers email, live chat, and social but lacks native TikTok Shop and SMS support.

If you don't sell on Shopify or a supported e-commerce platform, Gorgias's deepest features — AI Agent, order sidebar, refund actions — are gated to Shopify stores. Evaluate accordingly.

## Groove Data Extraction Constraints

Groove offers two production-ready extraction paths, with a third in progress. Both working options have real limitations.

### Groove JSON Export

Groove provides a full data export via **Settings → Company → More → Exports**. The export produces a GZIP-compressed JSON file following the v1 Tickets API "full" conversations format. It includes ticket metadata (status, assignee, tags), all messages in each conversation thread, and customer contact information. ([help.groovehq.com](https://help.groovehq.com/help/export-your-groove-tickets))

The JSON export is the recommended method for bulk historical migration because it bypasses API rate limits entirely. Only one export request can be queued at a time, and large exports can take up to 72 hours to complete.

> [!NOTE]
> **Download links expire quickly.** Groove generates short-lived download URLs for security. Trigger the export, wait for completion, and download immediately — the link will be dead if you come back hours later.

### Groove REST API (v1)

The REST API at `https://api.groovehq.com/v1` is no longer in active development but remains functional. It exposes tickets, messages, customers, agents, mailboxes, folders, groups, and attachments. Authentication uses a Bearer token from Settings → API.

```bash
curl -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     https://api.groovehq.com/v1/tickets?page=1
```

Key extraction endpoints:

| Endpoint | Method | Notes |
|---|---|---|
| `/v1/tickets` | GET | Lists tickets, paginated. Returns metadata, tags, assignee links, customer links. |
| `/v1/tickets/:number/messages` | GET | All messages for a ticket. Includes body, sender, timestamps. |
| `/v1/customers` | GET | Lists all customers. |
| `/v1/agents` | GET | Lists all agents. |
| `/v1/attachments` | GET | Lists attachments for a ticket. |

The REST API uses page-based pagination (`?page=N`). Groove does not publicly document per-plan rate limits for the v1 REST API; treat rate limit thresholds as unverified and implement adaptive backoff (respect `429` responses and `Retry-After` headers) rather than assuming a fixed ceiling. For delta migrations (syncing tickets created after the initial JSON export), the REST API is the right tool. For bulk extraction of your full history, use the JSON export.

### Groove GraphQL API (v2)

Groove's GraphQL API is the officially recommended path forward, but the Inbox and Knowledge Base portions are still under active development as of the date of this guide — check Groove's API changelog for current endpoint availability. The GraphQL API can work for flexible queries, but for bulk export of ticket history, the REST API or built-in JSON export is more battle-tested.

> [!WARNING]
> **API transition risk:** Groove's REST API is deprecated but functional. The GraphQL API coverage for Inbox data is incomplete. Before building extraction scripts, verify which endpoints return the data you need against your specific Groove plan. Test both APIs against a small dataset before committing to either extraction path.

## Gorgias API Constraints for Import

Gorgias exposes a RESTful API for creating and managing tickets, customers, messages, tags, and custom fields. All requests go to `https://{your-domain}.gorgias.com/api` with HTTP Basic authentication.

### Rate Limits

Gorgias rate limits are the single biggest constraint for any bulk import:

| Auth Method | Limit | Window | Effective RPM |
|---|---|---|---|
| API key | 40 requests | 20 seconds | ~120 |
| OAuth2 | 80 requests | 20 seconds | ~240 |
| Enterprise (API key) | 40 requests | 10 seconds | ~240 |
| Enterprise (OAuth2) | 80 requests | 10 seconds | ~480 |

The API uses a **leaky bucket algorithm**. Exceeding the limit returns `429 Too Many Requests` with `Retry-After` and `X-RateLimit-Reset` headers. Read both headers; use `Retry-After` for the backoff duration. At 40 requests per 20 seconds (the typical API key rate), you get roughly **120 requests per minute**. Creating a single ticket with its messages and customer requires multiple API calls — realistically, expect **30–60 tickets per minute** throughput during a migration (this is an empirical approximation based on typical call patterns, not a guaranteed API throughput figure).

For a 10,000-ticket migration at 30–60 tickets per minute, that's roughly 3–6 hours of continuous import time assuming no errors. For 50,000+ tickets, you're looking at multiple days. Build queueing, retry, resumability, and idempotency into the importer instead of assuming one uninterrupted pass.

> [!TIP]
> **Set `limit=100` on every list call.** Gorgias uses cursor-based pagination with a default page size of 30 and a max of 100. Bumping to 100 cuts your request count by more than 3x during read operations.

### Key Import Endpoints

| Endpoint | Method | Purpose |
|---|---|---|
| `POST /api/customers` | POST | Create or find a customer |
| `POST /api/tickets` | POST | Create a ticket with initial message(s) |
| `POST /api/tickets/{id}/messages` | POST | Add messages to an existing ticket |
| `PUT /api/tickets/{id}/custom-fields` | PUT | Set custom field values (replace-all semantics) |
| `PUT /api/tickets/{id}` | PUT | Update ticket status, tags, assignee |

The create-ticket endpoint accepts **1 to 500 messages** in the initial payload. Threads longer than 500 messages need follow-up calls to the ticket-message endpoint. ([developers.gorgias.com](https://developers.gorgias.com/reference/create-ticket))

> [!CAUTION]
> **Replace-all semantics on custom fields.** When you `PUT` to `/api/tickets/{id}/custom-fields`, the request body must contain **all** custom field values you want the ticket to have. Any field not included is deleted. Always fetch current values first, merge, then write back.

### Deduplication and Idempotency Behavior

The `external_id` field on Gorgias tickets is the correct mechanism for tracking migrated records, but Gorgias does **not** enforce uniqueness on `external_id` at the API level. A `POST /api/tickets` with the same `external_id` value will create a second ticket — it will not return a 409 or match the existing record. Deduplication is your responsibility:

- Before each ticket creation call, query `GET /api/tickets?external_id=groove-{number}` to check for an existing record.
- Maintain a local mapping table (SQLite works) of `groove_ticket_number → gorgias_ticket_id`.
- On script restart, load the mapping table and skip already-imported tickets.

Without this guard, a failed and restarted migration creates duplicate ticket records in Gorgias.

### Common API Error Responses

Understanding error responses prevents wasted debugging time:

| HTTP Status | Common Cause | Resolution |
|---|---|---|
| `400 Bad Request` | Malformed JSON, missing required field, invalid field value | Check response body for field-level error detail; `sent_datetime` missing and `from_agent` null are frequent causes |
| `422 Unprocessable Entity` | Valid JSON but invalid business logic (e.g., invalid `status` value, unrecognized `channel`) | Validate enum values (`status`: `open`/`closed`; `channel`: `email`, `chat`, `api`, `phone`, `sms`, `twitter`, `facebook`, `instagram`) against docs before bulk run |
| `429 Too Many Requests` | Rate limit exceeded | Read `Retry-After` header; implement exponential backoff |
| `404 Not Found` | Invalid ticket ID in message append call | Verify ticket was created successfully before appending messages |

The `channel` field on messages deserves particular attention: Gorgias validates that the channel value is a recognized type (`email`, `chat`, `api`, `phone`, `sms`, `twitter`, `facebook`, `instagram`). Using `channel: "api"` is always safe for migrated historical data regardless of original source channel, but loses channel provenance. If you want to preserve the original channel (e.g., `chat` for Groove live chat conversations), Gorgias does **not** require an active integration of that channel type to accept the message — the field is stored as metadata. Verify this against current Gorgias API behavior before relying on it in production.

### Automation and Webhook Behavior During Import

API-created messages with `channel: "api"` will not trigger auto-reply rules, but other rule actions (tagging, assignment, etc.) can still fire based on ticket creation and update events. This applies to **both** Rules and outbound webhooks — if you have external integrations (a Slack notifier, a CRM sync) subscribed to Gorgias webhook events, those webhooks will fire for every imported ticket. Tighten or pause automations **and** disable or filter webhooks before bulk import to prevent historical tickets from triggering modern workflows and external system noise.

> [!CAUTION]
> **Historical messages must carry `sent_datetime`.** If a message is created via the API without `sent_datetime`, Gorgias treats it as a new outbound message and will actually send it to the customer. This is how test imports turn into accidental customer emails. Always set `sent_datetime` on every historical message. ([developers.gorgias.com](https://developers.gorgias.com/reference/create-ticket-message))

> [!WARNING]
> **`from_agent: true` requires a valid Gorgias user.** If you set `from_agent: true` on a message and the associated `author` does not match an existing Gorgias user, the API returns a `400` error with a field-level message indicating the user could not be resolved. Create all agent accounts — including a "Legacy Agent" placeholder for deprovisioned agents — before importing tickets.

## Data Model Mapping: Groove → Gorgias

Both platforms are conversation-first inboxes, but the structural differences matter during transformation.

### Object-Level Mapping

| Groove Entity | Gorgias Entity | Notes |
|---|---|---|
| Conversation (ticket) | Ticket | 1:1 mapping. Store Groove `number` in Gorgias `external_id` for cross-reference. Note: Gorgias does not enforce `external_id` uniqueness — deduplication requires a local mapping table. |
| Message | Message | Includes body_html, sender, timestamps. Set `sent_datetime` on all historical messages. |
| Private note / internal phone log | Internal note message | Set `public: false`. Do not remap internal notes into customer-visible replies. |
| Customer | Customer | Email-based primary identity. |
| Agent | User | Map by email address. Create agents in Gorgias first. |
| Company | Customer `data` field or custom widget | Gorgias has no native company/organization entity. See note below. |
| Tag/Label | Tag | Tags are **case-sensitive** in Gorgias. Normalize case before import to avoid duplicates. |
| Mailbox | N/A (integration) | Gorgias uses channel integrations, not mailbox entities. Connect email addresses as integrations. |
| Group | Team | Create Gorgias Teams that mirror Groove Groups before importing tickets. |
| Folder (Smart Folder) | View | Rebuild manually. Map Smart Folder filter criteria to Gorgias View filters. |
| Instant Reply | Macro | Rebuild manually. Gorgias macro template variables differ from Groove's syntax. |
| Rule | Rule | Rebuild manually. Groove rule conditions/actions don't export programmatically. |
| Attachment | Attachment | Must be downloaded from Groove and re-uploaded. See attachment section. |
| Knowledge Base article | Help Center article | Separate migration. Gorgias Help Center import supports Zendesk, HelpDocs, Intercom, Re:amaze — not Groove. |

> [!WARNING]
> **Company/organization data has no native home in Gorgias.** If your team relies on company-level grouping (multiple contacts under one org), Gorgias does not support this natively. Store company data in the customer `data` field for sidebar display, but you lose the ability to group tickets by organization. Gorgias also limits you to **four active customer fields** — choose carefully which Groove contact/company fields to promote. ([developers.gorgias.com](https://developers.gorgias.com/reference/the-ticket-object))

### Status Mapping

This is where data silently degrades if you don't handle it explicitly.

Groove's current UI uses three statuses: **Open**, **Snoozed**, and **Closed**. The v1 API (which the JSON export uses) may also expose legacy states like `unread`, `opened`, `pending`, and `spam`. Gorgias exposes two statuses via API: **open** and **closed**.

| Groove Status | Gorgias API Status | Recommended Handling |
|---|---|---|
| `opened` / `unread` / Open | `open` | Direct map. |
| `pending` / Snoozed | `open` | Gorgias supports snoozing via `snooze_datetime` on open tickets. Set it as a separate update after ticket creation if the snooze date is still in the future. If the snooze date has already passed, close the ticket instead of importing it as open. Append a tag like `status:pending` or `status:snoozed` to preserve context in all cases. |
| `closed` / Closed | `closed` | Direct map. |
| `spam` | `closed` | Import as closed with a `spam` tag, or skip entirely depending on your retention policy. |

If you bulk-import snoozed tickets as `open` without setting `snooze_datetime`, they appear in agents' active queues and create noise on day one. If you import them as `closed`, you lose the intent that follow-up was planned.

### Field-Level Mapping for Tickets

| Groove Field | Gorgias Field | Transform Required |
|---|---|---|
| `number` | `external_id` | Store Groove ticket number for cross-reference. Prefix with `groove-` for namespacing (e.g., `groove-1045`). |
| `created_at` | `created_datetime` | ISO 8601 format. |
| `updated_at` | `updated_datetime` | ISO 8601 format. |
| `state` | `status` | Map per status table above. |
| `tags` | `tags` | Array of tag name objects: `[{"name": "tag"}]`. Case-sensitive. |
| `priority` | N/A | Gorgias has no native priority field. Map to a tag (e.g., `priority-high`) or a custom ticket field. |
| `assigned_group` | `assignee_team` | Map Groove group → Gorgias team. |
| `assignee` (agent email) | `assignee_user` | Map by email → Gorgias user ID. |
| `summary` | N/A | Gorgias derives the summary from the first message. No separate field. |

### Field-Level Mapping for Messages

| Groove Field | Gorgias Field | Transform Required |
|---|---|---|
| `body` (HTML) | `body_html` | Direct map. Sanitize encoding issues (see note below). |
| `body` (plain text) | `body_text` | Optional plain-text version. |
| N/A | `stripped_text` | Populate if you rely on body-based rules or full-text search. |
| `author` (agent/customer) | `from_agent` | Boolean. `true` if sent by agent, `false` if from customer. Agent messages require a valid Gorgias user — map unresolvable agents to a Legacy Agent before setting `from_agent: true`. |
| `created_at` | `sent_datetime` | **Critical.** Use `sent_datetime` to prevent Gorgias from treating historical messages as new outbound sends. |
| `note` (boolean) | `public` | Groove internal notes → `public: false` in Gorgias. |

> [!WARNING]
> **Encoding and character set issues.** Groove messages may contain non-UTF-8 characters, smart quotes, em-dashes, or emoji. The Gorgias API expects UTF-8. Transcode message bodies to UTF-8 and strip or replace invalid byte sequences before submission. Malformed encoding typically produces `400` errors with generic body-field messages that don't identify the character-level cause.

### Customer Mapping

| Groove Field | Gorgias Field | Notes |
|---|---|---|
| `email` | `email` | Primary identifier. |
| `name` | `firstname` / `lastname` | Groove stores a single `name`. Split on first space for Gorgias. Names with no space: put full value in `firstname`, leave `lastname` empty. |
| `company_name` | `data` or custom widget | Gorgias has no native company entity. |

### Payload Transformation Example

To illustrate the transformation, here is a simplified Groove ticket mapped to a Gorgias import payload:

**Source: Groove JSON Export**
```json
{
  "id": 987654,
  "number": 1045,
  "state": "pending",
  "customer_email": "user@example.com",
  "assignee": "agent@company.com",
  "tags": ["refund", "escalated"],
  "messages": [
    {
      "body": "<p>I need a refund for my order.</p>",
      "created_at": "2023-10-12T14:20:00Z",
      "is_agent": false
    }
  ]
}
```

**Target: Gorgias Ticket Creation Payload**
```json
{
  "external_id": "groove-1045",
  "channel": "email",
  "via": "api",
  "status": "open",
  "customer": {
    "email": "user@example.com"
  },
  "tags": [
    {"name": "refund"},
    {"name": "escalated"},
    {"name": "status:pending"}
  ],
  "messages": [
    {
      "channel": "email",
      "via": "api",
      "source": {
        "to": [{"address": "support@company.com"}],
        "from": {"address": "user@example.com"}
      },
      "body_html": "<p>I need a refund for my order.</p>",
      "sent_datetime": "2023-10-12T14:20:00Z",
      "from_agent": false,
      "public": true
    }
  ]
}
```

Key structural changes: tags become arrays of objects, `state` maps to `status` plus a preservation tag, messages require explicit `channel` and `source` objects, `sent_datetime` is set to prevent accidental outbound sends, and `external_id` is prefixed for namespacing. A follow-up `PUT /api/tickets/{id}` would set `snooze_datetime` if the pending snooze date is still in the future.

## The Attachment Problem

Attachments are the most common cause of silent data loss in helpdesk migrations.

Groove attachments are stored behind authenticated URLs. If you extract a Groove message payload and pass the raw attachment URL into Gorgias, Gorgias will attempt to fetch the file, hit an authentication wall, and fail silently — no error in the API response, just a missing file.

To migrate attachments, your script must:

1. Parse Groove message bodies for attachment URLs — including `<img>` tags for inline images in the HTML body.
2. Download each file using a Groove API token for authentication.
3. Re-host the file on a publicly accessible endpoint (e.g., an S3 bucket with temporary public read access, or GCS).
4. Pass the new public URL into the Gorgias API payload.
5. Gorgias fetches and hosts the file natively.

Inline images in HTML bodies deserve special attention. If you import the HTML as-is, those images reference Groove's CDN. They may work initially but will break if Groove decommissions the URLs after account closure. Download and re-host inline images alongside standard attachments, then rewrite the `src` attributes in the HTML body before submission.

This is the slowest part of any migration. Each attachment is a separate download-upload cycle, and Gorgias rate limits apply to the upload calls. For large migrations with many attachments, budget additional time proportional to attachment count — a migration with 5 attachments per ticket doubles or triples total import time compared to a text-only migration.

## Migration Methods: Trade-offs

### Method 1: JSON Export → Custom Script → Gorgias API

**Best for:** Teams with engineering capacity, any volume.

Export full conversation history as GZIP JSON. Write a transformation script. Import via Gorgias REST API. Full control over data transformation, attachment handling, and edge cases. Requires a developer comfortable with API orchestration, rate-limit management, and deduplication logic. Estimated effort: 1–2 weeks for a typical mid-size migration (5K–50K tickets).

### Method 2: Groove REST API → ETL Pipeline → Gorgias API

**Best for:** Teams that need incremental extraction or real-time sync during transition.

Pull data from Groove's API in batches and push to Gorgias. Supports selective migration (e.g., only last 12 months) and incremental sync during parallel-run periods. The trade-off: you face double rate-limit constraints (Groove extraction + Gorgias import) and depend on Groove's deprecated REST API, which may break without notice. Build the deduplication mapping table from the start — delta runs will overlap with bulk import records.

### Method 3: Third-Party Migration Tool

**Best for:** Non-technical teams with straightforward data.

Tools like Help Desk Migration support Gorgias as a target. Gorgias explicitly recommends partner tooling for teams coming from non-Zendesk providers. ([docs.gorgias.com](https://docs.gorgias.com/en-US/articles/helpdesk-migration-18379)) Pre-built mapping handles common cases. The trade-off: per-record pricing adds up on large datasets, limited control over transformation logic, and Groove may not be a natively supported source in these tools — you may need to export to CSV first, which loses some metadata fidelity. Validate attachment and internal note handling specifically before committing to a third-party tool for a Groove source.

### Method 4: Managed Migration Service

**Best for:** Teams with large volumes, complex data, or zero tolerance for data loss.

Hand the migration to an engineer-led service that handles extraction, transformation, rate-limit orchestration, attachment transfer, deduplication, and validation. Fastest path for complex migrations. Higher upfront cost than DIY, but includes error handling, test runs, and audit trails.

## Step-by-Step Migration Process

### Step 1: Audit Your Groove Data

Before touching any API, document what you have:

- **Total ticket count** by status (Open, Snoozed, Closed, Spam)
- **Active agents and groups** — list email addresses and group memberships
- **Tags in use** — export the full tag list, identify active vs. legacy tags
- **Conversation custom fields** — list all fields and their types
- **Contact/company custom fields** — decide which map to Gorgias's four customer field slots
- **Instant Replies** — copy each template's text for rebuild as Gorgias Macros
- **Rules** — document conditions and actions for each Rule
- **Active integrations** — Shopify, Stripe, Slack, HubSpot connections don't transfer
- **Knowledge Base** — plan a separate migration path if applicable
- **Channel mix** — identify email, chat, social, and phone conversation volumes; note that non-email channels require channel mapping decisions
- **Attachment volume** — count total attachments and average per ticket; this directly determines import timeline

### Step 2: Build the Gorgias Schema

Create the target environment before loading any data:

1. Create your Gorgias account and connect your Shopify store (if applicable)
2. Connect email channels — add the same email addresses used in Groove
3. Create agent accounts with matching email addresses — including a "Legacy Agent" placeholder for any agents who have left the team
4. Create Teams that mirror your Groove Groups
5. Create custom Ticket Fields (Dropdown, Number, Text, Yes/No) — only fields created before import can be used in Rules
6. Create tags matching your Groove tag taxonomy (remember: **case-sensitive** in Gorgias)
7. Pre-create dropdown values — Gorgias dropdown CSV imports cap at 2,000 values and 5 levels
8. Disable or filter automations and webhooks before running bulk import

> [!WARNING]
> **Do not connect the same email address to both Groove and Gorgias simultaneously.** Incoming messages will be duplicated across both platforms, creating conflicting ticket histories. Keep Groove active until migration is complete, then switch email routing.

### Step 3: Extract Data from Groove

**Option A — JSON Export (recommended for bulk):** Trigger the export from Settings → Company → More → Exports. Download immediately when ready. This is the fastest path for your full history and bypasses API rate limits.

**Option B — REST API (for deltas and selective extraction):** Use the Groove REST API to pull tickets created or updated after your JSON export timestamp. Implement adaptive backoff on `429` responses — do not assume a fixed rate limit ceiling.

```python
import requests
import time

GROOVE_TOKEN = "your_groove_api_token"
BASE_URL = "https://api.groovehq.com/v1"
headers = {
    "Authorization": f"Bearer {GROOVE_TOKEN}",
    "Content-Type": "application/json"
}

def get_all_tickets():
    tickets = []
    page = 1
    while True:
        resp = requests.get(f"{BASE_URL}/tickets?page={page}", headers=headers)
        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()
        tickets.extend(data.get("tickets", []))
        if not data.get("meta", {}).get("pagination", {}).get("next_page"):
            break
        page += 1
        time.sleep(0.5)  # Conservative pacing between pages
    return tickets
```

### Step 4: Build the Local Mapping Table

Before importing, initialize a local SQLite database to track progress and enable idempotent reruns:

```sql
CREATE TABLE ticket_map (
    groove_ticket_number INTEGER PRIMARY KEY,
    gorgias_ticket_id INTEGER,
    imported_at TIMESTAMP,
    status TEXT  -- 'pending', 'imported', 'failed'
);

CREATE TABLE customer_map (
    groove_customer_email TEXT PRIMARY KEY,
    gorgias_customer_id INTEGER
);
```

On script restart, load existing mappings and skip records with `status = 'imported'`. This prevents duplicate ticket creation, since Gorgias does not enforce `external_id` uniqueness.

### Step 5: Transform and Load into Gorgias

For each Groove conversation:

1. **Check the mapping table** — skip if already imported.
2. **Create or find the customer** via `POST /api/customers` using the customer email. Store the returned Gorgias Customer ID in the mapping table.
3. **Create the ticket** via `POST /api/tickets` with the first message(s), tags, channel, status, and `external_id` set to `groove-{number}`.
4. **Record the mapping** — write `groove_ticket_number → gorgias_ticket_id` to the mapping table immediately after successful creation.
5. **Add remaining messages** via `POST /api/tickets/{id}/messages` in chronological order. Set `sent_datetime` on every message. Batches above 500 messages require staged writes.
6. **Set custom fields** via `PUT /api/tickets/{id}/custom-fields` — remember replace-all semantics; fetch current values first if the ticket was partially updated.
7. **Update ticket metadata** — set assignee, final status, and `snooze_datetime` if applicable.

Your script must implement exponential backoff. When the Gorgias API returns `429`, read the `Retry-After` header and pause. Failing to handle 429s results in dropped messages and incomplete ticket threads.

```python
import requests
import time
import sqlite3

GORGIAS_DOMAIN = "your-store"
GORGIAS_EMAIL = "[email protected]"
GORGIAS_API_KEY = "your_api_key"
BASE = f"https://{GORGIAS_DOMAIN}.gorgias.com/api"

def gorgias_request(method, endpoint, **kwargs):
    """Wrapper with exponential backoff on 429."""
    url = f"{BASE}{endpoint}"
    auth = (GORGIAS_EMAIL, GORGIAS_API_KEY)
    delay = 20
    for attempt in range(5):
        resp = requests.request(method, url, auth=auth, **kwargs)
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", delay))
            time.sleep(retry_after)
            delay = min(delay * 2, 120)
            continue
        return resp
    raise Exception(f"Failed after 5 retries: {endpoint}")

def map_status(groove_state):
    return "open" if groove_state in ["opened", "pending", "unread"] else "closed"

def create_gorgias_ticket(conversation, db_conn):
    # Check mapping table first
    cursor = db_conn.execute(
        "SELECT gorgias_ticket_id FROM ticket_map WHERE groove_ticket_number = ?",
        (conversation["number"],)
    )
    row = cursor.fetchone()
    if row:
        return row[0]  # Already imported

    payload = {
        "external_id": f"groove-{conversation['number']}",
        "channel": "email",
        "via": "api",
        "status": map_status(conversation["state"]),
        "customer": {"email": conversation["customer_email"]},
        "tags": [{"name": t} for t in conversation.get("tags", [])],
        "messages": [{
            "body_html": conversation["first_message"]["body"],
            "from_agent": conversation["first_message"]["is_agent"],
            "sent_datetime": conversation["first_message"]["created_at"],
            "via": "api",
            "channel": "email",
            "public": True
        }]
    }
    resp = gorgias_request("POST", "/tickets", json=payload)
    resp.raise_for_status()
    gorgias_id = resp.json()["id"]

    # Record mapping immediately
    db_conn.execute(
        "INSERT INTO ticket_map VALUES (?, ?, datetime('now'), 'imported')",
        (conversation["number"], gorgias_id)
    )
    db_conn.commit()
    return gorgias_id
```

### Step 6: Handle Attachments

For each attachment and inline image:

1. Download from Groove's authenticated URL using your API token
2. Upload to a publicly accessible location (S3, GCS) with appropriate expiry
3. Update the message HTML to reference the new URLs before submitting the message payload to Gorgias
4. After Gorgias ingests the file, the public hosting URL can be removed or expired

Budget extra time for this phase. Large migrations with many attachments significantly extend total import time due to per-file download-upload cycles consuming Gorgias rate limit budget.

### Step 7: Validate

Do not rely on the script completing without errors as proof of success. Run these checks:

- **Ticket count match** — total in Groove vs. Gorgias
- **Message count per ticket** — spot-check 20–50 tickets for correct threading and message order
- **Customer linkage** — verify customers are correctly associated with their tickets
- **Tag preservation** — confirm tags transferred with correct case
- **Status accuracy** — check open/closed distribution matches source
- **Snoozed ticket handling** — verify `snooze_datetime` is set on pending tickets with future snooze dates; verify past-snooze tickets are closed
- **Attachment availability** — open 10–20 tickets with attachments and verify files download
- **Timestamp integrity** — confirm `sent_datetime` values match source data, not today's date
- **Inline images** — check HTML body rendering for broken `<img>` tags
- **Internal notes** — verify notes are marked private (`public: false`)
- **Encoding** — check a sample of tickets with special characters, emoji, or non-Latin scripts for correct rendering
- **Deduplication** — verify no ticket appears twice (query Gorgias for duplicate `external_id` values)

### Step 8: Rebuild Automations

No automation migrates programmatically between Groove and Gorgias:

- **Instant Replies → Macros:** Copy the text content. Gorgias macros use different template variables (`{{ticket.customer.firstname}}` vs. Groove's syntax). Macro CSV import supports `name`, `body_text`, `tags`, and `id` only — no formatting preservation on new imports. ([docs.gorgias.com](https://docs.gorgias.com/en-US/import-and-export-macros-207786))
- **Rules → Rules:** Gorgias Rules use similar conditions and actions, but available triggers and operators differ. Map each Groove Rule manually. Re-enable automations only after testing on a small set of live tickets.
- **Smart Folders → Views:** Recreate filter logic in Gorgias's view system.
- **Webhooks:** Reconnect and re-enable any webhook integrations that were disabled during import.

### Step 9: Run Delta Sync and Go Live

Your team will continue answering tickets in Groove during the migration. Once the bulk import and validation are complete:

1. Use the Groove REST API to fetch tickets updated since your JSON export timestamp
2. Transform and push these delta tickets to Gorgias (the mapping table prevents duplicates for tickets that appear in both the bulk export and the delta)
3. Validate the delta
4. Disable email forwarding to Groove
5. Verify Gorgias is receiving new emails on the connected addresses
6. Deactivate Groove integrations (Shopify, Slack, Stripe)
7. Activate equivalent integrations in Gorgias
8. Brief your support team on the new interface

## Edge Cases That Cause Data Loss

**Inline images in HTML bodies.** Groove messages may contain inline images hosted on Groove's CDN. If Groove decommissions those URLs after account closure, every imported ticket with inline images breaks. Download and re-host them during migration, and rewrite `src` attributes in the HTML before submitting to Gorgias.

**Snoozed tickets with past snooze dates.** If a ticket was snoozed until a date that has already passed, importing it as `open` creates a false active ticket in agents' queues. Import these as `closed` instead, with a `status:snoozed` tag for context.

**Merged conversations.** Groove supports merging conversations. Merged tickets may have discontinuous message threads or messages from multiple original customers. Verify message ordering and customer attribution after import.

**Phone call notes.** Groove logs phone calls as internal-only conversations with no customer-visible messages. Import as internal notes (`public: false`) in Gorgias, not as customer-facing tickets.

**Long message threads.** The create-ticket endpoint accepts up to 500 messages. Threads longer than that need staged writes — create the ticket with the first batch, then append remaining messages via the ticket-message endpoint. For noisy system-generated threads, consider collapsing low-value audit chatter into a single internal note rather than replaying every line.

**Non-email channels.** Groove conversations can come from email, live chat, Facebook, and logged phone interactions. Use `channel: "api"` as a safe default for all migrated messages if you cannot validate the target channel type; preserve the original channel in a tag (e.g., `source:chat`) so it is queryable.

**Groove ticket numbers vs. Gorgias IDs.** Gorgias assigns its own ticket IDs. Store the original Groove ticket number in `external_id` (prefixed as `groove-{number}`) so agents can cross-reference during the transition period.

**Agents no longer on the team.** If you push a ticket assigned to an agent email that doesn't exist in Gorgias, the API returns a `400` error. Create a "Legacy Agent" user for historical assignments before running the import.

**Encoding issues.** Non-UTF-8 characters in Groove message bodies produce `400` errors on the Gorgias API with generic messages that don't identify the offending character. Transcode all message content to UTF-8 and strip or replace invalid byte sequences as part of the transformation step.

**Duplicate `external_id` records.** Gorgias does not enforce uniqueness on `external_id`. A restarted migration without a local mapping table creates duplicate tickets. Always maintain the mapping table and check it before every ticket creation call.

**Mailbox-to-integration mapping.** Groove lets you create as many inboxes as you need. Gorgias organizes around stores, channels, teams, and views. A Groove mailbox might map to a store, a team, or just a routing tag/view pair. Decide this mapping before import, or historically correct tickets end up in the wrong operational lane.

## What Cannot Be Migrated

| Item | Why | Workaround |
|---|---|---|
| Instant Replies | No programmatic export from Groove | Copy text manually, rebuild as Gorgias Macros |
| Rules/Automations | No export API | Document conditions and actions, rebuild in Gorgias |
| Smart Folders | Configuration-only, not data | Recreate as Gorgias Views |
| Knowledge Base | Gorgias KB import doesn't support Groove | Export articles, reformat, import via Gorgias KB editor or CSV |
| Reports/Analytics | Platform-specific | Export historical data to a BI tool before decommissioning Groove |
| Integrations | Platform-specific credentials | Reconnect each integration (Shopify, Slack, Stripe) in Gorgias |
| Webhook subscriptions | Platform-specific configuration | Reconfigure webhooks in Gorgias after import is complete |

## Timeline Expectations

| Scenario | Ticket Volume | Estimated Timeline |
|---|---|---|
| Small team, simple data, minimal attachments | Under 5,000 tickets | 3–5 days |
| Mid-size, with attachments | 5,000–50,000 tickets | 1–2 weeks |
| Large, with complex data and KB | 50,000+ tickets | 2–4 weeks |

Timelines include data audit, extraction, transformation, import, validation, and automation rebuild. The Gorgias API rate limit is typically the bottleneck — not the extraction from Groove. Attachment volume is the primary multiplier on total time within a given ticket-count tier.

## When to DIY vs. Use a Managed Service

**Handle it yourself if:**
- Fewer than 10,000 tickets
- Minimal or no attachments
- A developer can dedicate 1–2 weeks
- Simple tag and status model
- No external webhooks or integrations dependent on Gorgias events

**Use a managed service if:**
- Volume exceeds 50,000 tickets
- Full attachment preservation required
- Snoozed ticket state and snooze datetimes must be preserved
- Migration must complete in days, not weeks
- Zero tolerance for data loss (compliance, audit trail requirements)
- External integrations are subscribed to Gorgias webhook events and require controlled cutover

ClonePartner has handled Groove migrations across various target platforms. If you need full attachment handling, rate-limit orchestration, deduplication, and validation — we can scope it in a 30-minute call.

> Planning a Groove to Gorgias migration? Book a free 30-minute scoping call with our engineering team. We'll audit your data model, map the edge cases, and give you an honest timeline.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Does Gorgias have a native importer for Groove?

No. Gorgias offers a native historical data importer for Zendesk and an email-history import for connected Gmail/Outlook addresses, but not for Groove. A Groove to Gorgias migration requires using the Gorgias REST API, Groove's JSON export, or a third-party migration tool like Help Desk Migration.

### How do Groove ticket statuses map to Gorgias?

Groove uses Open, Snoozed, and Closed (the v1 API also exposes unread, opened, pending, and spam). Gorgias only has open and closed via its API. Snoozed/pending tickets should be imported as open with snooze_datetime set separately if the snooze date is still in the future. Append a tag like status:pending to preserve context.

### Can I migrate Groove attachments to Gorgias?

Yes, but attachments must be downloaded from Groove's authenticated URLs and re-uploaded to Gorgias via the API. You cannot pass Groove attachment URLs directly — Gorgias will fail to fetch them. This is the slowest part of most migrations due to the per-file download-upload cycle and Gorgias rate limits.

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

Typically 3–5 days for under 5,000 tickets, 1–2 weeks for 5K–50K tickets, and 2–4 weeks for 50K+ tickets. The Gorgias API rate limit (40 requests per 20 seconds on API key auth) is usually the bottleneck.

### What is the biggest technical risk in a Groove to Gorgias migration?

Accidentally sending historical messages to customers. If you create messages via the Gorgias API without setting the sent_datetime field, Gorgias treats them as new outbound messages and actually sends them. Always set sent_datetime on every historical message.
