---
title: "eDesk to SurveySparrow Ticket Migration: Technical Guide"
slug: edesk-to-surveysparrow-ticket-migration-technical-guide
date: 2026-08-17
author: Roopi
categories: [Migration Guide, Help Desk]
excerpt: "Technical guide to migrating tickets from eDesk to SurveySparrow. Covers API constraints, field mapping, eCommerce data handling, rate limits, and step-by-step process."
tldr: "eDesk to SurveySparrow requires a custom API-to-API pipeline — no native importer exists. Key challenges: timestamp loss, eCommerce data flattening, per-message extraction overhead, and SurveySparrow's low API rate limits."
canonical: https://clonepartner.com/blog/edesk-to-surveysparrow-ticket-migration-technical-guide/
---

# eDesk to SurveySparrow Ticket Migration: Technical Guide


# eDesk to SurveySparrow Ticket Migration: Technical Guide

*Verified against eDesk API v1 and SurveySparrow API v3.*

Migrating from eDesk to SurveySparrow Ticket Management means translating an **eCommerce-native helpdesk** — where tickets are coupled to marketplace channels, sales orders, tracking links, and order-specific ticket types like `ReturnRequest` and `WrongItemReceived` — into a **feedback-first ticketing module** designed around survey responses, NPS detractors, and form submissions. These two platforms model support data under fundamentally different architectural assumptions.

No native migration path exists between them. No built-in importer on either side. No major third-party migration tool currently offers a verified eDesk-to-SurveySparrow connector. Zapier and n8n can connect both platforms for real-time event-based workflows, but neither handles bulk migration of thousands of tickets with threaded conversations and attachments.

Every migration requires extracting data via eDesk's REST API, transforming payloads to match SurveySparrow's ticket and contact schema, and loading through SurveySparrow's Ticket API (v3). This guide covers the full technical path: API constraints, field mapping, extraction strategies, the step-by-step migration process, delta sync, timeline estimation, and the edge cases that silently corrupt your data.

