---
title: "eDesk to Pylon Migration: A Technical Guide"
slug: edesk-to-pylon-migration-a-technical-guide
date: 2026-07-24
author: Rishabh
categories: [Migration Guide, Pylon]
excerpt: "A technical guide to migrating from eDesk to Pylon. Covers API rate limits, data model mapping, ETL process, edge cases, and validation."
tldr: "eDesk to Pylon migration requires a custom API pipeline. Pylon's 10 req/min Issues API is your primary bottleneck, and eDesk's order data has no native Pylon equivalent."
canonical: https://clonepartner.com/blog/edesk-to-pylon-migration-a-technical-guide/
---

# eDesk to Pylon Migration: A Technical Guide


# eDesk to Pylon Migration: A Technical Guide

Migrating from eDesk to Pylon is not a button-press import. You are moving from an eCommerce-first helpdesk built around marketplace integrations, order context, and seller workflows to an AI-native, Slack-first B2B support platform built around account-level relationships and conversational channels. The data models serve fundamentally different audiences, there is no native migration path between them, and API rate limits on both sides make brute-force approaches impractical at scale.

If you are searching for **how to migrate from eDesk to Pylon**, the short answer is: a custom API-based ETL pipeline or a managed migration service. eDesk's CSV export is limited to 1,000 rows per download and excludes conversation history. ([support.edesk.com](https://support.edesk.com/search-filter-download)) Middleware tools like Zapier cannot handle nested ticket-message-attachment relationships. And Pylon's Issues API is rate-limited to just 10 requests per minute, which constrains throughput regardless of how fast you extract. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/api-reference/issues?utm_source=openai))

*Last updated: July 2025. API rate limits, endpoint structures, and platform capabilities verified against published documentation as of this date.*

**Changelog:**
- *July 2025:* Initial publication with full endpoint verification against eDesk v1 API and Pylon REST API documentation.

## Related Guides

