---
title: "eDesk to Groove Knowledge Base Migration: Technical Guide"
slug: edesk-to-groove-knowledge-base-migration-technical-guide
date: 2026-07-31
author: Rishabh
categories: [Migration Guide, Groove]
excerpt: "Technical guide for migrating knowledge base content from eDesk to Groove. Covers extraction without a KB API, data mapping, HTML transformation, image re-hosting, and redirect planning."
tldr: "eDesk has no public KB API — migrate articles to Groove via scraping or manual extraction, then load through Groove's REST/GraphQL endpoints with HTML transformation, image re-hosting, and redirect mapping."
canonical: https://clonepartner.com/blog/edesk-to-groove-knowledge-base-migration-technical-guide/
---

# eDesk to Groove Knowledge Base Migration: Technical Guide


Migrating a knowledge base from eDesk to Groove means moving from an eCommerce-first helpdesk add-on to a SaaS-oriented shared-inbox platform with its own built-in knowledge base. There is no native migration path between the two. eDesk's documented API surface covers tickets, messages, contacts, templates, and orders — but does not list Knowledge Base libraries or articles. Groove exposes KB endpoints in both its REST and GraphQL APIs, but the REST API is deprecated in favor of GraphQL, and Groove's own docs note that KB APIs in GraphQL are still being built. ([developers.edesk.com](https://developers.edesk.com/))

In practice, this migration is an ETL job: extract from the eDesk UI or published KB pages, normalize hierarchy and HTML, re-host images, then load into Groove through its KB API. If you skip image re-hosting, your articles will break the moment your eDesk account is deactivated. If you skip redirect mapping, your organic search traffic disappears.

This guide covers every viable migration method, the exact data model mapping, API constraints on both sides, and the edge cases that cause silent content loss.

For a broader pre-migration checklist that covers URL redirects, SEO preservation, and inline image handling, see our [knowledge base migration checklist](https://clonepartner.com/blog/blog/the-ultimate-knowledge-base-migration-checklist-a-zero-downtime-plan/). If you're also moving Groove helpdesk data (tickets, conversations), see our [Groove to Zendesk migration guide](https://clonepartner.com/blog/blog/groove-to-zendesk-migration-data-mapping-apis-rate-limits/) for reference on Groove's data export behavior.

> [!NOTE]
> **Scope:** This guide covers eDesk Knowledge Base articles, categories, and media migrating into Groove's Knowledge Base. It does not cover ticket, conversation, or customer data migration. For eDesk helpdesk migrations, see our [eDesk to Pylon migration guide](https://clonepartner.com/blog/blog/edesk-to-pylon-migration-a-technical-guide/).

*Last verified: July 2025. API endpoints and platform capabilities confirmed against published eDesk and Groove documentation as of this date.*

## eDesk KB vs Groove KB: Feature Gaps That Shape Migration Decisions

Before mapping fields, understand which features exist only on one side. These asymmetries determine what content you can migrate faithfully, what requires architectural changes, and what you lose entirely.

**Features eDesk KB has that Groove KB lacks:**
- Nested subcategories (parent-child category hierarchy). Groove's category schema is flat — no `parent_id` field.
- Marketplace channel linking (articles surfaced per sales channel — Amazon, eBay, Shopify, etc.).
- Per-article password protection. Groove supports password protection only at the KB level.
- Direct integration with eDesk's AI chatbot and Smart Reply as training content.
- Custom CSS injection at the KB layout level with predefined layout templates.

**Features Groove KB has that eDesk KB lacks:**
- Native multi-language translation system with locale-based article variants.
- Per-article SEO metadata: `meta_robots`, `og_title`, `og_description`, `page_title`.
- Related article linking via `related_ids []` array.
- Programmatic KB management via REST and GraphQL APIs (eDesk has no public KB API).
- IP address restriction for KB access control.
- Article lifecycle states: `draft`, `published`, `wip` (editing a published article puts it in `wip` until republished).

**What this means for migration planning:**
- Nested eDesk categories must be flattened before loading. Decide on a convention: prefix naming (`Returns / EU`, `Returns / US`) or separate top-level categories.
- Channel-linked articles that reference eDesk-specific features (snippet shortcuts, chatbot behavior) should be reviewed and rewritten, not migrated verbatim.
- Groove's SEO fields (`meta_robots`, OG tags, `description`) have no eDesk equivalent — you gain capability but must populate these fields during migration or leave them at defaults.
- eDesk's per-article password protection cannot be replicated in Groove. If you have mixed public/protected articles, you'll need to split them across separate Groove KBs (one public, one password-protected).

## eDesk KB vs Groove KB: Data Model Comparison

| Concept | eDesk Knowledge Base | Groove Knowledge Base |
|---|---|---|
| **Top-level container** | Library (one per shop/brand) | Knowledge Base (multiple supported per account) |
| **Content grouping** | Categories with subcategories, keywords, and tags | Categories with slugs, SEO metadata, positioning — flat schema, no parent-category field |
| **Articles** | HTML body, categories, keywords, tags, Draft/Published | HTML body, slug, description, tags, related article IDs, SEO metadata (meta_robots, OG tags), Draft/Published/WIP |
| **Media** | Inline images (10MB limit per upload, hosted on eDesk CDN at `cdn.edesk.com` or similar) | Inline images (hosted on Groove/S3 CDN) |
| **Article body size limit** | Not documented | Not documented in public API docs; in practice, articles up to ~500KB HTML render without issues |
| **Translations** | Not natively supported | Built-in locale-based translation system (`locale` field per article variant) |
| **Access control** | Password protection (per-article), channel-linked access | Password protection (per-KB), IP address restriction |
| **SEO controls** | Google Analytics integration | meta_robots, page_title, OG tags, custom domains, GA code |
| **URL structure** | Typically `/{kb-slug}/{article-id}-{slug}` (article IDs often embedded in URL) | `/{kb-slug}/{category-slug}/{article-slug}` (no numeric IDs in URL) |
| **API access** | No public KB-specific API | REST v1 (deprecated but functional) + GraphQL v2 |

([support.edesk.com](https://support.edesk.com/edesk-knowledge-base?utm_source=openai))

## Migration Approaches: Every Viable Method

### Method 1: Manual Copy-Paste

**How it works:** Open each eDesk article in the editor, copy the content, and paste it into Groove's article editor. Recreate categories manually in Groove first.

**When to use it:** Fewer than 30 articles, no strict formatting requirements, and no need to preserve metadata like publish dates or author attribution.

**Pros:**
- Zero technical complexity
- Visual verification of each article during transfer
- No API keys or scripts needed

**Cons:**
- Does not scale past ~30 articles
- Formatting loss when pasting between rich-text editors (tables, embedded videos, code blocks)
- Inline images still reference eDesk's CDN — they break if eDesk deactivates the account
- No metadata preservation (creation dates, view counts, author info)

**Complexity:** Low
**Risk:** Medium (image links, formatting drift)

### Method 2: HTML Scraping + Groove API Import

**How it works:** Since eDesk does not expose a KB-specific API, you scrape the published articles from your eDesk KB's public URL structure. Parse the HTML to extract article titles, bodies, category assignments, and inline image URLs. Then push each article into Groove via the Knowledge Base Articles API.

**Step-by-step:**
1. Crawl your eDesk KB sitemap or category pages to discover all article URLs.
2. For each URL, extract the article title, body content (typically inside a main content `div`), and any `<img>` sources. eDesk KB pages typically use a content container — inspect your specific KB's HTML to identify the correct selector (common patterns include `div.article-content`, `div.kb-article-body`, or a `<main>` element). The image CDN domain is typically `cdn.edesk.com` or a subdomain of your eDesk instance.
3. Download and re-host all inline images (upload to Groove or your own CDN).
4. Replace image URLs in the article HTML.
5. Create categories in Groove via `POST /v1/kb/:kb_id/categories`.
6. Create articles via `POST /v1/kb/:kb_id/articles` with the cleaned HTML body.
7. Publish each article via `PUT /v1/kb/:kb_id/articles/:id/publish`.

**When to use it:** 30–500 articles where only published content matters and you don't need draft articles or internal notes.

**Pros:**
- Works even without eDesk API access (scrapes public pages)
- Captures the rendered article as customers see it
- Automatable with standard scraping libraries (BeautifulSoup, Puppeteer)

**Cons:**
- Only captures published articles — drafts and unpublished content are invisible
- Loses metadata (author, creation date, internal tags)
- HTML structure varies by eDesk KB template — scraper needs customization per layout
- eDesk may rate-limit or block aggressive crawling

**Complexity:** Medium
**Risk:** Medium (incomplete content, scraper brittleness)

### Method 3: Custom ETL Pipeline (API-to-API)

**How it works:** Build a script that extracts articles from eDesk (via scraping or manual JSON export), transforms the content (HTML cleanup, image re-hosting, field mapping), and loads it into Groove via its REST or GraphQL API.

**Extraction options for eDesk:**
- **Manual JSON capture:** Use browser dev tools to inspect network requests when loading the eDesk KB editor. The internal API responses contain article data in JSON format that is more structured than scraped HTML. This approach is fragile — internal APIs are undocumented and can change without notice.
- **Scraping:** As described in Method 2.
- **Report extracts:** eDesk's report export generates CSV files, but these cover tickets, agents, and channels — not KB articles directly.

**Loading into Groove (REST API v1):**

Groove's REST API requires a Bearer token passed in the `Authorization` header. Generate this token from your Groove account settings under API → API Keys. The token must have write permissions to the Knowledge Base; Groove does not document granular OAuth scopes for KB operations — API keys are account-level.

```python
import requests
import time

GROOVE_API_KEY = "your_bearer_token"
KB_ID = "your_knowledge_base_id"
BASE_URL = f"https://api.groovehq.com/v1/kb/{KB_ID}"

headers = {
    "Authorization": f"Bearer {GROOVE_API_KEY}",
    "Content-Type": "application/json"
}

def create_with_backoff(url, payload, headers, max_retries=5):
    """POST with exponential backoff on rate limit (429) responses."""
    for attempt in range(max_retries):
        resp = requests.post(url, json=payload, headers=headers)
        if resp.status_code == 429:
            wait = min(2 ** attempt * 5, 120)  # 5s, 10s, 20s, 40s, 80s
            time.sleep(wait)
            continue
        return resp
    return resp  # Return last response even if still 429

# Step 1: Create a category
category_payload = {
    "title": "Shipping & Returns",
    "description": "Articles about shipping policies and return procedures",
    "slug": "shipping-returns",
    "position": 1
}
cat_resp = create_with_backoff(f"{BASE_URL}/categories", category_payload, headers)
category_id = cat_resp.json()["category"]["id"]

# Step 2: Create an article in that category
article_payload = {
    "title": "How to Return an Item",
    "body": "<p>To initiate a return, navigate to your order page...</p>",
    "category_id": category_id,
    "author_id": "your_agent_id",
    "tags": ["returns", "shipping", "orders"],
    "slug": "how-to-return-an-item",
    "description": "Step-by-step guide to initiating a product return.",
    "meta_robots": "INDEX,FOLLOW",
    "og_title": "How to Return an Item",
    "og_description": "Step-by-step guide to initiating a product return."
}
art_resp = create_with_backoff(f"{BASE_URL}/articles", article_payload, headers)
article_id = art_resp.json()["article"]["id"]

# Step 3: Publish the article
requests.put(f"{BASE_URL}/articles/{article_id}/publish", headers=headers)
```

**Loading into Groove (GraphQL API v2):**

Groove's GraphQL endpoint is `https://api.groovehq.com/v2/graphql`. While Groove's docs note that KB APIs in GraphQL are still being built, the following pattern shows the expected mutation structure based on Groove's GraphQL schema:

```graphql
mutation CreateKBArticle($input: KnowledgeBaseArticleCreateInput!) {
  knowledgeBaseArticleCreate(input: $input) {
    article {
      id
      title
      slug
      state
    }
    errors {
      message
      path
    }
  }
}
```

```json
{
  "input": {
    "knowledgeBaseId": "kb_abc123",
    "categoryId": "cat_def456",
    "title": "How to Return an Item",
    "body": "<p>To initiate a return...</p>",
    "slug": "how-to-return-an-item",
    "tags": ["returns", "shipping"]
  }
}
```

Note: Verify the exact mutation name and input type against Groove's GraphQL introspection endpoint (`__schema` query) before building your pipeline, as the KB mutations may still be evolving.

**When to use it:** 50+ articles, need to preserve tags and category structure, or you want to automate the entire process.

**Pros:**
- Full control over transformation logic
- Can include image re-hosting, HTML sanitization, slug generation
- Repeatable — run test migrations before cutover

**Cons:**
- eDesk extraction is the bottleneck (no clean API for KB content)
- Groove's REST API is deprecated; GraphQL v2 is recommended but KB endpoints may still be in development
- Requires engineering time: 2–5 days for a senior developer

**Complexity:** High
**Risk:** Low (if tested thoroughly)

### Method 4: Third-Party Migration Tools

**How it works:** Point-and-click SaaS tools that move data between platforms.

**Reality check:** Coverage is mixed for this exact pair. Groove officially points customers to Import2 or to its API for migrations, but the source systems listed in Groove's own Import2 walkthrough do not include eDesk. eDesk promotes Help Desk Migration as a partner, but that positioning is centered on helpdesk data, not a verified eDesk KB-to-Groove KB field map. Confirm KB object coverage, media handling, and URL preservation in writing before committing. ([help.groovehq.com](https://help.groovehq.com/help/how-to-migrate-to-groove-and-import-your-tickets?utm_source=openai))

**Pros:**
- Less internal build work
- Project support, sample runs

**Cons:**
- Often fail on inline images (leaving them hosted on eDesk's CDN, which break when you cancel your account)
- Cannot handle custom 301 redirects
- Destination/source KB fidelity may be opaque

**Complexity:** Medium
**Risk:** Medium (unsupported edge cases)

### Method 5: Middleware (Zapier, Make)

**How it works:** Use a middleware tool to connect eDesk and Groove via visual workflows.

**Reality check:** This approach does not work well for KB migration. Neither Zapier nor Make have pre-built triggers for eDesk KB article events. Groove's Zapier integration covers conversations and customers — not knowledge base operations. You would need custom HTTP modules on both sides, at which point you're building Method 3 inside a visual tool with worse error handling, no retry logic, and poor logging.

**When to use it:** Only for light post-cutover forward sync, not historical migration.

**Complexity:** Medium (for what you get)
**Risk:** High (fragile, no error recovery)

## Migration Method Comparison

| Method | Complexity | Best For | Scales To | Preserves Metadata | Handles Images |
|---|---|---|---|---|---|
| Manual copy-paste | Low | <30 articles | 30 articles | No | Manual re-upload |
| HTML scraping + API | Medium | 30–500 articles | 500 articles | Partial | With custom code |
| Custom ETL pipeline | High | 50+ articles | Unlimited | Yes (with effort) | Automated |
| Third-party tool | Medium | Low eng bandwidth | Medium | Varies by vendor | Verify in writing |
| Middleware (Zapier/Make) | Medium | Post-cutover sync only | 20 articles | No | No |

**Recommendations by scenario:**

- **Small team, <30 articles, no SEO concerns:** Manual copy-paste.
- **50–200 articles, engineering bandwidth available:** Custom ETL with Groove REST API.
- **200+ articles, organic traffic to KB, tight timeline:** Managed migration service or dedicated contractor with KB migration experience.
- **Ongoing sync needed (unlikely for KB, but possible):** Custom ETL with scheduled runs.

## Pre-Migration Planning

### Data Audit Checklist

Before extracting anything, inventory what you actually have in eDesk KB:

- [ ] **Total article count** (published + draft + archived)
- [ ] **Category structure** — how many categories, nesting depth (remember: Groove categories are flat)
- [ ] **Inline images per article** — count them; each one needs re-hosting
- [ ] **Embedded videos** — YouTube/Vimeo embeds vs. self-hosted
- [ ] **Internal cross-links** — articles linking to other eDesk KB articles (count these; each requires URL rewriting)
- [ ] **External links** — links to your store, marketplace pages, etc.
- [ ] **Custom CSS/JS** — any styling applied at the KB layout level
- [ ] **Password-protected articles** — Groove supports this at KB level only, not per-article; decide if you need a separate protected KB
- [ ] **Linked channels** — eDesk KB articles linked to specific support channels
- [ ] **AI training content** — articles used as AI training data in eDesk's Content Hub
- [ ] **URL structure** — note whether eDesk URLs embed numeric article IDs (e.g., `/kb/123-returns-policy`), as this affects redirect mapping
- [ ] **Articles with translations** — if you have manually maintained translations, plan for Groove's locale-based system

### Identify What Not to Migrate

Not everything deserves to move:

- **Outdated articles** with zero views in the last 6 months — archive, don't migrate.
- **Duplicate articles** created for different marketplace channels with identical content.
- **Placeholder/draft articles** that were never published.
- **Channel-specific content** that references eDesk-specific features (the snippet shortcut, eDesk chatbot behavior).

### Migration Strategy

For knowledge base migrations, **big-bang cutover is almost always the right approach.** KB content is static — it doesn't change during migration the way live ticket data does. Extract everything, transform it, load it into Groove, validate, then cut over DNS and redirects in one move.

Phased migration only makes sense if you're migrating multiple eDesk KB libraries (one per shop) into separate Groove knowledge bases and want to do them sequentially.

> [!WARNING]
> If you're also migrating CRM-type data (accounts, contacts, leads, opportunities, activities), treat those as separate workstreams. Groove is a shared inbox, not a CRM — it lacks native Lead or Opportunity objects. Do not bury non-KB data inside the KB migration scope.

## Data Mapping: eDesk KB → Groove KB

### Object-Level Mapping

| eDesk KB Object | Groove KB Object | Notes |
|---|---|---|
| Library | Knowledge Base | 1:1 if single library. Multiple eDesk libraries → multiple Groove KBs |
| Category | Category | Map by title. Groove adds slug, SEO metadata, positioning |
| Subcategory | Flattened category or naming convention | Groove's category schema has no parent field — flatten or prefix names |
| Article | Article | Core content unit. HTML body, tags, category assignment |
| Inline image | Inline image (re-hosted) | Must download from eDesk CDN and re-upload or host externally |
| Keywords | Tags | eDesk "keywords" map to Groove's tag array |
| Article tags | Tags | Direct mapping; merge with keywords |
| Password protection | Password protection | Groove supports at KB level only, not per-article |
| Layout/CSS | KB settings/theme | Manual rebuild — no automated transport |

### Field-Level Mapping: Articles

| eDesk Article Field | Groove Article Field | Transformation Required |
|---|---|---|
| Title | `title` | None |
| Body (HTML) | `body` | HTML cleanup, image URL replacement, internal link rewriting |
| Category | `category_id` | Lookup Groove category ID by title |
| Keywords | `tags []` | Merge with any existing tags; normalize case |
| Tags | `tags []` | Direct mapping |
| Author | `author_id` | Map to Groove agent/user ID |
| Published status | State (publish/unpublish) | Use publish endpoint after creation |
| Created date | `created_at` | **Not settable via Groove REST API.** Original publish dates are lost. Document the original dates in a separate record for reference. |
| Updated date | `updated_at` | Set by Groove on write — reflects migration date, not original edit date |
| URL slug | `slug` | Extract from eDesk URL path or generate from title. If eDesk URLs embed numeric IDs (e.g., `/kb/123-returns-policy`), strip the ID prefix when generating the Groove slug. |
| — | `description` | Generate from first 160 chars of body or meta description |
| — | `meta_robots` | Default: `INDEX,FOLLOW` |
| — | `og_title`, `og_description` | Populate from title/description for SEO parity |
| — | `related_ids []` | Set after all articles created (requires second pass) |
| — | `position` | Set to maintain display order |
| — | `locale` | Set if using Groove's translation system; defaults to KB's primary language |

### Field-Level Mapping: Categories

| eDesk Category Field | Groove Category Field | Notes |
|---|---|---|
| Category name | `title` | Direct; handle duplicates across libraries |
| Parent category | — | No direct field in Groove; flatten or prefix |
| — | `slug` | Auto-generated or set manually |
| — | `description` | Optional; populate for SEO |
| — | `position` | Controls display order |
| — | `featured` | Boolean |

## Migration Architecture and API Constraints

### Data Flow

```
eDesk KB (HTML scrape / manual JSON capture)
        ↓
   [Extract Layer]
   - Crawl published articles
   - Parse HTML structure
   - Download inline images
        ↓
   [Transform Layer]
   - Sanitize HTML (strip eDesk-specific classes/scripts)
   - Replace image URLs with re-hosted versions
   - Flatten subcategories
   - Map categories → Groove category IDs
   - Generate slugs, descriptions, SEO metadata
   - Update internal cross-links (requires full article inventory)
        ↓
   [Load Layer]
   - POST categories to Groove KB API
   - POST articles to Groove KB API
   - PUT publish on each article
   - Set related_ids in second pass
        ↓
   [Validate]
   - Compare article counts
   - Spot-check rendered content
   - Verify image rendering
   - Test internal links and redirects
```

### eDesk Extraction Constraints

- **No public KB API.** eDesk's documented API covers tickets, messages, orders, tags, templates, and custom fields — not knowledge base articles. ([developers.edesk.com](https://developers.edesk.com/))
- **CSV export is irrelevant for KB content.** The export feature covers ticket/reporting data and is capped at 1,000 rows per download.
- **Enterprise plan required** for any API access.
- **Internal API exists** but is undocumented — browser network inspection during KB editor use reveals JSON payloads, but relying on internal APIs is fragile and may break without notice.
- **Media upload limit:** 10MB per file. ([support.edesk.com](https://support.edesk.com/edesk-knowledge-base?utm_source=openai))
- **Rate limit (public API):** 60 requests per minute with a restore rate of 2 requests per second. ([developers.edesk.com](https://developers.edesk.com/reference/rate-limit))

### Groove KB API Capabilities

- **REST API v1** (deprecated but functional): Full CRUD for knowledge bases, categories, articles, settings, and translations. Groove's docs state: "Groove REST API is no longer in active development; please use the new GraphQL API."
- **GraphQL API v2** (recommended): Endpoint is `https://api.groovehq.com/v2/graphql`. Covers conversations, customers, and KB. KB mutations may still be evolving — verify available mutations via introspection query before building.
- **Authentication:** Bearer token in the `Authorization` header. API keys are account-level; Groove does not document granular scopes for KB-specific write access.
- **Rate limits:** Plan-based: 200 calls/min (Starter), 400 calls/min (Plus), 800 calls/min (Pro). Build exponential backoff and checkpointing from day one.
- **No bulk import endpoint** — articles must be created one at a time.
- **`created_at` is not settable** — original publish dates from eDesk cannot be preserved in Groove. The `created_at` timestamp will reflect the migration date.
- **Public search API:** Unauthenticated endpoints for published articles and categories — useful for post-migration validation, but returns 404 for password- or IP-protected KBs.

> [!WARNING]
> Groove's REST API is labeled "no longer in active development." The REST KB endpoints still work as of July 2025, but plan for possible deprecation. If building a new integration from scratch, check GraphQL KB mutation availability first via schema introspection, and fall back to REST only where GraphQL coverage is incomplete.

## Step-by-Step Migration Process

### Step 1: Extract Articles from eDesk

Since there's no KB export API, use one of these extraction approaches:

**Option A: Web scraping (recommended for most teams)**

```python
import requests
from bs4 import BeautifulSoup
import json
import os
import time
import re
from urllib.parse import urljoin

def scrape_edesk_kb(base_url, output_dir="edesk_export"):
    """
    Scrape published articles from an eDesk KB.
    
    IMPORTANT: CSS selectors below are illustrative. Before running:
    1. Open your eDesk KB in a browser
    2. Right-click an article link → Inspect to find the actual link selector
    3. Right-click article body content → Inspect to find the body container
    4. Note the image CDN domain (typically cdn.edesk.com or a subdomain)
    
    Common eDesk KB HTML patterns observed:
    - Article links: a[href*="/article/"], a.article-title, li.article-item a
    - Body container: div.article-body, div.article-content, main .content
    - Breadcrumb for category: ol.breadcrumb li, nav.breadcrumb span
    - Image domain: cdn.edesk.com, or {yourdomain}.edesk.com/uploads
    """
    os.makedirs(output_dir, exist_ok=True)
    os.makedirs(f"{output_dir}/images", exist_ok=True)
    
    # Fetch the KB homepage to discover category links
    resp = requests.get(base_url)
    soup = BeautifulSoup(resp.text, "html.parser")
    
    articles = []
    
    # CUSTOMIZE THIS SELECTOR for your eDesk KB layout
    for link in soup.select("a[href*='/article/']"):
        article_url = urljoin(base_url, link.get("href"))
        
        art_resp = requests.get(article_url)
        art_soup = BeautifulSoup(art_resp.text, "html.parser")
        
        title_el = art_soup.find("h1")
        title = title_el.get_text(strip=True) if title_el else "Untitled"
        
        # CUSTOMIZE THIS SELECTOR for your eDesk KB layout
        body_div = art_soup.find("div", class_="article-body")
        body_html = str(body_div) if body_div else ""
        
        # Extract category from breadcrumb if available
        breadcrumb = art_soup.select("ol.breadcrumb li")
        category = breadcrumb[-2].get_text(strip=True) if len(breadcrumb) >= 2 else "Uncategorized"
        
        # Extract slug from URL, stripping numeric ID prefix if present
        url_path = article_url.rstrip("/").split("/")[-1]
        slug = re.sub(r"^\d+-", "", url_path)  # Remove leading "123-" pattern
        
        # Download inline images
        for img in (art_soup.find_all("img") if body_div else []):
            img_url = img.get("src")
            if img_url:
                img_url = urljoin(article_url, img_url)
                img_name = img_url.split("/")[-1].split("?")[0]
                try:
                    img_data = requests.get(img_url, timeout=10).content
                    with open(f"{output_dir}/images/{img_name}", "wb") as f:
                        f.write(img_data)
                    body_html = body_html.replace(img.get("src"), f"REHOST_PLACEHOLDER/{img_name}")
                except requests.RequestException:
                    print(f"  Warning: Failed to download image {img_url}")
        
        articles.append({
            "title": title,
            "body": body_html,
            "source_url": article_url,
            "slug": slug,
            "category": category
        })
        time.sleep(1)  # Polite crawling — adjust for your eDesk instance
    
    with open(f"{output_dir}/articles.json", "w") as f:
        json.dump(articles, f, indent=2)
    
    print(f"Extracted {len(articles)} articles")
    return articles
```

**Option B: Manual JSON capture**

Open the eDesk KB editor in your browser. Open DevTools → Network tab. Click through each article. Copy the JSON response payloads. This gives you structured data but requires manual effort per article and depends on undocumented internal endpoints.

### Step 2: Transform Content

Key transformations:

1. **HTML sanitization:** Strip eDesk-specific CSS classes, `data-` attributes, and tracking scripts. Remove wrapper divs that are layout artifacts, not content.
2. **Image URL replacement:** Swap all eDesk CDN URLs (typically matching `cdn.edesk.com` or your instance's upload domain) with your re-hosted image URLs.
3. **Internal link rewriting:** Replace `https://your-edesk-kb.edesk.com/article/...` with the new Groove URL pattern. This requires knowing the target slug for each article, so you need the full article list before rewriting links. Build a lookup table: `{edesk_url: groove_slug}` and do a string replacement pass across all article bodies.
4. **Subcategory flattening:** Groove's category schema is flat. Convert nested paths (e.g., `Shipping > International > EU`) to flat labels (`Shipping - International - EU`) or separate categories.
5. **Slug generation:** Create URL-friendly slugs from article titles or original URL paths. If eDesk URLs contain numeric IDs (`/kb/123-returns-policy`), strip the ID and keep only the text portion. Preserve slug text where possible to minimize redirect complexity.
6. **Tag normalization:** Merge eDesk keywords and tags into a single tag array for Groove. Deduplicate, normalize case (lowercase), and remove any tags that are eDesk-specific channel identifiers.
7. **Encoding verification:** Verify UTF-8 encoding is preserved. eDesk articles with em-dashes (—), curly quotes (" "), or non-Latin scripts are common sources of encoding failures. Decode and re-encode as UTF-8 during transformation.

### Step 3: Load into Groove

Create categories first, then articles, then publish. Persist a source-to-destination ID mapping so you can rerun failed batches without creating duplicates:

```python
import json

def load_to_groove(articles, kb_id, api_key, category_map, id_map_file="id_map.json"):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    base = f"https://api.groovehq.com/v1/kb/{kb_id}"
    
    # Load existing ID map for idempotency
    try:
        with open(id_map_file) as f:
            id_map = json.load(f)
    except FileNotFoundError:
        id_map = {}
    
    results = []
    
    for article in articles:
        # Skip if already migrated (idempotency)
        if article["source_url"] in id_map:
            print(f"  Skipping (already migrated): {article['title']}")
            continue
        
        payload = {
            "title": article["title"],
            "body": article["body"],
            "category_id": category_map.get(article["category"]),
            "tags": article.get("tags", []),
            "slug": article.get("slug"),
            "description": article.get("description", article["body"][:160] if article["body"] else ""),
            "meta_robots": "INDEX,FOLLOW",
            "og_title": article["title"],
            "og_description": article.get("description", ""),
        }
        
        resp = create_with_backoff(f"{base}/articles", payload, headers)
        
        if resp.status_code in (200, 201):
            art_id = resp.json()["article"]["id"]
            # Publish the article
            requests.put(f"{base}/articles/{art_id}/publish", headers=headers)
            
            id_map[article["source_url"]] = {
                "groove_id": art_id,
                "groove_slug": article.get("slug")
            }
            results.append({"title": article["title"], "groove_id": art_id, "status": "ok"})
        elif resp.status_code == 422 and "slug" in resp.text.lower():
            # Slug collision — append suffix and retry
            article["slug"] = article["slug"] + "-2"
            print(f"  Slug collision, retrying with: {article['slug']}")
            # Retry with modified slug (simplified; production code should loop)
        else:
            results.append({"title": article["title"], "status": "error", 
                          "code": resp.status_code, "body": resp.text})
            print(f"  ERROR: {article['title']} — {resp.status_code}: {resp.text}")
        
        # Persist ID map after each write for crash recovery
        with open(id_map_file, "w") as f:
            json.dump(id_map, f, indent=2)
        
        time.sleep(0.5)  # Throttle to stay under rate limits
    
    return results, id_map
```

### Step 4: Set Related Articles, SEO Metadata, and Translations

After all articles are loaded, make a second pass to set related article links and configure locale-based translations if applicable:

```python
# Second pass: link related articles and set SEO metadata
for source_url, mapping in id_map.items():
    article_id = mapping["groove_id"]
    metadata = article_metadata.get(source_url, {})
    
    update_payload = {
        "related_ids": metadata.get("related_ids", []),
        "meta_robots": "INDEX,FOLLOW",
        "og_title": metadata.get("title"),
        "og_description": metadata.get("description"),
    }
    resp = requests.put(
        f"{base}/articles/{article_id}",
        json=update_payload,
        headers=headers
    )
    
    if resp.status_code == 200:
        # Republish since editing puts article into 'wip' state
        requests.put(f"{base}/articles/{article_id}/publish", headers=headers)
    
    time.sleep(0.5)
```

**Handling translations:** If you have article content in multiple languages, Groove's translation system uses the REST endpoint `POST /v1/kb/:kb_id/articles/:article_id/translations` with a `locale` parameter (e.g., `fr`, `de`, `es`). Each translation is a separate article body associated with the parent article's ID. Create the primary-language article first, then add translations as a separate step.

### Step 5: Generate the Redirect Map

Map every old eDesk URL to the new Groove URL. eDesk KB URLs often embed numeric article IDs (e.g., `/kb/123-returns-policy`), while Groove URLs follow a `/{kb-slug}/{category-slug}/{article-slug}` pattern. Account for this structural difference:

```python
# Build redirect map from source-to-destination ID map
redirects = []
for source_url, mapping in id_map.items():
    # Extract the path from the full eDesk URL
    from urllib.parse import urlparse
    old_path = urlparse(source_url).path  # e.g., /kb/123-returns-policy
    new_path = f"/help/{mapping['groove_slug']}"  # Groove URL pattern
    redirects.append(f"{old_path} {new_path} 301;")

# Also redirect category pages
for edesk_cat, groove_cat_slug in category_redirect_map.items():
    redirects.append(f"/kb/category/{edesk_cat} /help/{groove_cat_slug} 301;")

with open("redirects.conf", "w") as f:
    f.write("\n".join(redirects))

print(f"Generated {len(redirects)} redirect rules")
```

### Step 6: Validate

See the Validation section below.

## Edge Cases and Failure Modes

- **Embedded videos:** eDesk articles with YouTube or Vimeo embeds use `<iframe>` tags. Groove's editor may strip or alter iframes. Test embed rendering in Groove before bulk loading. If iframes are stripped, use Groove's raw HTML mode to insert them post-import.
- **Tables:** HTML tables from eDesk may not render cleanly in Groove's article viewer. Inspect and simplify complex table structures. Tables with `colspan`/`rowspan` are particularly prone to rendering issues.
- **Custom CSS:** eDesk KB layouts support custom CSS. This does not transfer. Any visual customization needs to be rebuilt in Groove's theme editor.
- **Image hotlinking:** If you don't re-host images and your eDesk account is deactivated, every inline image in your Groove articles will return a 404. Always re-host. Verify by searching for the eDesk CDN domain (typically `cdn.edesk.com`) across all migrated article bodies.
- **Duplicate categories:** eDesk allows the same category name across different libraries. Groove uses slugs — duplicate names will generate duplicate slugs, causing a 422 error. Rename before loading.
- **Article ordering:** eDesk's article display order may be implicit (alphabetical or manual drag). Groove uses a `position` integer. Map the order explicitly during extraction by recording the display sequence.
- **Draft articles:** Scraping only captures published content. If you need drafts, extract them via the eDesk editor manually using the JSON capture approach.
- **Character encoding:** eDesk articles with special characters (em-dashes, curly quotes, non-Latin scripts) should be verified after import. UTF-8 mismatches cause garbled text. Decode all content as UTF-8 during the transform step and reject any articles with encoding errors.
- **Internal cross-links:** If Article A links to Article B in eDesk, that link will break in Groove unless you parse the HTML and update the `href` to Article B's new Groove URL. This requires a two-pass approach: load all articles first, build the URL map, then update internal links. The second update puts articles into `wip` state — you must republish each updated article.
- **Slug collisions:** Duplicate titles generate duplicate slugs. Build collision detection into your load script: if a `POST` returns a 422 with a slug-related error, append a numeric suffix (`-2`, `-3`) and retry. Log all collisions for manual review.
- **Article lifecycle state change on edit:** Editing a published Groove article via the API puts it into `wip` (work-in-progress) state. Any second-pass updates (related articles, SEO metadata, internal link rewrites) require a subsequent `PUT .../publish` call to make the article publicly visible again.

## Limitations and Constraints

### eDesk Limitations
- No public API for KB articles — extraction is always indirect
- CSV export covers ticket data only, not KB content; capped at 1,000 rows
- API access requires Enterprise plan
- Media upload limit of 10MB per file
- Rate limit: 60 requests/minute, 2 requests/second restore rate

### Groove Limitations
- REST API is deprecated — KB endpoints work as of July 2025 but may be removed
- GraphQL KB mutations may be incomplete — verify via schema introspection
- No bulk import endpoint — articles must be created one at a time
- `created_at` is not settable via API — original publish dates are lost
- Category schema is flat — no parent-category field for nested hierarchies
- No native article versioning or revision history import
- Groove KB doesn't have a bulk article export feature either (relevant for rollback planning)
- Editing a published article via API puts it into `wip` until you republish
- API keys are account-level; no granular KB-specific scopes documented
- Custom domain configuration: Groove KBs require a CNAME record pointing to Groove's servers. The KB URL structure (`/{kb-slug}/{category-slug}/{article-slug}`) is fixed — you cannot customize the path hierarchy

## Rollback Procedure

If the migration fails mid-load or validation reveals unacceptable content loss:

1. **Keep eDesk serving traffic** until Groove passes all validation checks. Do not update DNS or redirect rules until validation is complete.
2. **Track migration state via the ID map file.** The `id_map.json` persisted during loading tells you exactly which articles were successfully created in Groove.
3. **To roll back a partial load:** Use the ID map to identify all created Groove articles. Unpublish each via `PUT /v1/kb/:kb_id/articles/:id/unpublish`, or delete them via `DELETE /v1/kb/:kb_id/articles/:id`. Groove's REST API supports both operations.
4. **To rerun a failed batch:** Because the load script checks the ID map before creating each article, you can simply rerun the script. Already-migrated articles will be skipped; only failed ones will be retried.
5. **If Groove DNS was already pointed:** Revert the CNAME record to point back to eDesk. DNS propagation typically takes 5–30 minutes. During this window, some users will see the Groove KB and some will see eDesk.

## Validation and Testing

### Record Count Comparison

| Check | Method |
|---|---|
| Total articles extracted | Count entries in export JSON |
| Total articles in Groove | `GET /v1/kb/:kb_id/categories` → sum `articles_count` per category |
| Category count match | Compare source category list vs. Groove categories |
| Published vs. draft status | `GET /v1/kb/:kb_id/articles` → filter by `state` field |
| Image count | Grep all article bodies for `<img` tags; count unique `src` values |

### Content Validation

1. **Spot-check 10% of articles:** Open side-by-side in eDesk and Groove. Compare rendered output — check headings, lists, tables, and inline formatting.
2. **Image audit:** Grep all Groove article bodies for `src=` attributes. Verify each URL returns HTTP 200. Flag any URLs still pointing to eDesk's CDN domain.
3. **Internal link check:** Search for any remaining eDesk URLs (`edesk.com`) in Groove article bodies. All should have been rewritten to Groove URLs.
4. **Encoding check:** Search for common encoding artifacts: `â€™` (curly apostrophe), `â€"` (em-dash), `Ã©` (accented e). These indicate UTF-8 was misinterpreted as Latin-1.
5. **Search test:** Use Groove's public search API to search for key terms. Verify expected articles appear.

```bash
# Quick search validation via Groove public API
curl -s "https://api.groovehq.com/v1/kb/public/YOUR_KB_ID/articles/search?keyword=shipping" \
  -H "Content-Type: application/json" | python -m json.tool

# Image URL audit — find any remaining eDesk CDN references
grep -r "cdn.edesk.com\|edesk.com/uploads" exported_groove_articles/ 
```

### UAT Process

1. Share the Groove KB preview URL with 2–3 support agents.
2. Have them search for common customer questions.
3. Verify article links work when inserted into ticket replies.
4. Check mobile rendering — Groove's KB theme may render differently on mobile than eDesk's.
5. Test password-protected KB access if applicable.
6. For rollback, keep the old eDesk KB serving traffic until the new Groove KB passes redirect and content QA.

## Post-Migration Tasks

### URL Redirects

This is the most commonly missed step. If your eDesk KB had organic search traffic, every old URL needs a 301 redirect to the new Groove URL.

1. Build a redirect map: `edesk_url → groove_url` for every article and category page.
2. Implement redirects at the DNS/CDN level (Cloudflare page rules, Netlify `_redirects` file, nginx `rewrite` rules, or AWS CloudFront functions).
3. Submit an updated sitemap to Google Search Console.
4. Use the `Change of Address` tool in Search Console if the domain is changing entirely.

### DNS and Custom Domain

Groove KBs support custom domains via CNAME record. Point your `support.yourdomain.com` (or whatever subdomain served the eDesk KB) to Groove's designated hostname. Configure the custom domain in Groove's KB settings → Domain tab. SSL is provisioned automatically by Groove after DNS propagation.

If eDesk used a different subdomain than what you plan for Groove, add a redirect from the old subdomain to the new one.

### Agent Training

- Show agents how to insert Groove KB articles into replies (different workflow than eDesk's snippet shortcut).
- Update any saved macros or templates that reference eDesk KB URLs.
- Familiarize agents with Groove's article editor — particularly the `draft → published → wip → republished` lifecycle, which differs from eDesk's simpler `draft → published` model.

### AI Training Content

If you were using eDesk KB articles as AI training content for eDesk's chatbot or Smart Reply, you'll need to rebuild that integration. Groove's KB articles can be accessed programmatically via the REST API (`GET /v1/kb/:kb_id/articles`) or GraphQL for AI training pipelines.

### Monitoring

For the first 30 days after cutover:
- Monitor Google Search Console for crawl errors and 404 spikes.
- Check support ticket volume — a spike may indicate broken self-service.
- Watch Groove KB analytics for zero-view articles (possible redirect failures).
- Search for your old eDesk KB domain in Google to verify redirects are being followed and indexed.

## Best Practices

1. **Backup everything before starting.** Save a complete copy of your eDesk KB content (HTML + images) locally before any account changes.
2. **Implement a content freeze.** Stop publishing new articles in eDesk 48 hours before cutover to avoid delta-sync complexity.
3. **Run a test migration first.** Create a test Knowledge Base in Groove, load 10 articles, verify rendering, then delete the test KB and run the full migration.
4. **Re-host images immediately.** Don't wait. If your eDesk subscription lapses, images on their CDN will 404.
5. **Validate incrementally.** Check after each batch of 50 articles, not after loading everything.
6. **Generate the redirect map before cutover.** Not after. The redirect map is a deliverable, not an afterthought.
7. **Preserve slugs where possible.** If eDesk used readable URL slugs, replicate them in Groove to minimize redirect complexity.
8. **Document your category mapping.** Keep a spreadsheet linking eDesk category names (including subcategories) to Groove category IDs and slugs. You'll need it for debugging.
9. **Make writes idempotent.** Persist source-to-destination ID mappings so you can rerun failed batches without duplicating content.
10. **Remember to republish after second-pass updates.** Any PUT to an already-published article puts it into `wip` state. Your script must call the publish endpoint again after updating related articles, SEO metadata, or internal links.

## Sample Data Mapping Table

| # | eDesk Source | Groove Target | Transform |
|---|---|---|---|
| 1 | Article title | `article.title` | None |
| 2 | Article body (HTML) | `article.body` | Strip eDesk classes, replace image URLs, rewrite internal links |
| 3 | Category name | `category.title` → `article.category_id` | Lookup by name; flatten subcategories |
| 4 | Parent category | Flattened into category title | Prefix or rename (e.g., `Shipping - International - EU`) |
| 5 | Article keywords | `article.tags []` | Merge into tag array, normalize to lowercase |
| 6 | Article tags | `article.tags []` | Merge with keywords, deduplicate |
| 7 | Inline image URL | Re-hosted URL in body HTML | Download from eDesk CDN + re-upload |
| 8 | Published status | `publish` endpoint | POST article then PUT publish |
| 9 | URL path | `article.slug` | Extract from eDesk URL, strip numeric ID prefix |
| 10 | Article author | `article.author_id` | Map to Groove user ID |
| 11 | Library title | `KnowledgeBase.title` | Direct |
| 12 | Library language | `KnowledgeBase.language` | Direct; sets default locale |
| 13 | Layout/CSS | KB settings/theme | Manual rebuild in Groove theme editor |
| 14 | — | `article.description` | Generate from first 160 chars of body |
| 15 | — | `article.meta_robots` | Default: `INDEX,FOLLOW` |
| 16 | — | `article.related_ids []` | Set in second pass after all articles created |
| 17 | Old article URL | Redirect map | External 301 redirect rule (nginx/Cloudflare/etc.) |
| 18 | — | `article.locale` | Set if using Groove's translation system |
| 19 | Created date | Not settable | Document original dates externally for reference |

This table reflects the documented fields and constraints on both platforms as of July 2025. ([support.edesk.com](https://support.edesk.com/edesk-knowledge-base?utm_source=openai))

## When to Build In-House vs. Hire Help

**Build in-house when:**
- You have fewer than 50 articles with simple formatting (text + basic images)
- A developer is available for 2–5 days
- No SEO traffic to the KB (redirect mapping is optional)
- You're comfortable with the extraction constraint: eDesk has no documented KB export path

**Hire a migration specialist when:**
- You have 100+ articles with complex HTML, embedded media, or multiple KB libraries
- Organic search traffic depends on KB URLs — redirects must be comprehensive
- The migration is part of a broader helpdesk move (tickets + KB together)
- Your team doesn't have engineering bandwidth to build and debug a custom ETL pipeline
- You need to preserve content fidelity across languages, complex table layouts, or embedded interactive elements

The hidden cost of a DIY KB migration is not the initial script — it's the debugging cycle for the articles where images broke, iframes got stripped, encoding failed, or slugs collided. That tail of edge cases typically consumes more engineering time than the initial build.

## What Comes Next

Once your KB is live in Groove, audit it at 7, 14, and 30 days. Check for broken images, 404s in Search Console, and any support tickets that reference missing articles. Rebuild what lives around the articles: custom domain SSL verification, translations, KB settings (password protection, IP restrictions), contact form / rating widget configuration, and inbox-side article linking for agents.

A well-executed knowledge base migration is invisible to your customers — they search, they find answers, and they never know the platform changed.

For a full pre-and-post migration checklist, see our [knowledge base migration checklist](https://clonepartner.com/blog/blog/the-ultimate-knowledge-base-migration-checklist-a-zero-downtime-plan/).

## Frequently asked questions

### Can I export knowledge base articles from eDesk?

eDesk does not provide a dedicated KB article export or API endpoint. The CSV export feature covers ticket reporting data only (capped at 1,000 rows). To extract KB articles, you need to scrape your published KB pages or manually capture article data from the browser's developer tools.

### Does Groove have an API for importing knowledge base articles?

Yes. Groove's REST API v1 has full CRUD endpoints for knowledge bases, categories, and articles (POST to create, PUT to publish). Groove also has a GraphQL v2 API. The REST API is deprecated but functional as of July 2025. There is no bulk import endpoint — articles must be created one at a time.

### Can Groove preserve eDesk subcategories?

Not directly. eDesk supports parent categories and subcategories, but Groove's documented KB category schema is flat with no parent-category field. Most migrations flatten nested paths into renamed top-level categories (e.g., 'Returns / EU').

### Will I lose SEO rankings when migrating from eDesk KB to Groove KB?

You will lose rankings if you don't implement 301 redirects from every old eDesk article URL to the corresponding Groove URL. Build a complete redirect map before cutover and submit an updated sitemap to Google Search Console immediately after.

### Can Zapier or Make migrate eDesk knowledge base to Groove?

Not effectively. Neither platform has pre-built triggers for eDesk KB articles or Groove KB operations. You would need custom HTTP modules on both sides, making it more complex than a direct API script with no meaningful advantage. Middleware is only suitable for light post-cutover forward sync.