> [!WARNING]
> **Plan for a custom API migration.** eDesk's CSV exports from Insights contain ticket metadata only — no conversation threads, no attachments, no internal notes. The Search Download is Enterprise-only and capped at 1,000 rows. SurveySparrow's CSV import only captures the initial ticket body — it cannot thread historical replies or preserve private notes. A custom API-to-API migration is mandatory for preserving complete ticket histories. ([support.edesk.com](https://support.edesk.com/search-filter-download))

For related migrations, see our [eDesk to Pylon Migration Guide](https://clonepartner.com/blog/blog/edesk-to-pylon-migration-a-technical-guide/), [Freshdesk to SurveySparrow Ticket Migration Guide](https://clonepartner.com/blog/blog/freshdesk-to-surveysparrow-ticket-migration-technical-guide/), and [Zammad to SurveySparrow Ticket Migration Guide](https://clonepartner.com/blog/blog/zammad-to-surveysparrow-ticket-migration-technical-guide/).

## eDesk vs SurveySparrow: Architecture Differences

Before writing extraction scripts, understand how these data models diverge. Forcing eDesk's structure into SurveySparrow without transformation results in orphaned messages and lost context.

**eDesk** is an eCommerce-native helpdesk for multi-channel sellers. Tickets are organized around marketplace channels (Amazon, eBay, Shopify) and linked to sales orders, tracking links, and order notes. Each ticket contains a thread of messages — separate API objects with their own endpoints. Ticket types are eCommerce-specific: `Cancellation`, `ReturnRequest`, `OrderQuery`, `RefundRequest`, `ShippingQuery`, and 29 total enumerated types. Contacts are often secondary — marketplaces frequently mask customer emails (e.g., `12345abcde@marketplace.amazon.com`). ([support.edesk.com](https://support.edesk.com/n-a-16/what-is-a-ticket?utm_source=openai))

**SurveySparrow Ticket Management** is a feedback-first ticketing module. Tickets originate from survey responses, NPS detractor alerts, form submissions, or manual creation. The data model centers on contacts, ticket fields, priorities, statuses, and agents — not marketplace channels or sales orders. ([support.surveysparrow.com](https://support.surveysparrow.com/hc/en/articles/7080638598045-What-is-Ticket-Management-How-to-Manage-Customer-Responses-using-Ticket-Management/?utm_source=openai))

| Concept | eDesk | SurveySparrow |
|---|---|---|
| **Ticket origin** | Marketplace messages, email, chat, social | Surveys, NPS, forms, manual |
| **Ticket types** | 29 eCommerce-specific enums | No built-in type taxonomy |
| **Conversations** | Separate Messages API (per-message endpoints) | Comments/replies on ticket object |
| **Order data** | Sales orders, tracking links, order notes linked to tickets | No equivalent — must use custom fields |
| **Channels** | Amazon, eBay, Shopify, email, chat, social (per-channel SLAs) | No channel concept |
| **Custom fields** | Text, Date, Date & Time, Selectable, Multi-select, Number, Yes/No, URL, Email | Text, Dropdown, Multiselect, Date |
| **Statuses** | Open, Pending, Closed, Unread, Read, Spam, Scheduled, Archived, Priority, Unpriority | Open, Pending, Resolved, Closed (configurable) |
| **Contacts** | channel_id, client_id, full_name, phone_number, email | name, email, phone, custom properties |
| **Authentication** | Bearer token (Enterprise plan) | OAuth 2.0 or API token |

The biggest structural mismatch: **eDesk's eCommerce context has no native home in SurveySparrow.** Sales orders, tracking links, marketplace channel identifiers, and order-specific ticket types have no equivalent objects. You must flatten this data into SurveySparrow custom fields, append it to ticket descriptions, or accept the loss. Decide before migration, not during.

## API Constraints

Both platforms enforce constraints that dictate your migration architecture and timeline.

### eDesk API (Source)

- **Authentication:** Bearer token, generated per third-party application in eDesk settings. **API access is an Enterprise-plan feature.** If you don't have Enterprise, you can fall back to admin JSON exports (described below), but those are a poor fit for continuous sync. ([support.edesk.com](https://support.edesk.com/api/getting-started-with-the-edesk-api?utm_source=openai))
- **Rate limit:** 60 requests per minute with a restoration rate of 2 requests per second. Exceeding returns HTTP 429 with `"Out of quota"`. You can request a higher limit from eDesk support. ([developers.edesk.com](https://developers.edesk.com/reference/rate-limit?utm_source=openai))
- **Pagination:** Offset-based using `page` and `itemsPerPage` query parameters. Deep pagination on large accounts degrades performance — use date-range filtering (`filter_created_at_gte`, `filter_created_at_lte`) to keep payloads manageable.
- **Messages are separate API objects.** Each ticket returns a `messages_ids` array. You must call `GET /v1/messages/{messageId}` for each message to get the full body, sender, timestamps, and attachments. For a ticket with 10 messages, that's 11 API calls (1 ticket + 10 messages).

> [!NOTE]
> **The N+1 message problem.** At 60 requests/minute, a single 10-message ticket consumes ~11 seconds of your rate limit budget. For 5,000 tickets averaging 5 messages each, message extraction alone requires ~25,000 API calls — roughly 7 hours of continuous extraction at max throughput.

### eDesk Non-Enterprise Fallback

If your eDesk account is on the Growth or Basic plan, the REST API is not available. Your only extraction path is the admin JSON export:

1. In eDesk, navigate to **Settings → Data Management → Export**
2. Request a full export in JSON format
3. The export includes ticket metadata and message bodies, but excludes attachment binaries — you cannot retrieve attachment files through this path
4. The export is delivered as a download link via email, typically within 1–4 hours for accounts under 50,000 tickets
5. Attachment URLs in the exported JSON point to authenticated eDesk storage and will expire — they cannot be re-fetched without API access

For non-Enterprise migrations, attachments must either be abandoned, or you must upgrade to Enterprise temporarily to retrieve them via API before downgrading.

### SurveySparrow API (Target)

- **Base URL:** Region-specific — separate US, EU, AP, ME, UK, Sydney, and Canada endpoints. Confirm your data center before writing scripts. ([developers.surveysparrow.com](https://developers.surveysparrow.com/rest-apis/Introduction/?utm_source=openai))
- **Authentication:** OAuth 2.0 or API token (Bearer header).
- **Rate limit:** Plan-dependent. Documented limits vary by tier; the API returns HTTP 429 with a `Retry-After` header when exceeded. **Request your plan's specific quota from SurveySparrow support before starting, and ask for a temporary increase in writing before promising a cutover window.** If you cannot get official documentation of your limit, run a 100-ticket test batch while monitoring 429 frequency to estimate your actual ceiling empirically.
- **Batch endpoints:** Contacts and tickets have async batch-create endpoints that return a status token. Comments are only exposed as per-ticket create operations — one comment per request. The batch ticket endpoint does not expose the same field surface as single-ticket create (notably, attachment-capable fields are missing on batch). ([developers.surveysparrow.com](https://developers.surveysparrow.com/rest-apis/post-v-3-tickets-batch/?utm_source=openai))
- **Attachment restrictions:** SurveySparrow only accepts `pdf`, `png`, `jpeg`, `mp3`, `csv`, and `wav` attachments, with a **15 MB maximum** per file. DOCX, ZIP, EML, and larger binaries must be externalized. ([developers.surveysparrow.com](https://developers.surveysparrow.com/rest-apis/post-v-3-tickets/?utm_source=openai))
- **Custom field ID resolution:** SurveySparrow custom fields are referenced by numeric ID in API payloads, not by label. Before any write operation, call `GET /v3/ticket_fields` to retrieve the ID for each custom field you created. Build a label-to-ID lookup table and use it in every transform step. Writing to an incorrect field ID silently drops the data — no error is returned.
- **Sandbox environment:** SurveySparrow does not currently offer a publicly documented sandbox environment. Run all test loads against a dedicated test workspace (separate account) rather than your production workspace, and bulk-delete test tickets before switching to production data.

> [!WARNING]
> **SurveySparrow's rate limit is your bottleneck.** Comment writes — one per request — dominate the write operation count. At conservative rate limits, a 5,000-ticket migration with 5 comments each requires roughly 30,000 write operations. Request a rate limit increase from SurveySparrow *before* you begin, confirm the increased limit in writing, and calculate your migration window accordingly.

## The Timestamp Problem

This constraint changes the shape of every eDesk-to-SurveySparrow migration.

SurveySparrow's current create-ticket and create-comment endpoints **do not accept `created_at`, `updated_at`, or an explicit comment author field**. You can set requester, assignee, team, status, priority, and custom fields on tickets, and body plus `private` on comments — but not historical timestamps or authorship metadata. Imported records receive new platform timestamps. ([developers.surveysparrow.com](https://developers.surveysparrow.com/rest-apis/post-v-3-tickets/?utm_source=openai))

> [!CAUTION]
> **You cannot do a forensic replay of eDesk thread history in native SurveySparrow chronology.** If exact authorship and timing matter for your team, store source timestamps in custom fields and prepend metadata to every imported comment body.

A practical imported comment header:

```text
[Imported from eDesk]
Original author: jane@example.com
Original direction: Incoming
Original sent at: 2026-02-14T18:42:11Z
Original channel: Amazon UK

<original message body>
```

Not clean, but auditable, searchable, and honest about what the target platform preserves.

## Field Mapping: eDesk → SurveySparrow

Every field must be explicitly mapped or deliberately dropped.

### Ticket-Level Fields

| eDesk Field | SurveySparrow Field | Transformation Notes |
|---|---|---|
| `id` | Custom field: `edesk_ticket_id` | Idempotency key and audit trail |
| `subject` | `subject` | Enforce SurveySparrow's 200-character limit. Keep full original in custom field if truncated. |
| `status` | `status` (numeric) | See status mapping below |
| `created_at` | Custom field: `source_created_at` | SurveySparrow does not accept historical timestamps on create |
| `owner_user_id` | `assignee_id` | Pre-create agents. Map eDesk user IDs to SurveySparrow agent IDs. If an eDesk agent has no SurveySparrow account, map to a "Legacy Agent" placeholder. |
| `contact_id` | `contact` | Pre-create and deduplicate contacts first |
| `tags_ids` | `tags` | Pre-create matching tags, then map by name |
| `channel_id` | Custom field: `source_channel` (Dropdown) | No channel concept in SurveySparrow |
| `sales_order_id` | Custom field: `sales_order_id` (Text) | No order object in SurveySparrow |
| `external_order_id` | Custom field: `external_order_id` (Text) | Preserve lookup keys for external systems |
| `type` (e.g., `ReturnRequest`) | Custom field: `edesk_ticket_type` (Dropdown) | No equivalent taxonomy — pre-create all 29 values if reporting depends on type-based filtering |
| `custom_fields` | `custom_fields` | Map individually by numeric field ID. eDesk's Number, Yes/No, URL, Email types → flatten to Text or Dropdown |
| `time_left_to_reply` | — | SLA metadata, not transferable |

### Status Mapping

eDesk uses a granular status system. SurveySparrow uses Open, Pending, Resolved, and Closed, with ticket creation expecting numeric status values. Build the numeric map by calling `GET /v3/ticket_statuses` in a seeded SurveySparrow workspace — do not hardcode status IDs, as they vary by workspace.

| eDesk Status | SurveySparrow Status | Notes |
|---|---|---|
| Open | Open | Direct map |
| Unread | Open | — |
| Read | Open | — |
| Pending | Pending | Direct map |
| Scheduled | Pending | Store `source_status=Scheduled` in custom field |
| Closed | Closed or Resolved | — |
| Archived | Closed | Store `source_status=Archived` in custom field |
| Spam | Exclude | Unless audit requirements say otherwise |

### Message → Comment Mapping

eDesk stores conversations as separate Message objects. SurveySparrow stores them as comments on the ticket. For each eDesk message:

- Extract `body` (HTML), sender, `created_at`, and any attachment URLs
- Determine if it's an agent reply, customer message, or internal note
- Map to SurveySparrow comment with the appropriate visibility: public (`private=false`) or private (`private=true`)
- Sort chronologically. The first message becomes the ticket `description`; every subsequent message becomes a comment with a metadata header for author and timestamp fidelity.

| eDesk Message Type | SurveySparrow Comment | Notes |
|---|---|---|
| Incoming (customer) | Public comment (`private=false`) | Prepend metadata header |
| Outgoing (agent) | Public comment (`private=false`) | Prepend metadata header |
| Internal note | Private comment (`private=true`) | Verify SurveySparrow supports `private` flag on your plan |

**HTML sanitization:** eDesk message bodies are HTML. Before inserting into SurveySparrow comments, sanitize to strip potentially dangerous or unsupported tags while preserving readable content. Use Python's [`bleach`](https://pypi.org/project/bleach/) library (`bleach.clean(body, tags=ALLOWED_TAGS, strip=True)`) or `BeautifulSoup` with explicit tag filtering. Test your sanitization against a sample of your actual eDesk messages before running the full migration — marketplace-specific formatting and embedded order widgets frequently produce malformed output that breaks comment display.

### Contact Mapping

Match on email first. Use SurveySparrow's `referenceId` to store the eDesk source ID — prefer `referenceId` over `unique_id` if your source identifier contains dashes or symbols, because `unique_id` is alphanumeric-only.

**Deduplication is critical.** A single customer in eDesk may have multiple contact records — one per channel (Amazon, eBay, Shopify). SurveySparrow expects one contact per person. Deduplicate by email before import, and merge marketplace-specific identifiers into custom contact properties.

For marketplace-masked emails (e.g., `buyer-123@amazon.com`), retain the exact masked string so future replies route correctly if the marketplace proxy is still active.

### Custom Field Pre-Creation

Because SurveySparrow lacks native eCommerce fields, create these custom ticket fields before migrating anything. After creation, call `GET /v3/ticket_fields` to retrieve the numeric ID for each field — these IDs are required in all subsequent API write payloads.

| Field Name | Type | Purpose |
|---|---|---|
| `edesk_ticket_id` | Text | Idempotency key and audit trail |
| `source_channel` | Dropdown | eDesk channel names |
| `edesk_ticket_type` | Dropdown | All 29 eDesk ticket types if needed for reporting |
| `sales_order_id` | Text | eDesk order reference |
| `external_order_id` | Text | Marketplace order ID |
| `source_created_at` | Text | Original eDesk timestamp (ISO 8601) |
| `source_status` | Text | For statuses without a clean target mapping (Scheduled, Archived) |

## Step-by-Step Migration Process

### Step 1: Audit Your eDesk Instance

Before writing any code, inventory what you have:

```bash
# Get total ticket count
curl -s -H "Authorization: Bearer $EDESK_TOKEN" \
  "https://api.edesk.com/v1/tickets?itemsPerPage=1" | jq '.paginator.totalItemsCount'

# List all channels
curl -s -H "Authorization: Bearer $EDESK_TOKEN" \
  "https://api.edesk.com/v1/channels" | jq '.data[].name'

# List all tags
curl -s -H "Authorization: Bearer $EDESK_TOKEN" \
  "https://api.edesk.com/v1/tags" | jq '.data[] | {id, name}'

# Get ticket type distribution (sample 1,000 tickets)
curl -s -H "Authorization: Bearer $EDESK_TOKEN" \
  "https://api.edesk.com/v1/tickets?itemsPerPage=100&page=1" | jq '[.data[].type] | group_by(.) | map({type: .[0], count: length})'
```

Document: total ticket count, total messages (estimate: tickets × average messages/ticket), active channels, tags, custom fields, ticket type distribution, and attachment volume. This determines your rate limit budget and migration window.

Decide what is worth moving. In most eDesk exits, the defensible scope is:

- All contacts tied to open or recently active tickets
- All open and pending tickets with full thread history
- A closed-history window sized to your compliance or SLA audit requirements (typically 12–24 months)
- Only tags, order identifiers, and custom fields agents still reference

Skip spam tickets, `SystemMessage`-type tickets with no customer interaction, and channel-specific tickets for marketplaces you're discontinuing. A migration that moves 60% of tickets with 100% fidelity is better than one that moves 100% with corrupted or orphaned data.

### Step 2: Pre-Create Target Infrastructure in SurveySparrow

Before importing a single ticket:

1. **Create custom ticket fields** — see Custom Field Pre-Creation above
2. **Call `GET /v3/ticket_fields`** — retrieve and store the numeric ID for every field; write them to a `field_id_map.json` lookup file
3. **Create agents** matching your eDesk users
4. **Create contacts** — deduplicate by email across eDesk channels first (see Step 2a below)
5. **Create tags** matching your eDesk tag names
6. **Call `GET /v3/ticket_statuses`** — retrieve status IDs and build your status numeric map

Do not leave agent routing or field ID resolution as a migration-day exercise.

#### Step 2a: Pre-Create Contacts via Batch API

The contact creation step must happen before any ticket load. SurveySparrow's contact batch-create endpoint returns a status token — poll it until the batch completes before proceeding.

```python
import requests
import time

SS_BASE = "https://api.surveysparrow.com/v3"  # Confirm your region endpoint
SS_HEADERS = {
    "Authorization": f"Bearer {SS_TOKEN}",
    "Content-Type": "application/json"
}

def create_contacts_batch(contacts):
    """
    contacts: list of dicts with keys: name, email, phone, referenceId
    Returns: dict mapping edesk_contact_id -> surveysparrow_contact_id
    """
    # Submit batch
    resp = requests.post(
        f"{SS_BASE}/contacts/batch",
        headers=SS_HEADERS,
        json={"contacts": contacts}
    )
    resp.raise_for_status()
    batch_token = resp.json().get("data", {}).get("token")

    # Poll for completion
    while True:
        status_resp = requests.get(
            f"{SS_BASE}/contacts/batch/{batch_token}",
            headers=SS_HEADERS
        )
        result = status_resp.json().get("data", {})
        if result.get("status") == "completed":
            return {
                c["referenceId"]: c["id"]
                for c in result.get("contacts", [])
                if c.get("referenceId") and c.get("id")
            }
        elif result.get("status") == "failed":
            raise RuntimeError(f"Contact batch failed: {result}")
        time.sleep(5)

def deduplicate_contacts(edesk_contacts):
    """
    Merge eDesk contacts by email. One eDesk customer may have multiple
    contact records (one per channel). Returns a deduplicated list.
    """
    seen = {}
    for contact in edesk_contacts:
        email = contact.get("email") or f"masked-{contact['id']}@migration.internal"
        if email not in seen:
            seen[email] = {
                "name": contact.get("full_name", ""),
                "email": email,
                "phone": contact.get("phone_number", ""),
                "referenceId": str(contact["id"])  # eDesk contact ID as reference
            }
    return list(seen.values())
```

### Step 3: Extract from eDesk

Paginate through all tickets using `GET /v1/tickets`, ordered by `created_at` ascending:

```python
import requests
import time
import json

EDESK_BASE = "https://api.edesk.com/v1"
HEADERS = {"Authorization": f"Bearer {EDESK_TOKEN}"}

def extract_all_tickets(start_date=None, end_date=None):
    page = 1
    all_tickets = []
    params = {
        "itemsPerPage": 50,
        "order_by": "created_at",
        "order_direction": "asc"
    }
    if start_date:
        params["filter_created_at_gte"] = start_date  # ISO 8601
    if end_date:
        params["filter_created_at_lte"] = end_date

    while True:
        params["page"] = page
        resp = requests.get(f"{EDESK_BASE}/tickets", headers=HEADERS, params=params)
        if resp.status_code == 429:
            time.sleep(60)
            continue
        data = resp.json()
        tickets = data.get("data", [])
        if not tickets:
            break
        all_tickets.extend(tickets)
        page += 1
        time.sleep(1)  # Respect 60 req/min limit
    return all_tickets

def extract_messages(ticket):
    messages = []
    for msg_id in ticket.get("messages_ids", []):
        resp = requests.get(f"{EDESK_BASE}/messages/{msg_id}", headers=HEADERS)
        if resp.status_code == 429:
            time.sleep(60)
            resp = requests.get(f"{EDESK_BASE}/messages/{msg_id}", headers=HEADERS)
        messages.append(resp.json().get("data", {}))
        time.sleep(1)
    return sorted(messages, key=lambda m: m.get("created_at", ""))

def save_raw(ticket_id, ticket, messages):
    with open(f"raw/{ticket_id}.json", "w") as f:
        json.dump({"ticket": ticket, "messages": messages}, f)
```

> [!TIP]
> **Save raw extracts to disk.** Write each ticket + messages to a local JSON file before any transformation. This gives you a re-runnable source of truth without hitting eDesk's API again. Structure files as `raw/{ticket_id}.json`. If the migration spans multiple days, this cache prevents re-extraction and eliminates the risk of API changes mid-migration.

**Timezone warning:** eDesk's `filter_last_updated_at_gte` and `filter_last_updated_at_lte` are evaluated in the user's timezone. If your extraction script runs in UTC and your eDesk account is set to US/Eastern, you'll miss or double-count tickets at timezone boundaries. Normalize to UTC in your extraction logic, and use overlapping 48–72 hour windows for delta syncs to avoid gaps.

### Step 4: Transform

Map each eDesk ticket + messages into a SurveySparrow payload. Load `field_id_map.json` (created in Step 2) before running any transforms.

```python
import bleach
import json

# Load field ID map created in Step 2
with open("field_id_map.json") as f:
    FIELD_IDS = json.load(f)
# Example: {"edesk_ticket_id": 1042, "source_channel": 1043, ...}

ALLOWED_HTML_TAGS = ["p", "br", "b", "i", "u", "ul", "ol", "li", "a", "pre", "code"]

def sanitize_html(body):
    return bleach.clean(body or "", tags=ALLOWED_HTML_TAGS, strip=True)

def build_comment_header(msg):
    return (
        f"[Imported from eDesk]\n"
        f"Original author: {msg.get('sender', {}).get('email', 'unknown')}\n"
        f"Original direction: {msg.get('type', 'unknown')}\n"
        f"Original sent at: {msg.get('created_at', 'unknown')}\n"
        f"Original channel: {msg.get('channel', 'unknown')}\n\n"
    )

def transform_ticket(edesk_ticket, messages, agent_map, contact_map, channel_map):
    first_message = messages[0] if messages else {}
    return {
        "subject": edesk_ticket.get("subject", "(no subject)")[:200],
        "description": sanitize_html(first_message.get("body", "")),
        "status": map_status(edesk_ticket.get("status")),
        "priority": "Medium",  # eDesk uses Priority/Unpriority flags, not levels
        "assignee_id": agent_map.get(edesk_ticket.get("owner_user_id")),
        "contact_id": contact_map.get(edesk_ticket.get("contact_id")),
        "custom_fields": {
            FIELD_IDS["edesk_ticket_id"]: str(edesk_ticket.get("id")),
            FIELD_IDS["source_channel"]: channel_map.get(
                edesk_ticket.get("channel_id"), ""
            ),
            FIELD_IDS["edesk_ticket_type"]: edesk_ticket.get("type", ""),
            FIELD_IDS["sales_order_id"]: str(
                edesk_ticket.get("sales_order_id", "")
            ),
            FIELD_IDS["source_created_at"]: edesk_ticket.get("created_at", ""),
        },
        "comments": [
            {
                "body": build_comment_header(msg)
                    + sanitize_html(msg.get("body", "")),
                "private": msg.get("type") == "internal"
            }
            for msg in messages[1:]
        ]
    }

def map_status(edesk_status):
    mapping = {
        "Open": "Open",
        "Pending": "Pending",
        "Closed": "Resolved",
        "Unread": "Open",
        "Read": "Open",
        "Archived": "Resolved",
        "Spam": "Resolved",
    }
    return mapping.get(edesk_status, "Open")
```

**Attachment handling** is where many migrations fail quietly. eDesk attachment URLs require eDesk authentication and will expire. You must:

1. Download the file from eDesk into a local buffer
2. Check file type — SurveySparrow only accepts `pdf`, `png`, `jpeg`, `mp3`, `csv`, and `wav`
3. Check file size — 15 MB maximum
4. Upload accepted files to SurveySparrow's attachment endpoint
5. For unsupported types or oversized files, upload to external object storage (S3, GCS) and inject a signed URL into the comment body: *[Attachment externalized — original file type/size not supported by SurveySparrow. Access at: {url}]*

### Step 5: Load into SurveySparrow

Follow this load order to avoid foreign key failures and automation side effects:

1. **Contacts first** — batch create (Step 2a) must complete before any ticket load
2. **Ticket shells** — batch create for simple tickets without opener attachments; single-ticket create when the opener includes attachments (batch create does not expose the attachment field surface)
3. **Comments last** — via `POST /v3/tickets/:id/comments`, one comment per request, in chronological order
4. **Final status and assignment after thread replay** — set assignments and statuses after comments are loaded, or disable automation first

```python
def load_ticket(payload, ledger):
    """
    ledger: dict mapping edesk_ticket_id -> surveysparrow_ticket_id
    Skips tickets already in ledger (idempotent retry).
    """
    edesk_id = payload["custom_fields"].get(FIELD_IDS["edesk_ticket_id"])
    if edesk_id in ledger:
        return ledger[edesk_id]  # Already migrated

    resp = requests.post(
        f"{SS_BASE}/tickets",
        headers=SS_HEADERS,
        json={k: v for k, v in payload.items() if k != "comments"}
    )
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return load_ticket(payload, ledger)

    ss_ticket_id = resp.json().get("data", {}).get("id")
    ledger[edesk_id] = ss_ticket_id
    save_ledger(ledger)  # Persist after every write

    # Load comments chronologically
    for comment in payload.get("comments", []):
        comment_resp = requests.post(
            f"{SS_BASE}/tickets/{ss_ticket_id}/comments",
            headers=SS_HEADERS,
            json=comment
        )
        if comment_resp.status_code == 429:
            retry_after = int(comment_resp.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            requests.post(
                f"{SS_BASE}/tickets/{ss_ticket_id}/comments",
                headers=SS_HEADERS,
                json=comment
            )
        time.sleep(0.5)

    return ss_ticket_id

def save_ledger(ledger):
    with open("migration_ledger.json", "w") as f:
        json.dump(ledger, f)
```

> [!CAUTION]
> **Suppress webhooks and automation.** If SurveySparrow has active automation rules or webhooks — "send survey when ticket is created," auto-assignment rules, email notifications — importing thousands of historical tickets will trigger mass email floods and automation runs. **Disable all outbound notifications, survey triggers, and webhooks before starting.** Re-enable only after validation is complete. ([support.surveysparrow.com](https://support.surveysparrow.com/hc/en/articles/12633989912605-Workflows-in-Ticket-Management/?utm_source=openai))

> [!TIP]
> **Keep your own migration ledger.** SurveySparrow's list endpoints do not support lookup by custom field value. Store `edesk_ticket_id → surveysparrow_ticket_id` in a local `migration_ledger.json` file (or SQLite/Redis for larger volumes). Persist the ledger to disk after every successful write. If the process crashes at ticket 2,847, resume from that point — not from the beginning. Without a ledger, retries create duplicate tickets with no way to distinguish originals from duplicates. ([developers.surveysparrow.com](https://developers.surveysparrow.com/rest-apis/get-v-3-tickets/?utm_source=openai))

### Step 5a: Partial Failure Recovery

If the migration crashes mid-run — network failure, rate limit exhaustion, API error — do not restart from scratch. The migration ledger in `migration_ledger.json` is your recovery checkpoint:

1. Load the ledger: `edesk_id → ss_ticket_id` for all successfully created tickets
2. Resume ticket creation from the first ticket not in the ledger
3. For tickets in the ledger but with incomplete comment threads, query `GET /v3/tickets/:id/comments` to count existing comments, then append only the missing ones (match by position in the original chronological order)

SurveySparrow does not currently expose a bulk-delete ticket endpoint. If a test load goes wrong, tickets must be deleted individually via `DELETE /v3/tickets/:id` — scripted, but slow. For large bad loads, contact SurveySparrow support directly and request a workspace reset, which is faster than per-record deletion.

### Step 6: Delta Sync

A complete migration of large ticket volumes takes days at standard rate limits. During this time, your team continues working in eDesk. Handle this in two phases:

1. **Historical load:** Migrate all closed tickets and tickets created before the migration start date.
2. **Delta sync:** After the historical load completes, re-query eDesk with `filter_last_updated_at_gte` set to your last extraction timestamp — with an overlapping 48–72 hour window to account for timezone drift. For each ticket returned, check the migration ledger:
   - If `edesk_ticket_id` is not in the ledger → create the ticket as new
   - If it exists in the ledger → call `GET /v3/tickets/:ss_id/comments` to count existing comments, then append only the delta messages by position

The `edesk_ticket_id` custom field combined with the local ledger is your idempotency mechanism. Without it, retries create duplicates with no reconciliation path.

### Step 7: Validate

After loading, run validation checks before go-live:

- **Count match:** Total ticket count in eDesk (scoped to migration window) vs. total in SurveySparrow
- **Comment count match:** Per-ticket message count in eDesk vs. comment count in SurveySparrow for a sampled subset
- **Field integrity:** Spot-check 50–100 tickets for correct status, assignee, contact, and custom field values — including numeric field IDs resolving to the correct labels
- **Attachment verification:** Confirm attachments are accessible (not returning 403/404), and externalized files have working signed URLs
- **Automation state:** Confirm no spurious survey sends or notification emails were triggered during load

Use both platforms' export tooling for reconciliation. Hand-audit a statistically meaningful sample stratified across channels, ticket types, and status buckets — not just the most recent tickets.

## Timeline Estimation

SurveySparrow's API rate limit is the binding constraint. The table below uses conservative estimates assuming comment writes dominate the operation count. Adjust the "Total API Writes" column based on your actual average comment count per ticket.

| Volume | Avg Messages/Ticket | Total API Writes (tickets + comments) | Time at 120/hr | Time at 500/hr (increased limit) |
|---|---|---|---|---|
| 1,000 tickets | 3 | ~4,000 | ~33 hours | ~8 hours |
| 5,000 tickets | 5 | ~30,000 | ~250 hours | ~60 hours |
| 10,000 tickets | 5 | ~60,000 | ~500 hours | ~120 hours |
| 25,000 tickets | 5 | ~150,000 | ~1,250 hours | ~300 hours |

These estimates assume no errors, no retries, and no attachment uploads. Real-world migrations add 20–40% overhead for retries, validation, and partial failure recovery. Attachment upload rounds are additive and depend on your attachment volume.

For any migration above 5,000 tickets, request a rate limit increase from SurveySparrow before starting. The difference between default and increased limits is the difference between a multi-week project and a few-day execution.

## Edge Cases That Cause Silent Failures

### Amazon PII Purge

Amazon enforces strict PII retention policies. eDesk automatically redacts or deletes buyer names and addresses from Amazon tickets after 30 days. When you extract historical tickets, many Amazon tickets will have null or redacted contact fields. Your migration script must detect `null` contact values and assign these tickets to a fallback contact (e.g., `amazon-redacted@yourdomain.com`) to satisfy SurveySparrow's requirement that every ticket has a valid contact.

### Multi-Channel Contact Deduplication

A single customer in eDesk might have 3 contact records: one from Amazon (masked email), one from eBay, and one from Shopify. eDesk treats these as separate contacts. SurveySparrow expects one contact per person. Define merge rules before migration: which email wins? What happens to tickets linked to the Amazon contact with no real email? These rules must be encoded in your deduplication logic before any contacts are created.

### HTML Message Bodies

eDesk messages can contain HTML with inline images, marketplace-specific formatting, and embedded order widgets. SurveySparrow ticket comments may not render arbitrary HTML consistently. Use `bleach.clean()` with an explicit allowlist of safe tags (see Step 4 above) before inserting any message body into SurveySparrow. Test sanitization against a sample of 20–30 of your most complex historical tickets — Amazon and eBay message formatting in particular tends to produce malformed output.

### Unsupported Attachment Types

SurveySparrow only accepts `pdf`, `png`, `jpeg`, `mp3`, `csv`, and `wav`. eDesk histories commonly include DOCX, ZIP, and large buyer-uploaded images of damaged goods. For blocked types, upload to external object storage and inject a reference URL into the comment. For oversized files (> 15 MB), append: *[Attachment omitted — original file exceeded 15 MB limit. Stored at: {url}]*. Log every externalized attachment to a separate file for post-migration audit.

### Custom Field ID Mismatch

SurveySparrow custom fields are referenced by numeric ID in API payloads. If you hardcode IDs based on a test workspace and then run against a production workspace, every custom field write will fail silently — the API does not return an error for writes to non-existent field IDs; it simply drops the data. Always resolve field IDs dynamically from `GET /v3/ticket_fields` at the start of each migration run, never from a static constant.

### eCommerce Data Without a Home

eDesk tickets can reference sales orders with tracking links, order notes, and marketplace-specific identifiers. SurveySparrow has no order object. If you flatten into custom fields, you preserve the data but lose relational context. If you append it to ticket descriptions, it becomes unsearchable noise. Neither option is ideal — pick the least bad one for your operations and commit to it before writing a single line of migration code.

### Ticket Types Without Equivalents

eDesk's 29 ticket types (`BuyerNotes`, `Cancellation`, `DefectiveItemReceived`, `NegativeFeedback`, `OrderClaim`, etc.) encode business meaning. SurveySparrow has no built-in ticket type taxonomy. If your reporting depends on type-based filtering, pre-create a Dropdown custom field with all 29 values. Otherwise, this data is lost.

## When SurveySparrow Is the Wrong Target

If your support operation lives inside marketplace-native eCommerce workflows — if agents rely on channel context, seller order IDs, and order-linked SLAs for every interaction — SurveySparrow may be a downgrade in operating fit even if the migration succeeds technically. eDesk is built around multi-channel commerce support. SurveySparrow Ticket Management is strongest when tickets exist to act on feedback, surveys, NPS, and lighter support flows.

If that's your end state, the migration is workable. If you still need a deep eCommerce mailbox, challenge the destination before you move a single record. ([edesk.com](https://www.edesk.com/about-us/?utm_source=openai))

---

## What ClonePartner Handles

We've run helpdesk migrations across every major platform, including eDesk. For this specific migration path, we typically handle:

- Full eDesk API extraction with rate limit orchestration and resume-on-failure
- Contact deduplication across marketplace channels
- Custom field pre-creation and numeric ID resolution in SurveySparrow
- eCommerce data preservation strategy (sales orders, tracking, ticket types)
- Timestamp metadata injection for historical auditability
- HTML sanitization for marketplace-formatted message bodies
- Attachment triage — upload supported formats, externalize the rest with signed URLs
- Delta sync to capture tickets created during the migration window
- Post-migration validation with count matching, field integrity checks, and attachment verification
- Zero-downtime execution so your support team never stops working in eDesk during the process

For a deeper look at managing live cutover windows, see our [Zero-Downtime Help Desk Data Migration](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/) guide.

> Planning an eDesk to SurveySparrow migration? Our engineers will map your specific migration path — data model, rate limits, timeline, and edge cases. Book a free 30-minute call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I migrate tickets from eDesk to SurveySparrow using CSV export?

Not for a production migration. eDesk's CSV exports from Insights contain ticket metadata only — no conversation threads, attachments, or internal notes. The Search Download is Enterprise-only and capped at 1,000 rows. You need a custom API-to-API pipeline for a complete migration.

### Can SurveySparrow preserve original eDesk comment timestamps?

No. SurveySparrow's current create-ticket and create-comment endpoints do not accept historical created_at or explicit comment-author fields. Imported records receive new platform timestamps. Preserve source time and author in custom fields or comment body headers.

### How long does an eDesk to SurveySparrow migration take?

SurveySparrow's default API rate limit (~120 calls/hour) is the bottleneck. A 5,000-ticket migration with 5 messages per ticket takes roughly 250 hours at default limits. With an increased limit (500/hr), the same migration takes about 60 hours. Request a rate limit increase before starting.

### What happens to eDesk sales order data during migration?

SurveySparrow has no native sales order object. You can preserve order IDs, tracking info, and channel names by creating custom Dropdown and Text fields. The relational link between tickets and orders is lost — only flat metadata survives.

### Do I need eDesk Enterprise for an API-based migration?

For direct API access, yes — eDesk documents the API as an Enterprise-plan feature. Without it, you can fall back to admin JSON exports, but that approach is capped and unsuitable for delta syncs.