- **[Pylon to eDesk Migration: The CTO's Technical Guide](https://clonepartner.com/blog/blog/pylon-to-edesk-migration-the-ctos-technical-guide/):** For teams evaluating the reverse direction.
- **[Zendesk to Pylon Migration: The CTO's Technical Guide](https://clonepartner.com/blog/blog/zendesk-to-pylon-migration-the-ctos-technical-guide/):** For teams migrating to Pylon from Zendesk.
- **[Freshdesk to Pylon Migration: The CTO's Technical Guide (2025)](https://clonepartner.com/blog/blog/freshdesk-to-pylon-migration-the-ctos-technical-guide-2025/):** For teams migrating to Pylon from Freshdesk.
- **[Help Desk Data Migration Playbook](https://clonepartner.com/blog/blog/help-desk-data-migration-playbook/):** Deciding what data to move and what to leave behind.

## Why Teams Move from eDesk to Pylon

The shift is architectural, not cosmetic.

**eDesk** is a purpose-built eCommerce helpdesk that unifies customer queries and order data from Amazon, eBay, Shopify, and dozens of marketplaces into a centralized smart inbox. Its core data model revolves around the **Order** — tickets are linked to sales orders, tracking information, and channel-specific workflows. The relationship chain is flat: Customer → Ticket → Messages → Sales Orders. ([edesk.com](https://www.edesk.com/helpdesk/?utm_source=openai))

**Pylon** is purpose-built for B2B support where conversations happen in shared Slack channels, Microsoft Teams, and Discord. Its core object is the **Issue** — a conversational thread tied to an **Account**, with messages, internal notes, custom fields, and CRM-synced metadata. The relationship chain is relational: Account → Contact → Issue → Messages. ([usepylon.com](https://www.usepylon.com/help-desk?utm_source=openai))

Teams migrate for three reasons:

- **Business model shift.** Companies that started in eCommerce and evolved into B2B SaaS or service businesses need account-centric support, not order-centric support. eDesk's data model does not map to recurring customer relationships.
- **Channel alignment.** B2B customers live in Slack Connect and Teams. Pylon integrates natively with these channels. eDesk is built for email, marketplace messaging, and social — not real-time collaboration.
- **Cost and stack fit.** eDesk's API access requires the Enterprise plan. Teams that no longer need marketplace integrations are paying for features they will not use. ([edesk.com](https://www.edesk.com/en/pricing/?utm_source=openai))

## Core Data Model Differences: eDesk vs Pylon

This table defines the structural mismatch you will reconcile during migration:

| Concept | eDesk | Pylon | Migration Notes |
|---|---|---|---|
| Core record | Ticket (tied to channel/order) | Issue (tied to Account) | 1:1 mapping possible, but context differs |
| Customer | Customer record | Contact (linked to Account) | eDesk customers flatten to Pylon contacts |
| Company | No formal company object | Account | Must be derived from customer email domains or tags |
| Conversations | Messages within a ticket | Messages within an Issue | Thread structure differs |
| Order data | Sales Orders, tracking links, order notes | No equivalent | Must be archived or stored in custom fields |
| Channels | Amazon, eBay, Shopify, email, chat, social | Slack, Teams, Discord, email, chat widget, WhatsApp | Marketplace channels have no Pylon equivalent |
| Tags | Tags and tag groups | Tags | Direct mapping |
| Custom fields | Custom fields on tickets | Custom fields on Issues, Contacts, Accounts | Type matching required |
| Automations | eDesk rules and templates | Triggers, Macros | Must be manually rebuilt |
| Knowledge base | eDesk knowledge base | Pylon Knowledge Base | Separate migration required |

> [!WARNING]
> **eDesk Sales Orders, tracking links, and marketplace-specific metadata have no native equivalent in Pylon.** If this data is business-critical, archive it separately or flatten it into Pylon custom fields before migration. There is no way to reconstruct marketplace order context inside Pylon. ([developers.edesk.com](https://developers.edesk.com/reference/listsalesorders?utm_source=openai))

## Migration Approaches

There are five viable methods. Each has different trade-offs for fidelity, throughput, and engineering effort.

### 1. Native CSV Export → Manual Import

**How it works:** Export tickets from eDesk's Insights section as CSV files, transform them, and use Pylon's API to import.

**When to use it:** Proof-of-concept migrations under 1,000 records, or when you only need metadata without conversation history.

**Pros:**
- No engineering required for extraction
- Quick for small datasets

**Cons:**
- eDesk's Search Download is Enterprise-only and capped at 1,000 rows. Ticket Download defaults to the first 500 tickets with no filters and produces JSON, not CSV. Report Extracts are snapshots and documentation indicates data goes back to July 2023. ([support.edesk.com](https://support.edesk.com/search-filter-download))
- Exports contain ticket metadata only — no conversation threads, no attachments, no internal notes
- Pylon has no bulk CSV import — you still need API calls to load data
- Relationship data (customer → ticket → messages) is flattened

**Scalability:** Unsuitable beyond a small proof-of-concept.

### 2. API-Based Migration (eDesk REST API → Pylon REST API)

**How it works:** Build a custom pipeline that reads from eDesk's REST API and writes to Pylon's REST API. Extract tickets, messages, and customers; transform into Pylon's Account → Contact → Issue → Message hierarchy; load via Pylon endpoints.

Pylon documents a dedicated `/import/issues` endpoint for historical data that accepts threaded messages, timestamps, and internal notes in a single payload. This is the recommended path for backfilling closed tickets. Each message in the payload requires `body_html`, `is_private` (boolean), and exactly one of `user_id` (for agent messages) or `contact_id` (for customer messages). The endpoint returns the created Issue object with its Pylon `id`. ([support.usepylon.com](https://support.usepylon.com/articles/1886544233-how-do-i-import-conversation-data-with-threaded-messages-using-the-api?lang=en))

**When to use it:** Any production migration that requires conversation history, attachments, and relationship integrity.

**Pros:**
- Full-fidelity extraction including messages, notes, and attachments
- Relationship preservation (customer → ticket → messages)
- Programmatic control over transformation logic
- Handles custom fields, tags, and metadata

**Cons:**
- eDesk API requires the Enterprise plan
- Pylon's Issues API is rate-limited to 10 requests per minute for create operations
- Building the pipeline takes 2–4 weeks of engineering time
- Error handling, retry logic, and idempotency must be built from scratch
- Imported issues do not trigger Pylon's AI topic generation ([support.usepylon.com](https://support.usepylon.com/articles/1886544233-how-do-i-import-conversation-data-with-threaded-messages-using-the-api?lang=en))

**Scalability:** The only approach that scales to production workloads, but Pylon's 10 req/min ceiling means 50,000 tickets take approximately 3.5 days of uninterrupted API writes (50,000 ÷ 10 per min ÷ 60 min ÷ 24 hr ≈ 3.47 days).

### 3. Third-Party Migration Tools

Platforms like Help Desk Migration offer automated eDesk connectors. eDesk lists Help Desk Migration as a partner supporting 60+ platforms, and Pylon's migration materials reference Relokia. ([edesk.com](https://www.edesk.com/partners/help-desk-migration/?utm_source=openai))

**When to use it:** Mid-size migrations where you want to avoid building a custom pipeline but need more than CSV.

**Pros:**
- Pre-built connectors reduce development time
- Built-in field mapping UI
- Support for test migrations before committing

**Cons:**
- Pylon is a newer platform — third-party tool support may be limited or incomplete. Run a proof-of-concept before committing.
- Do not assume a direct eDesk → Pylon template handles sales-order data or custom cutover logic without validation
- Limited control over edge cases and complex relationships
- Per-record pricing can be significant at scale. Help Desk Migration's published pricing (as of 2025) uses tiered per-record rates — check their calculator for current costs with your dataset size.

**Scalability:** Good for mid-size datasets (5K–50K tickets). May hit limitations on complex data models.

### 4. Custom ETL Pipeline

**How it works:** Build a dedicated data pipeline using Python, Node.js, or a data engineering framework. Extract from eDesk into an intermediate data store (PostgreSQL, MongoDB, or flat files), transform to match Pylon's schema, then load via Pylon's API.

**When to use it:** Enterprise migrations with complex transformation requirements, multiple source systems, or regulatory constraints.

**Pros:**
- Full control over every transformation
- Intermediate staging allows auditing and validation before loading
- Supports incremental and delta syncs
- Audit trail and replayability

**Cons:**
- Highest engineering investment (3–6 weeks)
- Requires monitoring, logging, and alerting infrastructure
- Maintenance burden if the migration is one-time

**Scalability:** Best option for enterprise datasets (100K+ tickets).

### 5. Middleware / Integration Platforms (Zapier, Make)

**How it works:** Connect eDesk and Pylon through a middleware platform using triggers and actions.

**When to use it:** Ongoing sync of new tickets only. Not suitable for historical migration.

**Pros:**
- No-code setup
- Good for forwarding new tickets post-migration

**Cons:**
- Cannot migrate historical data in bulk
- Cannot handle nested relationships (ticket → messages → attachments)
- Limited error handling and retry logic
- Running historical tickets through Zapier will trigger rate limits and cost thousands in task usage

**Scalability:** Only suitable for low-volume, forward-looking sync.

### Migration Approach Comparison

| Method | Fidelity | Scalability | Engineering Effort | Best For |
|---|---|---|---|---|
| CSV Export | Low | <1K records | Low | Proof-of-concept |
| API-Based | High | 5K–100K+ | High | Production migrations |
| Third-Party Tools | Medium–High | 5K–50K | Medium | Mid-size, standard schemas |
| Custom ETL | Highest | 100K+ | Very High | Enterprise, multi-source |
| Middleware (Zapier/Make) | Low | Ongoing sync only | Low | Post-migration forwarding |

**Recommendations by scenario:**

- **Small business (<5K tickets, no dev team):** Third-party migration tool with a test run first. Validate Pylon adapter coverage before committing.
- **Mid-market (5K–50K tickets, some engineering bandwidth):** API-based migration with a staged approach. Extract to a local database first, transform, then load.
- **Enterprise (50K+ tickets, dedicated dev team):** Custom ETL pipeline with intermediate staging, throttled loading, and full audit logging.
- **Ongoing sync (post-migration):** Middleware platform for new ticket forwarding only.

## When to Use a Managed Migration Service

### When NOT to Build In-House

- Your engineering team is allocated to product work and cannot absorb 3–6 weeks of migration development
- You have less than 2 weeks until your eDesk contract ends
- Your data includes complex relationships, custom fields, and attachments that require per-record validation
- You have never built against Pylon's API and need to learn its rate limit behavior, pagination model, and error handling from scratch

Pylon's own documentation is clear that standard data migration covers historical closed tickets and knowledge base content, while process recreation, open-ticket cutover, views, SLAs, triggers, and training are separate workstreams. ([support.usepylon.com](https://support.usepylon.com/articles/9886835391-data-migration))

### Hidden Costs of DIY Migration

These are real costs teams underestimate — not reasons to avoid building internally, but factors to budget for:

- **Engineering time:** A senior engineer at $150/hr for 4 weeks = $24,000+ in opportunity cost, not counting debugging and re-runs. This is based on US market rates; adjust for your team's location and seniority.
- **Data loss risk:** Partial failures without resumability mean re-running the entire migration
- **Downtime exposure:** Without a delta sync strategy, tickets created between extraction and go-live are lost
- **Validation overhead:** Building record-count comparison, field-level validation, and sampling logic is its own project — typically 20–30% of total migration engineering time

### About ClonePartner

ClonePartner has completed 1,500+ migrations across helpdesk platforms, including migrations into Pylon from Zendesk, Freshdesk, and other sources. Specific capabilities relevant to this migration path:

- **Relationship reconstruction:** Deriving Pylon Accounts from eDesk customer data where no formal company object exists
- **Rate limit orchestration:** Managing Pylon's 10 req/min Issues ceiling without timeout failures
- **Attachment handling:** Downloading from eDesk, uploading to Pylon's attachments API, and linking to the correct Issues and Messages
- **Custom field mapping:** Transforming eDesk's field types to Pylon's supported types
- **Zero-downtime execution:** Delta sync strategies that capture tickets created during the migration window

*Disclosure: ClonePartner is the publisher of this guide. If you have the engineering bandwidth and timeline, the API-based approach documented below works without external help.*

## Pre-Migration Planning

### Data Audit Checklist

Before writing any migration code, audit your eDesk instance:

| Object | Audit Action |
|---|---|
| Tickets | Count total, by status, by channel. Identify oldest records. |
| Messages/Replies | Count messages per ticket. Flag tickets with 50+ messages (may hit Pylon payload size limits). |
| Customers | Deduplicate by email. Identify customers with multiple channel identities. |
| Tags | Export full tag list. Map to Pylon tags. |
| Custom Fields | Document field names, types, and picklist values. |
| Attachments | Estimate total file size. Identify file types. Note any files >25 MB. |
| Sales Orders | Decide: archive separately or flatten into Pylon custom fields? |
| Templates | Document all templates. These must be manually recreated as Pylon Macros. |
| Knowledge Base | Export all articles with their HTML/markdown content. Plan a separate KB migration. |
| Automations/Rules | Document all eDesk rules including trigger conditions and actions. Must be rebuilt as Pylon Triggers. |

eDesk's documented API objects include tickets, messages, contacts, sales orders, order notes, tags, users, channels, templates, ticket fields, and order fields. The API uses offset-based pagination with `page` and `per_page` parameters (max `per_page` varies by endpoint, typically 100). If you have CRM data (accounts, leads, opportunities) in an upstream system, migrate those from the system of record — eDesk's API surface is support and order oriented, not a CRM pipeline. ([developers.edesk.com](https://developers.edesk.com/reference/pagination))

### Identify Unused Data

Most eDesk accounts accumulate dead weight:

- Closed tickets older than 24 months with no business value
- Duplicate customer records created by marketplace channel splits
- Test tickets and internal test data
- Orphaned attachments from deleted tickets

Every record you exclude reduces migration time and Pylon API calls. At 10 Issues per minute, cutting 10,000 unnecessary tickets saves approximately 16.7 hours of load time (10,000 ÷ 10 per min ÷ 60 min).

### Migration Strategy

| Strategy | Best For | Risk Level |
|---|---|---|
| **Big bang** | Small datasets (<5K), tight timelines | High — no rollback window |
| **Phased** | Mid-size, multiple channels | Medium — can validate per phase |
| **Incremental** | Enterprise, zero-downtime requirement | Low — continuous validation |

For most eDesk-to-Pylon migrations, a **phased approach** works best: migrate historical closed tickets first, run a delta sync, then cut over active operations. Pylon's own migration guidance positions historical backfill as milestone 3, not the step that makes go-live possible — process recreation comes first. ([support.usepylon.com](https://support.usepylon.com/articles/5198864213-overview-migrating-to-pylon))

**Delta sync for the cutover window:** eDesk supports webhooks for ticket events (create, update, close). Configure an eDesk webhook to capture tickets created or updated during the migration window. Buffer these events and replay them against Pylon after the primary migration completes. This eliminates the gap between extraction and go-live. If webhooks are not available on your eDesk plan, poll the eDesk tickets endpoint filtered by `updated_at > extraction_start_timestamp` as a fallback.

For the full decision framework, see our [Help Desk Data Migration Checklist](https://clonepartner.com/blog/blog/help-desk-data-migration-checklist/).

## Data Model & Object Mapping

This is the section that determines whether your migration succeeds or fails.

### Object-Level Mapping

| eDesk Object | Pylon Object | Mapping Notes |
|---|---|---|
| Customer | Contact | Map by email address. Deduplicate first. |
| Customer (company domain) | Account | eDesk has no company object — derive Accounts from customer email domains or tags. |
| Ticket | Issue | 1:1 mapping. Map status, priority, tags, custom fields. |
| Message (in ticket) | Message (in Issue) | Map as reply or internal note based on message type. |
| Note / Order note | Private message or Account Activity | Use `is_private=true` for internal notes. |
| Tag | Tag | Direct mapping. Create tags in Pylon first. |
| Custom Field | Custom Field | Type-match required. |
| Sales Order | No equivalent | Archive or flatten to Issue custom fields. |
| Template | Macro | Manual recreation required. |
| Knowledge Base Article | KB Article | Separate migration via KB API. |

### Status Mapping

eDesk and Pylon use different status taxonomies. eDesk tickets use `Open`, `Pending`, `Closed`, `Archived`, and `Spam`. ([developers.edesk.com](https://developers.edesk.com/reference/listtickets)) Pylon Issues use `new`, `waiting_on_you`, `waiting_on_customer`, `on_hold`, and `closed`.

There is no vendor-provided one-to-one mapping. You must make design decisions:

| eDesk Status | Recommended Pylon State | Notes |
|---|---|---|
| Open | `waiting_on_you` | Active, needs agent action |
| Pending | `waiting_on_customer` | Awaiting customer response |
| Closed | `closed` | Direct map |
| Archived | `closed` | Preserve original status in a custom field |
| Spam | Exclude from migration | Or store in custom field for audit |

> [!NOTE]
> **Preserve the original eDesk status in a custom field** if you need audit accuracy. The Pylon state set is smaller and semantically different.

### Field-Level Mapping

| eDesk Ticket Field | Pylon Issue Field | Transformation |
|---|---|---|
| `ticket_id` | Custom field: `edesk_ticket_id` | Cast to string. Critical for deduplication and delta syncs. |
| `subject` | `title` | Direct map. Default to `"(no subject)"` if empty. |
| `body` | `body_html` | Convert to HTML if plain text. Wrap in `<p>` tags. |
| `status` | `state` | Map per status table above. |
| `priority` | `priority` | Map values. |
| `channel` | Custom field: `source_channel` | Store as metadata. Marketplace channels have no Pylon equivalent. |
| `assigned_agent` | `assignee_id` | Lookup Pylon user ID by agent email. |
| `tags` | `tags` | Array of tag slugs. |
| `created_at` | `created_at` | ISO 8601. Ensure UTC normalization. |
| `customer_email` | `requester_email` | Direct map. |
| `customer_name` | `requester_name` | Direct map. |
| Custom fields | `custom_fields` | Slug-based mapping with type conversion. |
| `sales_order.order_id` | Custom field: `order_id` | Flatten. No native order object in Pylon. |
| `messages [].body` | Message `body_html` | Convert to HTML. |
| `messages [].attachments` | `attachment_urls` | Re-upload to Pylon. |

> [!NOTE]
> **Pylon custom fields support these types:** text, number, decimal, boolean, date, datetime, user, url, select, and multiselect. If eDesk has a custom field type that does not map cleanly (e.g., rich text), convert to `text` and accept the formatting loss. Match option sets for select fields before loading data — Pylon will reject values not in the defined option list. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/api-reference/custom-fields?utm_source=openai))

### Handling the Order Discrepancy

eDesk relies heavily on Order data. Pylon does not have an Order object. You have three options:

1. **Custom fields on Issues:** Create fields for `order_id`, `marketplace`, `SKU`, and `tracking_number` on the Pylon Issue. Best for fast agent lookup.
2. **Internal notes:** Append the eDesk order details as an internal note on the first message of the Pylon Issue. Lower ongoing maintenance, but harder to search.
3. **External system of record:** Keep order data in your eCommerce or ERP system and link by customer email or order ID. Best for data integrity if the order data is complex or frequently updated.

For most teams, option 1 gives agents the fastest lookup. Option 3 is better if order records contain nested line items, shipment updates, or refund histories that exceed what custom fields can represent.

### Handling Relationships and Dependencies

The load order matters. Pylon Issues reference Accounts and Contacts by ID, so you must create parent records first:

1. **Accounts** — Derive from customer email domains. Create via `POST /accounts`. Use `external_id` (e.g., the domain string) for idempotency.
2. **Contacts** — Create and link to Accounts via `account_id`. Create via `POST /contacts`. Use `external_id` (e.g., eDesk customer ID) for idempotency.
3. **Tags** — Create all tags before loading Issues. Pylon tags are referenced by slug in Issue payloads.
4. **Custom Fields** — Create field definitions before loading Issues. Note the field `slug` returned — you will reference it in Issue payloads.
5. **Issues** — Create with `account_id`, `requester_id`, `tags`, `custom_fields`. Use `/import/issues` for historical data.
6. **Messages** — If using `/import/issues`, messages are included in the Issue payload. If creating Issues via `POST /issues`, add messages separately.
7. **Attachments** — Upload via `POST /attachments`, then reference the returned URL in Messages.

> [!WARNING]
> **Every Issue must be associated with an Account or Contact.** If an eDesk ticket lacks a valid customer email, the Pylon API will reject the payload. Build fallback logic to assign orphaned tickets to a default "Unknown Customer" Account and Contact record.

### Handling Multi-Language Content

eDesk serves international eCommerce sellers. If your tickets contain non-English content (German, French, Japanese, etc.):

- Ensure all text fields are UTF-8 encoded before sending to Pylon's API
- HTML entities in eDesk message bodies (e.g., `&auml;` for ä) should be preserved or converted to their Unicode equivalents
- Right-to-left (RTL) languages may require additional HTML direction attributes (`dir="rtl"`) in `body_html` for correct rendering in Pylon's UI

## Migration Architecture

### Data Flow

```
eDesk REST API          →    Staging DB/Files    →    Transform Layer    →    Pylon REST API
(60 req/min extraction)       (local PostgreSQL       (field mapping,          (10 req/min for Issues,
                               or JSON files)          deduplication,            60 req/min for Accounts,
                                                       relationship             Contacts, Tags)
                                                       linking)
```

### eDesk API Details

- **Base URL:** `https://api.edesk.com/v1/` (v2 also available for some endpoints)
- **Authentication:** Bearer token generated in Settings → API Tokens
- **Rate limit:** 60 requests per minute per client ([developers.edesk.com](https://developers.edesk.com/reference/authentication?utm_source=openai))
- **Pagination:** Offset-based with `page` and `per_page` parameters. Max `per_page` is typically 100. Response includes `total` and `last_page` fields for iteration control.
- **Token expiry:** 90 days by default (configurable)
- **Plan requirement:** Enterprise plan required for API access
- **Key endpoints:** `GET /tickets`, `GET /tickets/{id}/messages`, `GET /customers`, `GET /sales-orders`, `GET /tags`, `GET /ticket-fields`
- **Webhooks:** Available for ticket create/update/close events. Useful for delta sync during cutover.

### Pylon API Details

- **Base URL:** `https://api.usepylon.com/`
- **Authentication:** Bearer token (Admin users only can create tokens)
- **Rate limits (per endpoint):**
  - `GET /issues`, `POST /issues`, `/import/issues`: 10 requests per minute
  - `PATCH /issues/{id}`: 20 requests per minute
  - `GET /accounts`, `POST /accounts`: 60 requests per minute
  - `GET /contacts`, `POST /contacts`: 60 requests per minute
  - `POST /attachments`: 60 requests per minute
  - `GET /knowledge-bases`: 60 requests per minute
- **Pagination:** Cursor-based. Response includes a `cursor` field; pass it as a query parameter in subsequent requests.
- **Error format:** Standard HTTP status codes. `429` for rate limit exceeded (include `Retry-After` header in seconds). `422` for validation errors (response body includes field-level error details). `401` for invalid or expired tokens.
- **Idempotency:** Use `external_id` on Accounts and Contacts to enable safe re-runs without duplicates ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/external-ids?utm_source=openai)). Issues created via `/import/issues` do not currently support `external_id` — use the `edesk_ticket_id` custom field for deduplication logic in your pipeline.
- **Sandbox environments:** Contact Pylon support to provision a test environment. Alternatively, create a separate Pylon workspace for test migrations — this is the most common approach for validating before production.

> [!CAUTION]
> **Pylon's 10 requests/minute ceiling on Issues is the primary throughput bottleneck.** This rate limit is per-workspace, not per-token — creating multiple API tokens does not increase throughput. Every issue creation and every issue list query consumes from this budget. At this rate, loading 10,000 issues takes approximately 16.7 hours. Loading 50,000 issues takes roughly 3.5 days of continuous writes. Plan your migration timeline around this constraint.

### Handling Large Datasets

- **Extraction (eDesk):** Batch extraction by date range using `created_at_from` and `created_at_to` query parameters. Paginate through results and store raw JSON payloads in a local PostgreSQL table or JSONL files rather than holding them in memory.
- **Loading (Pylon):** The rate limit is per-workspace, not per-token. Parallelization of Pylon writes is not possible within a single workspace.
- **Batching strategy:** Extract all eDesk data first into a staging layer. Transform offline. Load into Pylon as a separate, throttled operation. This decouples extraction speed from loading constraints.

## Step-by-Step Migration Process

### Step 1: Extract Data from eDesk

Use the eDesk API to extract all required objects. Start with customers, then tickets with their messages. Order of operations matters — you need customer IDs to resolve foreign keys during transformation.

```python
import requests
import time
import json

EDESK_BASE = "https://api.edesk.com/v1"
EDESK_TOKEN = "your_edesk_token"

def edesk_get(endpoint, params=None):
    """Make a GET request to eDesk API with automatic retry on rate limit."""
    headers = {"Authorization": f"Bearer {EDESK_TOKEN}"}
    resp = requests.get(f"{EDESK_BASE}/{endpoint}", headers=headers, params=params)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 2))
        time.sleep(retry_after)
        return edesk_get(endpoint, params)
    resp.raise_for_status()
    return resp.json()

def extract_all_tickets():
    """Extract all tickets with offset-based pagination."""
    tickets = []
    page = 1
    while True:
        data = edesk_get("tickets", {"page": page, "per_page": 100})
        if not data.get("data"):
            break
        tickets.extend(data["data"])
        if page >= data.get("last_page", page):
            break
        page += 1
    return tickets

def extract_messages(ticket_id):
    """Extract all messages for a specific ticket."""
    return edesk_get(f"tickets/{ticket_id}/messages")

def extract_all_customers():
    """Extract all customers with pagination."""
    customers = []
    page = 1
    while True:
        data = edesk_get("customers", {"page": page, "per_page": 100})
        if not data.get("data"):
            break
        customers.extend(data["data"])
        if page >= data.get("last_page", page):
            break
        page += 1
    return customers
```

### Step 2: Transform Data

Map eDesk's flat customer model into Pylon's Account → Contact hierarchy. Derive Accounts from customer email domains, filtering out freemail providers.

```python
FREEMAIL_DOMAINS = {
    "gmail.com", "yahoo.com", "hotmail.com", "outlook.com", "aol.com",
    "icloud.com", "mail.com", "protonmail.com", "zoho.com", "yandex.com"
}

MARKETPLACE_DOMAINS = {
    "marketplace.amazon.com", "members.ebay.com"
}

def derive_account(customer):
    """Derive a Pylon Account from an eDesk customer's email domain.
    Returns None for freemail and marketplace-anonymized addresses."""
    email = customer.get("email", "")
    if not email or "@" not in email:
        return None
    domain = email.split("@")[-1].lower()
    if domain in FREEMAIL_DOMAINS or domain in MARKETPLACE_DOMAINS:
        return None  # No company account for freemail or anonymized users
    return {
        "name": domain,
        "domains": [domain],
        "external_id": f"edesk-domain-{domain}"
    }

STATUS_MAP = {
    "open": "waiting_on_you",
    "pending": "waiting_on_customer",
    "closed": "closed",
    "archived": "closed",
}

def map_ticket_to_issue(ticket, account_id, contact_id, agent_id_map):
    """Transform an eDesk ticket into a Pylon /import/issues payload.

    The /import/issues endpoint accepts the full issue with messages
    in a single request. Each message requires body_html, is_private,
    and exactly one of user_id or contact_id.
    """
    messages = []
    for msg in ticket.get("messages", []):
        message_payload = {
            "body_html": msg.get("body", ""),
            "is_private": msg.get("is_internal", False),
        }
        if msg.get("is_agent"):
            pylon_user_id = agent_id_map.get(msg.get("agent_id"))
            if pylon_user_id:
                message_payload["user_id"] = pylon_user_id
            else:
                message_payload["user_id"] = agent_id_map.get("default")
        else:
            message_payload["contact_id"] = contact_id
        messages.append(message_payload)

    return {
        "title": ticket.get("subject") or "(no subject)",
        "body_html": ticket.get("body", "") or "<p>(no body)</p>",
        "account_id": account_id,
        "requester_id": contact_id,
        "state": STATUS_MAP.get(ticket.get("status", "").lower(), "waiting_on_you"),
        "tags": [t["name"] for t in ticket.get("tags", [])],
        "created_at": ticket.get("created_at"),  # ISO 8601 UTC
        "custom_fields": [
            {"slug": "edesk-ticket-id", "value": str(ticket["id"])},
            {"slug": "edesk-channel", "value": ticket.get("channel", "")},
            {"slug": "edesk-original-status", "value": ticket.get("status", "")},
            {"slug": "order-id", "value": ticket.get("order_id", "")},
        ],
        "messages": messages,
    }
```

Clean HTML formatting where necessary — Pylon renders markdown in its Slack integration. Convert timestamps to ISO 8601 with UTC normalization. Strip Base64-encoded inline images from `body_html` (see edge cases section).

### Step 3: Load into Pylon

Respect rate limits. Create parent records first, then Issues.

For historical ticket backfill, use Pylon's `/import/issues` endpoint. This accepts the full issue payload including threaded messages in a single request. Accounts, contacts, and users must exist before import. ([support.usepylon.com](https://support.usepylon.com/articles/1886544233-how-do-i-import-conversation-data-with-threaded-messages-using-the-api?lang=en))

```python
import time
import json
import sqlite3

PYLON_BASE = "https://api.usepylon.com"
PYLON_TOKEN = "your_pylon_token"

# Checkpoint database for resumability
checkpoint_db = sqlite3.connect("migration_checkpoint.db")
checkpoint_db.execute("""
    CREATE TABLE IF NOT EXISTS migrated_issues (
        edesk_ticket_id TEXT PRIMARY KEY,
        pylon_issue_id TEXT,
        created_at TEXT
    )
""")

def pylon_post(endpoint, payload):
    """POST to Pylon API with automatic retry on 429."""
    headers = {
        "Authorization": f"Bearer {PYLON_TOKEN}",
        "Content-Type": "application/json"
    }
    resp = requests.post(f"{PYLON_BASE}/{endpoint}", headers=headers, json=payload)
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        print(f"Rate limited. Waiting {retry_after}s. Request ID: {resp.headers.get('x-request-id', 'N/A')}")
        time.sleep(retry_after)
        return pylon_post(endpoint, payload)
    if resp.status_code == 422:
        error_body = resp.json()
        print(f"Validation error: {json.dumps(error_body, indent=2)}")
        raise ValueError(f"Pylon validation error for {endpoint}: {error_body}")
    resp.raise_for_status()
    return resp.json()

def is_already_migrated(edesk_ticket_id):
    """Check checkpoint DB to enable resumable migrations."""
    row = checkpoint_db.execute(
        "SELECT pylon_issue_id FROM migrated_issues WHERE edesk_ticket_id = ?",
        (str(edesk_ticket_id),)
    ).fetchone()
    return row is not None

def load_issues(issues):
    """Load issues into Pylon via /import/issues, respecting 10 req/min limit.
    Checkpoints each successful create for resumability."""
    for i, (edesk_id, payload) in enumerate(issues):
        if is_already_migrated(edesk_id):
            print(f"Skipping already-migrated ticket {edesk_id}")
            continue
        result = pylon_post("import/issues", payload)
        pylon_id = result["data"]["id"]
        checkpoint_db.execute(
            "INSERT INTO migrated_issues VALUES (?, ?, datetime('now'))",
            (str(edesk_id), pylon_id)
        )
        checkpoint_db.commit()
        print(f"Created issue {pylon_id} from eDesk #{edesk_id} ({i+1}/{len(issues)})")
        time.sleep(6)  # 10 req/min = 1 request per 6 seconds
```

### Step 4: Migrate Attachments

Attachments require a separate process. Download the binary file from eDesk's CDN URL, upload to Pylon via `POST /attachments`, then reference the returned URL when creating the Message. This triples the API call count per attachment (download from eDesk, upload to Pylon, associate with message).

```python
def migrate_attachment(edesk_attachment_url, pylon_issue_id):
    """Download from eDesk CDN and re-upload to Pylon."""
    # Step 1: Download binary from eDesk
    file_resp = requests.get(edesk_attachment_url)
    file_resp.raise_for_status()

    # Step 2: Upload to Pylon
    headers = {"Authorization": f"Bearer {PYLON_TOKEN}"}
    files = {"file": ("attachment", file_resp.content)}
    upload_resp = requests.post(
        f"{PYLON_BASE}/attachments",
        headers=headers,
        files=files
    )
    if upload_resp.status_code == 429:
        time.sleep(int(upload_resp.headers.get("Retry-After", 60)))
        return migrate_attachment(edesk_attachment_url, pylon_issue_id)
    upload_resp.raise_for_status()
    return upload_resp.json()["data"]["url"]
```

**Important:** If eDesk CDN URLs are not stable long-term, add an intermediate object-store step (S3, GCS) during extraction so you are not depending on eDesk infrastructure after contract cancellation. Download all attachments to your own storage during the extraction phase, then upload to Pylon from there. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/api-reference/attachments?utm_source=openai))

### Step 5: Migrate Knowledge Base

eDesk's knowledge base articles must be migrated separately from ticket data. The process:

1. **Extract from eDesk:** Use the eDesk API to pull KB articles including title, body (HTML), category, and published status.
2. **Transform:** Convert eDesk's article HTML to match Pylon's expected format. Update any internal links that reference eDesk URLs.
3. **Load into Pylon:** Use Pylon's Knowledge Base API (`GET /knowledge-bases` to find your KB ID, then create articles via the appropriate endpoint). Rate limit: 60 req/min.

If your KB contains fewer than 50 articles, manual recreation through Pylon's UI may be faster than building an API pipeline.

### Step 6: Validate

After loading, run validation checks (covered in the Validation section below).

## Edge Cases & Challenges

### Duplicate Customer Records

eDesk frequently creates separate customer records for the same person across different marketplace channels. A customer who emails from a personal address and buys via Amazon may have two or three eDesk profiles. Before loading into Pylon, deduplicate by email address and merge into a single Contact. Use `external_id` on Pylon contacts to prevent re-creating duplicates on subsequent runs. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/external-ids?utm_source=openai))

**Deduplication strategy:** Group eDesk customer records by normalized email (lowercase, strip whitespace). For records sharing the same email, merge names (prefer the most recent), combine tag sets, and consolidate ticket references. For marketplace-anonymized emails (`abc123@marketplace.amazon.com`), treat each as a separate contact — there is no way to link them to real identities.

### Missing or Inconsistent Data

- eDesk tickets from marketplace channels may lack a subject line. Pylon Issues require a `title`. Default to `"(no subject)"` or derive from the first 100 characters of the first message body.
- Customer emails from marketplace messaging (e.g., Amazon buyer messages) use anonymized addresses like `abc123@marketplace.amazon.com`. These cannot be matched to real contacts and will not produce useful Account derivations. Assign them to a catch-all "Marketplace Customers" Account.
- Some eDesk tickets may have `null` or empty `created_at` timestamps. Default to the earliest message timestamp in the thread, or the current time as a last resort.

### Multi-Channel Threading

eDesk flattens emails, Amazon messages, and live chats into a single ticket feed. Pylon expects clearer threading. If a customer switched channels mid-conversation in eDesk, insert an internal note in Pylon to denote where a channel switch occurred in the historical thread:

```python
# Example: Insert a channel-switch note
if previous_msg_channel != current_msg_channel:
    messages.insert(i, {
        "body_html": f"<p><em>[Channel switched from {previous_msg_channel} to {current_msg_channel}]</em></p>",
        "is_private": True,
        "user_id": system_user_id
    })
```

### Inline Images and Base64 Encoding

eDesk sometimes embeds images as Base64 strings in the HTML body (e.g., `<img src="data:image/png;base64,iVBOR..."/>`). Pylon's API may reject payloads that exceed size limits due to large Base64 strings. During transformation:

1. Scan `body_html` for Base64 image patterns using regex: `data:image/[^;]+;base64, [A-Za-z0-9+/=]+`
2. Extract each image, decode to binary, and upload as a standard attachment via `POST /attachments`
3. Replace the Base64 `src` attribute with the returned Pylon attachment URL

### API Failures and Retries

- **eDesk:** Returns `429` when rate-limited. The `Retry-After` header specifies wait time in seconds. Implement exponential backoff starting at 2 seconds, capped at 60 seconds.
- **Pylon:** Returns `429` when rate limit is exceeded. Use the `Retry-After` header if provided, otherwise wait 60 seconds. Log the Pylon `x-request-id` from response headers for debugging with Pylon support. Returns `422` for validation errors — the response body contains an `errors` object with field-level details.
- **Idempotency:** Store eDesk ticket IDs as Pylon custom fields (`edesk-ticket-id`) and query before creating to prevent duplicates on re-runs. Use a resilient queue (BullMQ in Node.js, Celery in Python) with exponential backoff for production migrations — do not use simple `for` loops without checkpointing.

**Sample Pylon error responses:**
```json
// 429 Rate Limit
{"error": "Rate limit exceeded", "retry_after": 60}

// 422 Validation Error
{"errors": {"title": ["can't be blank"], "account_id": ["must reference a valid account"]}}
```

### Status Mapping Edge Cases

eDesk has statuses like `Archived` and `Spam` that have no direct Pylon equivalent. Map `Archived` to `closed` and either exclude `Spam` tickets entirely or store the original eDesk status in a custom field for audit purposes. ([developers.edesk.com](https://developers.edesk.com/reference/listtickets))

If your eDesk instance uses custom statuses beyond the defaults, document each one and map it to the closest Pylon state. Store the original in the `edesk-original-status` custom field.

## Limitations & Constraints

### Pylon Limitations vs eDesk

| Limitation | Impact |
|---|---|
| No Sales Order object | eCommerce order context is lost unless flattened to custom fields |
| No marketplace channel support | Amazon, eBay, Shopify channel data has no target |
| Issues API: 10 req/min | Migration throughput is severely constrained (16.7 hrs per 10K issues) |
| No bulk import endpoint for Issues | Each `/import/issues` call creates one issue with its messages |
| Custom Objects require Enterprise plan | Cannot store arbitrary eDesk data structures without Enterprise |
| No `external_id` on Issues | Deduplication must use custom fields, not native idempotency |
| No bulk delete API for Issues | Rollback of failed migrations requires manual cleanup or Pylon support |

### Data That Will Not Transfer

- **Marketplace SLAs:** eDesk's Amazon/eBay SLA timers do not translate to Pylon. Pylon uses standard B2B SLA policies based on priority and account tier.
- **Agent performance metrics:** eDesk's CSAT data and response-time metrics tied to agents have no direct import path into Pylon's analytics.
- **Live open tickets:** Pylon's standard migration path is for historical closed tickets. Open-ticket continuity requires a separate cutover workflow where agents manually re-engage in Pylon. ([support.usepylon.com](https://support.usepylon.com/articles/9886835391-data-migration))
- **Channel-specific metadata:** Seller SKU, tracking number, and marketplace-specific fields must be stored as custom fields or discarded.
- **eDesk Smart Inbox rules and AI classifications:** These are eDesk-proprietary and must be rebuilt using Pylon's trigger and automation system.

### Potential Data Loss Scenarios

| Scenario | Mitigation |
|---|---|
| Tickets created between extraction start and go-live | Delta sync via eDesk webhooks or polled re-extraction |
| Inline images referencing eDesk's CDN (URLs expire after contract cancellation) | Download all attachment URLs during extraction phase |
| Marketplace-anonymized emails that cannot resolve to real contacts | Assign to catch-all Account; flag in custom field |
| eDesk custom field values that exceed Pylon's text field length limits | Truncate with ellipsis; store full value in an internal note |
| Agent accounts that do not exist in Pylon | Map to a default "Migrated Agent" user; note original agent in custom field |

## Validation & Testing

Do not assume a `200 OK` response means the data is correct.

### Record Count Comparison

| Object | eDesk Count | Pylon Count | Status |
|---|---|---|---|
| Customers → Contacts | — | — | ✓ / ✗ |
| Derived Accounts | — | — | ✓ / ✗ |
| Tickets → Issues | — | — | ✓ / ✗ |
| Messages | — | — | ✓ / ✗ |
| Attachments | — | — | ✓ / ✗ |
| Tags | — | — | ✓ / ✗ |

Count discrepancies are expected and acceptable in specific cases: duplicate customers merged into fewer contacts, spam tickets excluded, and orphaned records dropped. Document every expected discrepancy.

### Field-Level Validation

For a random sample of 50–100 records, compare:

- Ticket subject → Issue title
- Ticket status → Issue state (verify mapping logic)
- Message count per ticket → Message count per Issue
- Attachment count per message
- Customer email → Contact email
- Tag assignments
- Custom field values (especially `edesk-ticket-id` for traceability)

### Sampling Strategy

Use a structured sample instead of random selection alone:

- 10 tickets with 1 message each (baseline)
- 10 tickets with 10+ messages (threading validation)
- 10 tickets with attachments (binary transfer validation)
- 10 tickets with custom fields (type mapping validation)
- 10 tickets from each channel (email, marketplace, chat)
- 5 tickets with `Archived` or `Spam` status (edge case validation)

### UAT Process

1. Run a test migration with 5–10% of data into a separate Pylon workspace (contact Pylon support to provision, or create a test workspace)
2. Have 2–3 support agents verify 20 tickets each against the eDesk originals, using a structured checklist (subject, status, message count, attachments, custom fields, tags)
3. Test Pylon workflows (triggers, SLAs, assignments) with migrated data to confirm automation rules fire correctly
4. Validate that links between Accounts → Contacts → Issues resolve correctly in the Pylon UI
5. Verify that Slack/Teams channel integrations display migrated Issues properly

### Rollback Planning

Pylon does not offer a bulk delete API for Issues. If validation fails:

- **For small datasets (<500 Issues):** Delete individually via the Pylon UI or `DELETE /issues/{id}` API calls (if available — verify with Pylon support)
- **For large datasets:** Contact Pylon support for bulk cleanup assistance, or provision a fresh Pylon workspace and re-run the migration
- **Best practice:** Always run test migrations into a separate workspace before touching production. The cost of a second workspace is negligible compared to recovering from a bad production load.

For a full post-migration QA workflow, see our [Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/).

## Post-Migration Tasks

### Rebuild Automations

eDesk's rules and templates do not transfer. You must manually recreate:

- **eDesk templates** → Pylon Macros (recreate text, variables, and any conditional logic)
- **eDesk auto-assignment rules** → Pylon Triggers (define matching conditions and assignment actions)
- **eDesk SLA rules** → Pylon SLA policies (configure response and resolution time targets by priority/account tier)
- **eDesk auto-responders** → Pylon AI or Trigger-based responses

Document each eDesk automation before decommissioning. Create a mapping spreadsheet: eDesk rule name, trigger condition, action, and corresponding Pylon trigger configuration.

### Delta Sync

Run a final sync to capture tickets created or updated in eDesk during the migration window. Two approaches:

1. **Webhook-based:** If you configured eDesk webhooks before migration, replay buffered events through your transform-and-load pipeline.
2. **Poll-based:** Query eDesk API for tickets with `updated_at` greater than your extraction start timestamp. Transform and load only net-new records (check against your checkpoint database to avoid duplicates).

Keep eDesk in read-only mode (remove agent access) until signoff.

### DNS and Routing Cutover

- Update support email forwarding rules (MX records or forwarding rules) to point to Pylon instead of eDesk
- If you use shared Slack channels, connect them to Pylon before go-live
- Update any CNAME records for your help center domain if migrating the knowledge base
- Disable eDesk email ingestion to prevent duplicate ticket creation during the transition

### CRM Sync Caveat

If you enable CRM attribute syncing (e.g., Salesforce, HubSpot) after migration, test on a subset of 10–20 Accounts first. Pylon documents that CRM-to-Pylon sync can overwrite or clear existing Pylon values when the CRM field is blank. This can wipe out custom field data you just migrated. ([support.usepylon.com](https://support.usepylon.com/articles/4220063775-platform-orientation-pylon-101))

### User Training

The UX paradigm shift is significant. Agents accustomed to eDesk's order-centric inbox will need training on:

- Pylon's Account-centric navigation (finding Issues by company, not by order number)
- Slack/Teams-based conversation flows (responding in-channel vs. in-app)
- Issue states vs. eDesk ticket statuses (fewer states, different semantics)
- Custom field usage for order data lookups (where order context now lives)
- Using the `edesk-ticket-id` custom field to cross-reference historical tickets

Plan for 1–2 hours of hands-on training per agent, plus a reference guide covering the status mapping and custom field conventions.

### Monitor for Inconsistencies

For the first 2 weeks post-migration:

- Spot-check 10 random Issues daily against eDesk originals
- Monitor for missing attachments (CDN URL expiry from eDesk)
- Verify that Pylon triggers fire correctly on migrated data
- Watch for duplicate Contacts created by new incoming messages from customers who already exist
- Check that CRM sync has not overwritten migrated custom field values
- Monitor Pylon's error logs for any delayed-processing failures

## Best Practices

1. **Never migrate directly from production to production.** Extract to a secure intermediary database first. This protects against network timeouts and allows re-running transformations without re-extracting.
2. **Run test migrations.** Push 5–10% of data into a separate Pylon workspace to validate mapping logic before touching production.
3. **Validate incrementally.** Check after each phase (Accounts, Contacts, Issues, Messages) — do not wait until the end.
4. **Store eDesk IDs.** Save the original `edesk_ticket_id` in a Pylon custom field. If a customer replies to an old eDesk email thread months later, your routing rules can use that ID to find the correct Pylon Issue.
5. **Use `external_id` on Accounts and Contacts.** Set deterministic external IDs so re-runs are idempotent and do not create duplicates. ([docs.usepylon.com](https://docs.usepylon.com/pylon-docs/developer/api/external-ids?utm_source=openai))
6. **Plan for the gap.** Tickets created in eDesk between extraction and go-live need a delta sync strategy using webhooks or polled re-extraction.
7. **Archive what Pylon cannot hold.** Export Sales Orders, tracking data, and marketplace metadata to a separate data store (S3, BigQuery, or your ERP) before cancelling eDesk.
8. **Automate repetitive transforms.** Field mapping, deduplication, and status conversion should be scripted — not manual.
9. **Checkpoint every write.** Write each created Pylon ID to a local mapping store (SQLite or JSON) as you go. If the migration fails at record 5,000 of 10,000, you must resume from 5,001 — not restart from zero.
10. **Download all attachments during extraction.** Do not depend on eDesk CDN URLs remaining accessible after your contract ends. Store them in your own object storage before loading into Pylon.

## Automation Script Outline

For engineering teams building this internally, here is a high-level Python structure for an end-to-end API-based migration:

```python
# edesk_to_pylon_migration.py
# Production code requires error handling, logging, checkpointing,
# and idempotency checks beyond what is shown here.

import logging
import sqlite3
from dataclasses import dataclass
from datetime import datetime

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("migration")

@dataclass
class MigrationConfig:
    edesk_token: str
    edesk_base_url: str = "https://api.edesk.com/v1"
    pylon_token: str = ""
    pylon_base_url: str = "https://api.usepylon.com"
    pylon_issues_rate_limit: float = 6.0  # seconds between requests (10 req/min)
    pylon_general_rate_limit: float = 1.0  # seconds between requests (60 req/min)
    checkpoint_db_path: str = "migration_checkpoint.db"

def main(config: MigrationConfig):
    checkpoint_db = sqlite3.connect(config.checkpoint_db_path)
    init_checkpoint_tables(checkpoint_db)

    # Phase 1: Extract from eDesk
    logger.info("Phase 1: Extracting from eDesk...")
    customers = extract_edesk_customers(config)
    tickets = extract_edesk_tickets_with_messages(config)
    logger.info(f"Extracted {len(customers)} customers, {len(tickets)} tickets")

    # Phase 2: Transform
    logger.info("Phase 2: Transforming data...")
    accounts = derive_accounts_from_customers(customers)
    contacts = transform_customers_to_contacts(customers)
    issues = transform_tickets_to_issues(tickets)
    logger.info(f"Derived {len(accounts)} accounts, {len(contacts)} contacts, {len(issues)} issues")

    # Phase 3: Load into Pylon (order matters)
    logger.info("Phase 3: Loading into Pylon...")
    account_id_map = load_accounts(config, accounts, checkpoint_db)     # 60 req/min
    contact_id_map = load_contacts(config, contacts, checkpoint_db)     # 60 req/min
    create_tags(config, extract_unique_tags(tickets))                   # 60 req/min
    create_custom_fields(config)                                        # One-time setup
    issue_id_map = load_issues(config, issues, checkpoint_db)           # 10 req/min ← bottleneck
    load_attachments(config, tickets, issue_id_map, checkpoint_db)      # 60 req/min

    # Phase 4: Validate
    logger.info("Phase 4: Validating...")
    validate_record_counts(config, len(tickets), len(customers))
    validate_sample_records(config, tickets[:50], issue_id_map)

    logger.info(f"Migration complete at {datetime.utcnow().isoformat()}")

if __name__ == "__main__":
    config = MigrationConfig(
        edesk_token="your_token",
        pylon_token="your_token"
    )
    main(config)
```

> [!TIP]
> **Checkpoint everything.** Write each created Pylon ID to a local mapping file (SQLite is recommended over JSON for concurrent access safety) as you go. If the migration fails at record 5,000 of 10,000, you need to resume from 5,001 — not restart from zero. The code samples above demonstrate this pattern with the `migration_checkpoint.db` SQLite database.

## Making the Decision

This migration is technically straightforward in concept but operationally demanding in execution. The data model gap — eDesk's eCommerce focus vs. Pylon's B2B focus — means you will make structural decisions about what data to keep, what to archive, and what to discard. The rate limit on Pylon's Issues API (10 req/min) is the single biggest factor in your migration timeline.

**Timeline estimates by dataset size:**

| Dataset Size | Estimated Load Time (Issues only) | Total Migration (including planning, testing, validation) |
|---|---|---|
| 1,000 tickets | ~1.7 hours | 1 week |
| 5,000 tickets | ~8.3 hours | 1–2 weeks |
| 10,000 tickets | ~16.7 hours | 2–3 weeks |
| 50,000 tickets | ~3.5 days | 4–6 weeks |
| 100,000 tickets | ~6.9 days | 6–8 weeks |

For teams with fewer than 5,000 tickets and available engineering bandwidth, a self-built API migration is achievable in 1–2 weeks. For anything larger, the math on engineering time, rate limit management, and validation overhead usually favors a managed service or third-party tool.

For most teams, the right sequence is: rebuild support processes in Pylon first, cut over open work with a planned live handoff, and backfill closed history second. If you are planning a live cutover window, [Zero-Downtime Help Desk Data Migration](https://clonepartner.com/blog/blog/zero-downtime-help-desk-data-migration/) is the right companion read.

> Planning an eDesk to Pylon migration? Our engineers have handled helpdesk migrations across every major platform. Book a free 30-minute call and we'll map out your specific migration path — data model, rate limits, timeline, and all.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## Frequently asked questions

### Can I migrate from eDesk to Pylon using CSV export?

Not for production use. eDesk's Search Download is Enterprise-only and capped at 1,000 rows. It excludes conversation history, attachments, and internal notes. You need the eDesk API (Enterprise plan required) to extract full ticket data, then load via Pylon's REST API or historical import endpoint.

### What are the Pylon API rate limits for migration?

Pylon's Issues endpoints (GET and POST) are limited to 10 requests per minute. Account, Contact, and most other endpoints allow 60 requests per minute. Issue updates allow 20 per minute. This means loading 10,000 issues takes approximately 16.7 hours of continuous API writes.

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

It depends on volume. Loading into Pylon is the bottleneck at 10 Issues/min. A 5,000-ticket migration takes roughly 8+ hours of load time. A 50,000-ticket migration takes 3–4 days. Add 1–2 weeks for pipeline development and validation if building in-house.

### What eDesk data is lost when migrating to Pylon?

Pylon has no native equivalent for eDesk Sales Orders, order tracking links, marketplace channel metadata, or order notes. This data must be archived separately or flattened into Pylon custom fields. Marketplace-anonymized email addresses also cannot be resolved to real contacts.

### Can open eDesk tickets stay live after moving to Pylon?

Not through standard historical backfill. Pylon's documented migration path covers closed tickets and knowledge base content. Live open-ticket continuity requires a separate cutover or handoff workflow.
