Skip to content

Freshdesk to Pylon Migration: The CTO's Technical Guide (2026)

A CTO-level guide to migrating from Freshdesk to Pylon — covering API rate limits, object mapping, attachment handling, and zero-downtime cutover approaches.

Raaj Raaj · · 29 min read
Freshdesk to Pylon Migration: The CTO's Technical Guide (2026)
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Migrating from Freshdesk to Pylon is not a settings import. You are moving from a traditional, email-first ticketing system to an AI-native, Slack-first B2B support platform. The data models are fundamentally different, the API rate limits are asymmetric, and there is no native import path between them.

If you are evaluating what's the cleanest way to migrate from Freshdesk to Pylon preserving data, the answer is a custom API-based ETL pipeline or a managed migration service. Native CSV exports drop attachments and thread context, while standard middleware tools fail at scale.

Freshdesk → Pylon Migration Resource Index

To navigate this process, use the following resources and guides:

Why B2B Teams Move from Freshdesk to Pylon

The shift is architectural, not cosmetic. Freshdesk is a mature, highly configurable helpdesk built around email-centric ticket queues with a flat contact-and-company structure. Pylon is purpose-built for B2B support where the primary conversation happens in shared Slack channels and Microsoft Teams — not email portals. (developers.freshdesk.com)

Teams migrate for three reasons:

  • Channel alignment. B2B customers live in Slack Connect and Teams. Pylon integrates natively with these channels, plus Discord, WhatsApp, and in-app chat, so customers never leave their workspace. Freshdesk treats chat as a bolt-on to an email-first architecture.
  • Account-centric support model. Pylon ties every Issue to an Account, giving agents full customer context — health scores, CRM data, renewal risk — directly in the support view. Freshdesk can achieve this with custom apps and marketplace add-ons, but the experience requires significant configuration.
  • AI-native workflows. Pylon's AI handles triage, routing, knowledge gap detection, and draft replies out of the box. Freshdesk's Freddy AI capabilities are available as paid add-ons (Copilot, AI Agent sessions), with sessions that expire monthly.

If you are comparing Pylon to other migration targets, our Zendesk to Pylon Migration: The CTO's Technical Guide covers the Zendesk-specific data model differences.

Core Architecture Differences

Dimension Freshdesk Pylon
Primary object Ticket Issue
Contact model Contacts + Agents (separate APIs) Contacts + Users (unified)
Company model Companies Accounts (with subaccounts, partner accounts)
Conversation model Conversations (replies, notes) Messages
Custom fields On tickets, contacts, companies On issues, contacts, accounts
Statuses Open, Pending, Resolved, Closed (+ custom) new, waiting_on_you, waiting_on_customer, on_hold, closed (+ custom)
Priority Low, Medium, High, Urgent (integers 1–4) low, medium, high, urgent (strings)
Attachments content_url in ticket/conversation JSON (temporary S3 URLs) attachment_urls array on issues
Auth model Basic Auth (API key) Bearer token
Rate limits 200–700 req/min account-wide (by plan) 10–60 req/min (by endpoint)
Pagination Page-based (100/page, 300-page ceiling) Cursor-based

This asymmetry is the source of most migration failures. You can extract data from Freshdesk roughly 3–10x faster than you can write it to Pylon, so any pipeline needs a buffer or write-side throttle.

Migration Approaches: CSV vs. API vs. Managed Service

There are five ways to move your data. Choosing the wrong one leads to broken threads, orphaned attachments, or weeks of wasted engineering time.

1. Native CSV Export → Manual Import

How it works: Export tickets, contacts, and companies from Freshdesk's admin UI as CSV files. Restructure the data, then use Pylon's API to bulk-import. Freshdesk also offers a full Account Export (Admin → Account → Export) that generates an XML dump including archived tickets. (support.freshdesk.com)

When to use it: Fewer than 1,000 tickets, minimal custom fields, no attachments to preserve.

Limitations: Freshdesk's CSV export does not include full conversation histories, attachments, or archived tickets. You get a flat snapshot — ticket metadata without thread context. Pylon has no CSV import UI, so you still need the API to load data.

Complexity: Low to extract. Medium to load.

2. API-Based Custom ETL Pipeline

How it works: Write a script (Python, Node.js) that extracts data via Freshdesk's v2 REST API, transforms it to match Pylon's schema, and loads it via Pylon's REST API. For enterprise programs or regulated environments, land the data into a staging database first for better observability, dead-letter handling, audit logs, and rerun safety. (developers.freshdesk.com)

When to use it: You have engineering bandwidth, need full control over transformation logic, and want repeatable delta syncs.

Pros:

  • Full control over field mapping and transformation
  • Can handle custom fields, attachments, and conversation threading
  • Repeatable for delta syncs
  • Staging-database approach gives the strongest audit trail and rollback posture

Cons:

  • Freshdesk's List All Tickets API is capped at 30,000 tickets (300 pages × 100 records). Larger accounts require date-range segmentation or the Account Export feature.
  • Pylon's issue creation endpoint is rate-limited to 10 requests per minute. A 10,000-ticket migration with multiple API calls per ticket takes significant write time.
  • Attachments require separate HTTP GET requests to Freshdesk's temporary S3 URLs, then re-upload before referencing in Pylon.

Complexity: High.

3. Third-Party Automated Migration Tools

How it works: Tools like Help Desk Data Migration or Import2 offer pre-built connectors for Freshdesk. Import2's workflow runs sample-to-full: connect source and destination, run a sample migration, inspect results, edit mappings, then execute. Their docs note that system configuration (users, reports, templates, automations, workflows) is not migrated, and validation errors on a parent record can block linked records. (help.import2.com)

When to use it: Only if a connector for Pylon exists and your data structure is straightforward. As of mid-2026, dedicated Freshdesk-to-Pylon connectors are extremely limited.

Limitations: Standard migration tools often fail on knowledge base articles, complex custom fields, multi-level account hierarchies, inline images, conversation threading, and CSAT data. They typically do not offer delta syncs, forcing a hard weekend cutover.

For a comparison of automated tools vs. managed services, see ClonePartner vs Help Desk Data Migration: 2026 Price & Feature Comparison.

Complexity: Medium (when connectors exist).

4. Middleware Platforms (Zapier, Make)

How it works: Use prebuilt connectors to create or update Pylon objects from events. Zapier exposes Pylon actions such as Create Account and Create Account Activity. Make currently lists Pylon as a community connector that talks to official API endpoints. (zapier.com)

When to use it: Ongoing sync after the bulk backfill is complete. Not for replaying years of ticket history.

Limitations: Connector throttling, task costs, and community-adapter stability make these unviable for historical bulk loads. Migrating 100,000+ tickets through Zapier is financially and technically unworkable.

Complexity: Low for simple syncs. Unviable for bulk migration.

5. Managed Migration Service

How it works: A specialized team handles the full ETL pipeline — extraction, transformation, loading, validation, and delta sync — on your behalf.

When to use it: More than 10,000 tickets, complex custom fields, attachments that must be preserved, or zero tolerance for downtime.

Pros:

  • No engineering distraction from product work
  • Handles rate-limit orchestration, attachment re-hosting, and relationship integrity
  • Supports parallel operation and delta sync for zero-downtime cutover

Cons:

  • Cost — you are paying for specialized engineering time
  • Requires sharing API credentials and granting account access
  • Less direct control over transformation logic (mitigated by review cycles)

Complexity: Low (for your team). Scalability: High.

Approach Comparison

Criteria CSV Export Custom ETL Auto Tool Middleware Managed Service
Engineering effort Low High Low-Medium Low None
Conversation history Partial
Attachments ✅ (complex) Partial
Custom fields Partial Partial Partial
Scale (50k+ tickets) Risky Depends
Delta sync DIY Partial
Zero downtime DIY

Scenario recommendations:

  • Small business, <1,000 tickets, low engineering bandwidth: Use a self-service tool if official Freshdesk-to-Pylon support exists. Otherwise, go straight to a managed service.
  • Enterprise, attachments, deep history, or account hierarchy: API-based ETL or managed service. CSV and iPaaS are false economies here.
  • Bulk migration + ongoing sync: Run the bulk move via API or managed service, then keep deltas alive with a small worker or iPaaS flow.
  • Zero-downtime requirement: Stage historical imports, validate, then cut channels over after delta sync reaches zero.

Pre-Migration Planning & Data Audit

Before touching any API, audit your Freshdesk instance. Freshdesk archives closed inactive tickets after 120 days, and the standard UI ticket export excludes archived history and full conversations. Decide up front what is in scope. (support.freshdesk.com)

Data inventory checklist:

  • Total ticket count (check if >30,000 — you will hit the pagination ceiling)
  • Total contacts and companies
  • Custom ticket fields (names, types, picklist values)
  • Custom contact/company fields
  • Ticket tags and categories
  • Conversation count per ticket (affects API call volume)
  • Attachment volume (count and total size in GB)
  • Knowledge base articles and folder structure
  • Automations, SLAs, and business hours (must be rebuilt manually)
  • Canned responses / saved replies

Archiving Old Tickets Before Migration

Not every ticket needs to land in Pylon. Before starting extraction, decide which historical tickets to migrate and which to archive externally.

What to consider:

  • Active and recent tickets (last 6–12 months): Migrate to Pylon as Issues. These are relevant for agent context, account health, and ongoing conversations.
  • Old closed/resolved tickets (>12 months): Evaluate whether agents actually reference them. If not, export to a data warehouse (BigQuery, Snowflake, S3 as JSON/Parquet) rather than polluting Pylon's active queues.
  • Archived tickets (120+ days inactive): Freshdesk archives these automatically. They are excluded from standard CSV exports and the List All Tickets API. Retrieve them via the Account Export (Admin → Account → Export) or the Search API.
  • Compliance-driven tickets: If you must retain them for regulatory or contractual reasons but do not need them in active support, store them in your data warehouse with a retention policy. If they must be accessible in Pylon, load them with a tag like migrated_archived so agents can filter them out of daily views.

Practical approach:

  1. Export all tickets to your staging database.
  2. Tag each record with an age bucket: active, recent, historical, archived.
  3. Migrate active and recent to Pylon.
  4. Store historical and archived in your data warehouse.
  5. If compliance requires searchability, load historical tickets into Pylon with a migrated_archived tag and exclude that tag from default Issue views.

This keeps Pylon clean for agents while preserving the full audit trail externally.

What to leave behind:

  • Spam and deleted tickets
  • Duplicate contacts (merge before migration)
  • Test tickets from implementation
  • Time-tracking entries (Pylon has no equivalent)
  • Historical SLA metrics (not transferable as native SLA objects)
  • Leads and Opportunities from Freshsales or your CRM — these are a separate migration; sync only the context Pylon needs through its CRM layer

Migration strategy:

  • Big bang: Migrate everything in one pass over a weekend. Works for fewer than 10,000 tickets.
  • Phased by channel: Migrate email tickets first, then move Slack/Teams channels to Pylon. Lets you validate before going all-in.
  • Parallel operation with delta sync: Run both systems simultaneously. New tickets go to Pylon; historical data migrates in the background. A final delta pass catches anything created during migration. This is the zero-downtime approach and what we recommend for most B2B teams.
Warning

Disable all Freshdesk automations and notification rules before starting any data export. Bulk API reads can trigger webhooks, which consume your rate limit and can send unintended emails to customers.

Data Model & Object Mapping: Tickets to Issues

This is where migrations break. Freshdesk and Pylon use fundamentally different schemas, and raw field copy is the wrong strategy. Map the semantic next-action state, not just labels.

Object-Level Mapping

Freshdesk Object Pylon Object Notes
Company Account Pylon supports subaccounts and partner accounts. Preserve source company ID as external_id.
Contact Contact Link to Accounts via account_id. Match by email.
Agent User Pylon uses roles (Admin, Member, etc.). Manual rebuild.
Ticket Issue Core mapping — see field table below.
Conversation (reply/note) Message Each reply becomes a Message on the Issue. Preserve internal vs. public visibility strictly.
Ticket attachment Attachment URL Must be re-hosted; Freshdesk S3 URLs are temporary.
Knowledge Base article Knowledge Base article Separate API; requires collection structure. Migrate as a separate workstream.
Tags Tags Direct mapping; create tags in Pylon first. Normalize casing before load.
Custom fields Custom fields Must pre-create in Pylon with matching slugs.
Groups Teams Manual rebuild.
Canned responses Macros Manual rebuild.
SLAs SLAs Manual rebuild.
Automations Triggers Manual rebuild — see Automation Rebuild section below.
Parent/child companies Subaccounts Use when roll-up reporting matters.
Agencies / consultants Partner accounts Avoid duplicating cross-customer contacts.

Field-Level Mapping: Tickets → Issues

Freshdesk Field Pylon Field Transformation
id external_id Store for cross-reference and idempotent reruns
subject title Direct
description / description_text body_html Freshdesk provides HTML; Pylon accepts HTML. Parse for inline images.
status (2=Open, 3=Pending, 4=Resolved, 5=Closed) state (new, waiting_on_you, waiting_on_customer, on_hold, closed) Integer → string mapping (see below)
priority (1=Low, 2=Med, 3=High, 4=Urgent) priority (low, medium, high, urgent) Integer → string
requester_id contact_id Lookup by email from pre-migrated contacts
responder_id assignee_id Lookup by email from pre-created users
company_id account_id Lookup from pre-migrated accounts
tags tags Array → array
created_at created_at RFC3339 format
updated_at Used for delta sync polling (see Delta Sync section); not mapped to a Pylon field
custom_fields.cf_* custom_fields [{slug, value}] Map cf_ prefix fields to Pylon slugs
attachments [].attachment_url attachment_urls Download from Freshdesk S3, re-host, pass URLs
Info

Pylon's issue creation API accepts a created_at timestamp (RFC3339), which lets you preserve original ticket creation dates. If omitted, the current time is used — destroying your historical timeline.

Status Mapping

Freshdesk uses integer status codes. Pylon uses string states:

Freshdesk Status Code Pylon State
Open 2 new
Pending 3 waiting_on_customer
Resolved 4 closed
Closed 5 closed
Custom statuses 6+ Map to Pylon custom statuses (create before import)

Pylon's issue workflow supports five core status categories. Custom statuses are supported but must be created before import. Closed issues can reopen on new activity — keep that in mind when mapping Resolved vs. Closed.

Freshdesk allows unlimited custom statuses (codes 6+). Pylon supports custom statuses within its status categories. That is usually enough for support operations, but it is not a 1:1 substitute for every bespoke Freshdesk taxonomy.

Warning

Custom objects and schema constraints: Pylon has a streamlined schema. If you rely heavily on Freshdesk Custom Objects or integrations that simulate custom objects (e.g., asset tracking), those patterns don't transfer directly. Flatten this data into standard Issue attributes or Account-level fields. Push CRM data back to the CRM rather than forcing it into the support platform.

Warning

Multi-company contacts: Freshdesk can associate contacts with multiple companies. Pylon's contact creation centers on one account reference at a time. Decide early whether to collapse those relationships, duplicate contacts selectively, or model the intermediary as a partner account. (support.freshdesk.com)

Custom Field Type Mapping

Freshdesk contact and company fields expose types like custom_text, custom_paragraph, custom_checkbox, custom_number, custom_dropdown, custom_phone_number, custom_url, and custom_date. Pylon custom fields support Text, Number, Decimal, Select, Multi-select, Boolean, Date, User, URL, Datetime, and Formula. Build from the overlap, and normalize picklist values before load so Select fields do not fail on unmapped values. (developers.freshdesk.com)

Freshdesk API Constraints & Attachment Handling

This is where DIY scripts fail. If your custom script breaks here, see Help Desk Data Migration Failed? The Engineer's Rescue Guide.

Warning

Common Pitfalls Summary:

  • API Rate Limits: Freshdesk allows 200–700 req/min globally; Pylon restricts issue creation to 10 req/min. You must build a write-side throttle.
  • Attachments: Freshdesk provides temporary S3 URLs. You must download and re-host them before pushing to Pylon.
  • Data Loss: Relying on CSV exports will orphan your attachments and flatten your conversation threads.

Rate Limits by Plan

Freshdesk enforces account-wide rate limits plus endpoint-specific sublimits. The account-wide limit is shared with every other integration, app, and webhook running on that Freshdesk instance. Even failed requests (400, 404) count against the limit.

Freshdesk Plan Account-Wide Rate Limit Ticket List Sublimit
Trial 50/min
Growth 200/min ~20 list calls/min
Pro 400/min ~40 list calls/min
Enterprise 700/min ~70 list calls/min

Monitor usage via response headers: X-Ratelimit-Total, X-Ratelimit-Remaining, X-Ratelimit-Used-CurrentRequest. A 429 Too Many Requests response includes a Retry-After header — queue your calls and obey it. (developers.freshdesk.com)

The 30,000-Ticket Ceiling

Freshdesk's List All Tickets endpoint (/api/v2/tickets) is capped at 300 pages × 100 records = 30,000 tickets maximum. The endpoint also returns only the last 30 days by default. If your account has more tickets, you must:

  1. Segment by date range using the updated_since parameter to pull batches under the ceiling.
  2. Use Freshdesk's Account Export (Admin → Account → Export), which generates a full XML dump of all data including archived tickets.
  3. Use the Search API (/api/v2/search/tickets) for targeted extraction only — it is capped at 10 pages (300 results per query), uses URL-encoded query syntax with case-sensitive field names, and is unsuitable for bulk export.

Attachment Extraction

Freshdesk attachments are not embedded in the ticket JSON. The API response includes an attachments array where each file has a content_url pointing to a temporary S3 URL. To migrate attachments:

  1. Fetch the ticket with include=conversations to get conversation attachments. Note: this returns only up to 10 conversations per ticket and consumes extra API cost. For tickets with more replies, use the dedicated /api/v2/tickets/{id}/conversations endpoint with pagination.
  2. Download each file from the content_url via authenticated GET request.
  3. Re-host the file (S3 bucket, GCS, or any publicly accessible URL).
  4. Pass the hosted URL into Pylon's attachment_urls array when creating the issue.

Freshdesk's S3 URLs are temporary. If you extract ticket metadata today but download attachments next week, the URLs may have expired. Treat every attachment as a separate download-and-reupload problem, and do it during extraction, not later.

Danger

Inline images will break. Freshdesk inline images in HTML ticket descriptions use Freshdesk-hosted URLs (*.freshdesk.com/...). After you cancel Freshdesk, those URLs die. Every inline image in every ticket description must be parsed, downloaded, re-hosted, and URL-rewritten before loading into Pylon.

Inline Image Rewriting: Step-by-Step

Inline images are the most common source of silent data loss. Here is the concrete process:

  1. Parse the HTML body of each ticket description and conversation reply. Look for <img> tags where the src attribute contains a Freshdesk-hosted domain (e.g., *.freshdesk.com, *.freshdesk-cdn.com, or *.amazonaws.com URLs generated by Freshdesk).
  2. Download each image via authenticated GET request during extraction — not later. These URLs expire.
  3. Re-host to your own storage (S3, GCS, or any CDN that serves public URLs).
  4. Rewrite the src attribute in the HTML to point to the new hosted URL.
  5. Pass the rewritten HTML as body_html when creating the Issue or Message in Pylon.
import re
import requests
 
def rewrite_inline_images(html_body, fd_api_key, s3_upload_fn):
    """Parse HTML for Freshdesk-hosted images, re-host, rewrite URLs."""
    img_pattern = re.compile(r'(<img[^>]+src=["\'])([^"\']*(freshdesk\.com|freshdesk-cdn\.com|amazonaws\.com)[^"\']*)(["\'][^>]*>)')
 
    def replace_match(match):
        prefix, original_url, _, suffix = match.groups()
        # Download from Freshdesk
        resp = requests.get(original_url, auth=(fd_api_key, "X"))
        if resp.status_code != 200:
            return match.group(0)  # Leave original if download fails
        # Upload to your storage, get new URL
        new_url = s3_upload_fn(resp.content, original_url)
        return f"{prefix}{new_url}{suffix}"
 
    return img_pattern.sub(replace_match, html_body)
Tip

Run this during extraction, not as a post-processing step. If you wait until after Freshdesk account cancellation, the source URLs will be dead and the images are gone permanently.

Pylon-Side Rate Limits

Pylon enforces per-endpoint rate limits that are significantly lower than Freshdesk's extraction limits. Your pipeline needs a write-side throttle. (docs.usepylon.com)

Pylon Endpoint Rate Limit
Account/Contact create & update 60 req/min
Issue creation 10 req/min
Issue search 20 req/min
Attachment upload 10 req/min

At 10 issues per minute, a 10,000-ticket migration takes approximately 1,000 minutes (~16.7 hours) of write time for issue creation alone — before adding messages, attachments, and custom field updates. Plan accordingly.

Only Admin users can create Pylon API tokens. And one safety feature matters during migration: issue creation supports destination_metadata with an "internal" destination, so imported history does not trigger notifications to customers.

Step-by-Step API Migration Process

The freshdesk to pylon migration data export import steps follow a strict dependency order. Violating it breaks relationships. When building your pipeline, always cross-reference the Pylon support docs for the latest schema requirements, as the freshdesk to pylon migration data export import process requires precise payload formatting.

Phase 1: Extract from Freshdesk

Extraction order:
1. Companies     → GET /api/v2/companies?per_page=100
2. Contacts      → GET /api/v2/contacts?per_page=100
3. Agents        → GET /api/v2/agents?per_page=100
4. Tickets       → GET /api/v2/tickets?per_page=100&updated_since=...&include=description
5. Conversations → GET /api/v2/tickets/{id}/conversations
6. Attachments   → GET each content_url
7. KB Articles   → GET /api/v2/solutions/categories → folders → articles

Store everything locally (JSON files or staging database) before attempting any writes. This decouples extraction from loading and lets you retry the load phase without re-extracting.

Sample: Paginated ticket extraction with rate-limit backoff

import requests
import time
 
FD_DOMAIN = "yourdomain.freshdesk.com"
FD_API_KEY = "your_api_key"
 
def extract_all_tickets(updated_since="2026-01-01T00:00:00Z"):
    """Extract tickets with pagination and rate-limit backoff."""
    all_tickets = []
    page = 1
 
    while page <= 300:  # Freshdesk hard cap: 300 pages
        url = (
            f"https://{FD_DOMAIN}/api/v2/tickets"
            f"?per_page=100&page={page}"
            f"&updated_since={updated_since}"
            f"&include=description"
        )
        resp = requests.get(url, auth=(FD_API_KEY, "X"))
 
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            print(f"Rate limited. Sleeping {retry_after}s...")
            time.sleep(retry_after)
            continue  # Retry same page
 
        resp.raise_for_status()
        tickets = resp.json()
 
        if not tickets:
            break  # No more results
 
        all_tickets.extend(tickets)
        print(f"Page {page}: {len(tickets)} tickets (total: {len(all_tickets)})")
 
        # Check remaining rate limit
        remaining = int(resp.headers.get("X-Ratelimit-Remaining", 1))
        if remaining < 5:
            time.sleep(10)  # Proactive backoff
 
        page += 1
 
    return all_tickets
curl -u "$FD_API_KEY:X" \
  "https://yourdomain.freshdesk.com/api/v2/tickets?updated_since=2026-01-01T00:00:00Z&include=description&page=1"

Phase 2: Transform

  • Map Freshdesk company IDs → Pylon account names/domains
  • Map Freshdesk contact emails → Pylon contact objects
  • Map Freshdesk agent emails → Pylon user IDs
  • Convert status integers to Pylon state strings
  • Convert priority integers to Pylon priority strings
  • Map cf_* custom fields to Pylon custom field slugs
  • Parse HTML descriptions for inline images; download, re-host, and rewrite URLs
  • Upload attachments to your hosting; generate public URLs
  • Build ID lookup tables (Freshdesk ID → staged ID) for accounts, contacts, and users

Phase 3: Load into Pylon

Load order (strict):
1. Create Accounts      → POST /accounts
2. Create Contacts      → POST /contacts (with account_id)
3. Create Custom Fields  → Pre-create via Pylon UI or API before loading issues
4. Create Issues        → POST /issues (with account_id, contact_id, assignee_id)
5. Add Messages         → POST /issues/{id}/messages (in chronological order)
6. Create KB Collections → POST /knowledge-bases/{id}/collections
7. Create KB Articles    → POST /knowledge-bases/{id}/articles
Warning

When creating issues via API, set destination_metadata.destination to "internal" to prevent Pylon from sending email or Slack notifications to the customer for every historical ticket you import.

Example: Creating an Issue in Pylon

import requests
import time
 
PYLON_BASE = "https://api.usepylon.com"
PYLON_TOKEN = "your_bearer_token"
 
def create_pylon_issue(ticket, account_map, contact_map, user_map):
    payload = {
        "title": ticket["subject"],
        "body_html": ticket["description"],
        "account_id": account_map.get(ticket["company_id"]),
        "contact_id": contact_map.get(ticket["requester_id"]),
        "assignee_id": user_map.get(ticket["responder_id"]),
        "priority": {1: "low", 2: "medium", 3: "high", 4: "urgent"}.get(
            ticket["priority"], "medium"
        ),
        "state": {2: "new", 3: "waiting_on_customer", 4: "closed", 5: "closed"}.get(
            ticket["status"], "new"
        ),
        "created_at": ticket["created_at"],
        "tags": ticket.get("tags", []),
        "attachment_urls": ticket.get("rehosted_attachment_urls", []),
        "destination_metadata": {"destination": "internal"},
        "custom_fields": [
            {"slug": slug, "value": val}
            for slug, val in transform_custom_fields(ticket).items()
        ],
    }
 
    resp = requests.post(
        f"{PYLON_BASE}/issues",
        json=payload,
        headers={"Authorization": f"Bearer {PYLON_TOKEN}"},
    )
 
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return create_pylon_issue(ticket, account_map, contact_map, user_map)
 
    resp.raise_for_status()
    return resp.json()["data"]["id"]

Example: Adding Messages to an Issue

Conversation threading is where migrations most commonly lose fidelity. Each Freshdesk conversation entry (reply or note) becomes a Message on the corresponding Pylon Issue.

def create_pylon_message(issue_id, conversation, contact_map, user_map):
    """Create a message on a Pylon issue from a Freshdesk conversation."""
 
    # Determine if this is an internal note or customer-facing reply
    is_private = conversation.get("private", False)
 
    # Map the sender
    # Freshdesk conversations have user_id (agent) or from_email (contact)
    sender_id = None
    if conversation.get("user_id"):
        sender_id = user_map.get(conversation["user_id"])
 
    payload = {
        "body_html": conversation.get("body", ""),
        "is_internal": is_private,
        "attachment_urls": conversation.get("rehosted_attachment_urls", []),
        "created_at": conversation["created_at"],
    }
 
    if sender_id:
        payload["user_id"] = sender_id
 
    resp = requests.post(
        f"{PYLON_BASE}/issues/{issue_id}/messages",
        json=payload,
        headers={"Authorization": f"Bearer {PYLON_TOKEN}"},
    )
 
    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 60))
        time.sleep(retry_after)
        return create_pylon_message(issue_id, conversation, contact_map, user_map)
 
    resp.raise_for_status()
    return resp.json()["data"]["id"]
 
 
def migrate_conversations(issue_id, freshdesk_ticket_id, contact_map, user_map):
    """Fetch all conversations for a ticket and create them as messages in order."""
    conversations = []
    page = 1
 
    # Paginate through all conversations (don't rely on include=conversations,
    # which caps at 10)
    while True:
        resp = requests.get(
            f"https://{FD_DOMAIN}/api/v2/tickets/{freshdesk_ticket_id}/conversations"
            f"?per_page=100&page={page}",
            auth=(FD_API_KEY, "X"),
        )
 
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 60)))
            continue
 
        resp.raise_for_status()
        batch = resp.json()
        if not batch:
            break
        conversations.extend(batch)
        page += 1
 
    # Sort chronologically to preserve thread order
    conversations.sort(key=lambda c: c["created_at"])
 
    message_ids = []
    for conv in conversations:
        msg_id = create_pylon_message(issue_id, conv, contact_map, user_map)
        message_ids.append(msg_id)
 
    return message_ids
Info

Critical: Preserve message order. Sort conversations by created_at before loading. If messages arrive out of order, the thread in Pylon will be unreadable. Also ensure is_internal is set correctly — leaking internal notes to customer-facing views is a serious support failure.

Error Handling & Logging

Every API call should log:

  • Freshdesk source ID
  • Pylon target ID (on success)
  • HTTP status code
  • Error body and Pylon request_id (on failure)
  • Timestamp

Build an ID mapping table (Freshdesk ID → Pylon ID) and persist it. You need it for rebuilding relationships, debugging, running validation, and future integrations. Pylon explicitly supports external identifiers on accounts and contacts, which makes deterministic upserts easier on reruns. (docs.usepylon.com)

Make loaders idempotent using external IDs and mapping tables. Send failed records to a dead-letter queue, not back into the hot path.

Delta Sync & Cutover Plan

A bulk migration gives you a snapshot. But tickets keep coming in while you migrate. Without a delta sync, you either freeze Freshdesk (downtime) or lose every ticket created after the bulk export started.

How Delta Sync Works

The goal is simple: after the bulk migration completes, continuously poll Freshdesk for changes and replay them into Pylon until the delta reaches zero, then cut over.

Polling strategy:

  1. Record the timestamp when your bulk extraction started — call this bulk_start_ts.
  2. After the bulk load completes, start a delta sync loop that queries Freshdesk for tickets updated since bulk_start_ts:
def delta_sync_loop(bulk_start_ts, mapping_table, account_map, contact_map, user_map):
    """Continuously sync changes from Freshdesk to Pylon until delta reaches zero."""
    poll_since = bulk_start_ts
    consecutive_zero_rounds = 0
 
    while consecutive_zero_rounds < 3:  # 3 consecutive empty polls = safe to cut over
        tickets = extract_tickets_updated_since(poll_since)
 
        if not tickets:
            consecutive_zero_rounds += 1
            print(f"No changes found. Zero round {consecutive_zero_rounds}/3")
            time.sleep(300)  # Wait 5 minutes before next poll
            continue
 
        consecutive_zero_rounds = 0  # Reset on any changes found
 
        for ticket in tickets:
            fd_id = ticket["id"]
            pylon_id = mapping_table.get(fd_id)
 
            if pylon_id:
                # Ticket was already migrated — update it
                update_pylon_issue(pylon_id, ticket)
                # Re-sync conversations (fetch all, diff against what's in Pylon)
                sync_new_conversations(pylon_id, fd_id, contact_map, user_map)
            else:
                # New ticket created after bulk export — create it
                new_id = create_pylon_issue(ticket, account_map, contact_map, user_map)
                mapping_table[fd_id] = new_id
                migrate_conversations(new_id, fd_id, contact_map, user_map)
 
        # Advance the watermark to the latest updated_at in this batch
        poll_since = max(t["updated_at"] for t in tickets)
        print(f"Delta batch: {len(tickets)} tickets. Next poll from {poll_since}")
        time.sleep(60)  # Poll every minute during active sync
 
    print("Delta sync reached zero. Safe to cut over.")
 
 
def extract_tickets_updated_since(since_ts):
    """Fetch tickets updated since a given timestamp."""
    all_tickets = []
    page = 1
    while page <= 300:
        resp = requests.get(
            f"https://{FD_DOMAIN}/api/v2/tickets"
            f"?per_page=100&page={page}&updated_since={since_ts}&include=description",
            auth=(FD_API_KEY, "X"),
        )
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 60)))
            continue
        resp.raise_for_status()
        batch = resp.json()
        if not batch:
            break
        all_tickets.extend(batch)
        page += 1
    return all_tickets
  1. For each ticket returned:
    • If fd_id exists in the mapping table: The ticket was already migrated. Update the corresponding Pylon issue with any changed fields. Fetch conversations and create any that are newer than the last synced conversation.
    • If fd_id does not exist in the mapping table: This is a new ticket. Create it in Pylon (with messages and attachments) and add it to the mapping table.
  2. After each batch, advance the poll_since watermark to the latest updated_at value in the batch.
  3. Repeat until three consecutive polling rounds return zero results.

Conflict resolution: Freshdesk is the source of truth during the delta sync period. If an agent updates a ticket in both Freshdesk and Pylon (which should not happen if you enforce process discipline), Freshdesk wins. The simplest approach: do not give agents write access to migrated issues in Pylon until cutover.

Cutover go/no-go checklist:

  • Three consecutive delta polling rounds returned zero changes
  • Validation counts match between Freshdesk and Pylon (accounts, contacts, issues)
  • Golden-sample field-level QA passed
  • Email routing is ready to switch (see Email Routing section)
  • Slack/Teams channels are mapped and ready to reconnect
  • Support team has been trained on Pylon

Cutover Sequence

  1. Run a final delta sync pass.
  2. Verify the delta is zero.
  3. Switch email routing to Pylon (see Email Routing section below).
  4. Reconnect Slack/Teams channels to Pylon.
  5. Set Freshdesk to read-only (remove agent licenses or disable email intake).
  6. Monitor Pylon for 24–48 hours.
  7. After 7 days with no issues, decommission Freshdesk.

Email Routing & DNS Cutover

Post-migration, email sent to support@yourdomain.com needs to route to Pylon instead of Freshdesk. This is a critical cutover step that, if mishandled, causes lost tickets.

Steps:

  1. Before cutover — set up email in Pylon. Configure your support email address in Pylon's settings so it is ready to receive.
  2. Update email forwarding rules. If your support email uses forwarding (e.g., Google Workspace or Microsoft 365 forwarding support@ to Freshdesk's intake address like support@yourdomain.freshdesk.com), update the forwarding destination to Pylon's intake address.
  3. If using MX records pointing to Freshdesk: Update MX records to point to your email provider (Google Workspace, Microsoft 365, etc.) and set up forwarding from there to Pylon. MX record changes can take up to 48 hours to propagate — plan for this.
  4. Update SPF and DKIM records to authorize Pylon to send email on behalf of your domain. Without this, outbound replies from Pylon may land in spam.
  5. Test the full loop before cutover: Send a test email to support@yourdomain.com and verify it creates an Issue in Pylon with correct routing.
  6. During the transition period, monitor both Freshdesk and Pylon for 24–48 hours to catch emails that arrive at the old destination due to DNS propagation delays.
Warning

Do not cancel your Freshdesk account until email routing is fully verified in Pylon and you have confirmed no emails are still arriving at Freshdesk. A premature cancellation means bounced customer emails.

Edge Cases & What Breaks

Duplicate Contacts

Freshdesk allows multiple contacts with the same email across different companies. Pylon's /import/contacts endpoint supports this — the same email can exist across different accounts — but standard POST /contacts may enforce uniqueness. Deduplicate before loading, or use the import endpoint.

Missing Relationships

If a ticket's requester_id references a contact that was not migrated (deleted user, spam contact), the issue creation will fail or create an orphaned record. Pre-validate all foreign keys before loading.

Inline Images

Freshdesk users often paste images directly into replies. These are hosted on Freshdesk's CDN. If you do not download and re-host these images during migration, they will render as broken links in Pylon once the Freshdesk account is closed.

Time Tracking and CSAT

Freshdesk time entries have no Pylon counterpart — archive them to a data warehouse. Historical CSAT scores can be stored as custom field values but will not feed into Pylon's native CSAT analytics.

Historical SLA Metrics

Pylon calculates SLAs based on its own internal logic. Historical SLA compliance metrics from Freshdesk cannot be imported as native SLA objects. Store them as custom text fields or export to a data warehouse.

Conversation Replay

Before committing to a DIY build, confirm the exact historical message replay behavior you need. Pylon's API supports creating messages on issues, but verify whether reply ordering, internal note attribution, and full thread reconstruction match your requirements.

Validation & Testing

Do not trust the migration until you have validated it.

Record count comparison:

  • Compare total accounts, contacts, and issues between Freshdesk and Pylon
  • Account for intentionally excluded records (spam, duplicates, test data, archived tickets you chose not to migrate)

Field-level validation (golden sample):

  • Pull 50–100 tickets at random from Freshdesk
  • Compare every field against the corresponding Pylon issue
  • Verify: title, body HTML, status, priority, requester, assignee, account, tags, custom fields, created_at, attachment count

Relationship integrity:

  • Every issue should link to a valid account and contact
  • Every contact should link to a valid account
  • No orphaned issues (requester = null)

Conversation threading:

  • Verify that reply order is preserved
  • Verify internal notes vs. customer-facing messages are correctly attributed
  • Check that conversation counts match between Freshdesk and Pylon for sampled tickets

Attachment QA:

  • Open files from 20–50 sampled issues across file types
  • Check for broken inline images in HTML descriptions
  • Verify that rewritten inline image URLs resolve correctly

Email routing verification:

  • Send test emails to support@yourdomain.com and confirm Issues are created in Pylon
  • Verify outbound replies from Pylon are not flagged as spam (SPF/DKIM)

Rollback plan:

A real rollback plan goes beyond "keep using Freshdesk." Here is what to prepare:

  1. Do not cancel Freshdesk until at least 7 days post-cutover. Keep it in read-only mode (disable email intake, remove agent licenses).
  2. Email routing revert: If you updated forwarding rules, keep the old Freshdesk intake address documented so you can re-point in minutes. If you changed MX records, keep the old values documented (MX revert takes up to 48 hours — this is your biggest rollback latency).
  3. Slack/Teams channels: Document which channels were reconnected to Pylon. To roll back, disconnect from Pylon and re-bind to Freshdesk.
  4. Pylon data cleanup: If you need to wipe the Pylon instance and retry, use Pylon's bulk delete API. Delete in reverse load order: Messages → Issues → Contacts → Accounts.
  5. Staging database is your safety net. Do not delete your staging database until the migration is fully signed off. If the Pylon import fails or data corruption is discovered, you can wipe Pylon and re-run the load from staging without re-extracting from Freshdesk.
  6. Decision criteria — rollback vs. fix-forward:
    • Rollback if: data corruption is widespread (>5% of records), email routing is failing and cannot be fixed within 2 hours, or agents cannot perform basic support operations in Pylon.
    • Fix forward if: issues are isolated (specific field mapping errors, a handful of missing attachments), email is routing correctly, and agents can work while you patch.

Rebuilding Automations & Triggers

No migration tool transfers Freshdesk automations, Dispatch'r rules, Observer rules, or Supervisor rules to Pylon. These must be rebuilt manually. Here is a mapping of common patterns:

Freshdesk → Pylon Automation Mapping

Freshdesk Automation Pylon Equivalent Rebuild Notes
Dispatch'r (ticket creation rule): Assign to group based on tag Pylon Trigger: condition on tag → action: assign to Team Create the Team first, then build the trigger
Dispatch'r: Set priority based on subject keyword Pylon Trigger: condition on title contains keyword → action: set priority Pylon triggers support regex or keyword matching on issue title
Dispatch'r: Auto-assign to agent based on company Pylon Trigger: condition on account → action: assign to user Requires account to be linked on issue creation
Observer (ticket update rule): Notify manager when priority = Urgent Pylon Trigger: condition on priority change to urgent → action: send notification Configure notification channel (Slack, email) in Pylon
Observer: Reopen ticket on customer reply Pylon: Built-in behavior Pylon reopens closed issues on new activity by default
Supervisor (time-based rule): Escalate if no response in 4 hours Pylon SLA policy + escalation trigger Set up SLA policy first, then configure breach action
Canned responses Pylon Macros Manual recreation — export canned response text from Freshdesk, create as macros in Pylon
Auto-responder on ticket creation Pylon Trigger: on issue creation → action: send reply Configure reply template in Pylon
Satisfaction survey after resolution Pylon CSAT configuration Enable in Pylon settings; configure timing and channel
Tip

Before rebuilding, audit which automations are actually used. Many Freshdesk instances accumulate dozens of Dispatch'r and Observer rules over the years, some conflicting. Migration is a good time to prune. Export the list from Freshdesk (Admin → Automations), mark each as keep/discard, then only rebuild the ones that matter.

Rebuild sequence:

  1. SLA policies and business hours — these must exist before escalation triggers reference them.
  2. Teams — assignment triggers need Teams to exist.
  3. Triggers — build in order: creation-time rules first, then update-time rules, then time-based rules.
  4. Macros — recreate canned responses as Pylon macros.
  5. CSAT surveys — enable and configure.
  6. Views and assignment rules — set up agent-facing dashboards.

Best Practices

  1. Back up everything. Request a full Account Export from Freshdesk before touching anything.
  2. Run a test migration first. Migrate 100 tickets to a Pylon sandbox environment. Validate every field.
  3. Migrate in dependency order. Accounts → Contacts → Custom Fields → Issues → Messages. Never reverse this.
  4. Use destination: internal on all issue creation calls to prevent customer notifications.
  5. Preserve created_at timestamps. Without this, your historical reporting in Pylon is useless.
  6. Build an ID mapping table. Freshdesk ID → Pylon ID for every object. You need it for debugging, validation, and any future integrations.
  7. Disable Freshdesk automations before export to prevent webhooks from consuming your rate limit.
  8. Schedule bulk extraction during off-peak hours to avoid competing with live agent API usage.
  9. Pre-create all custom fields, tags, and teams in Pylon before loading any records. Select fields will reject unmapped values.
  10. Download attachments and inline images during extraction, not later. Freshdesk S3 URLs expire, and Freshdesk-hosted inline image URLs die when you cancel the account.

Post-Migration Tasks

Data migration is half the job. The other half is rebuilding operational configuration that no migration tool transfers.

Rebuild in Pylon (manual):

  • SLA policies and support hours
  • Triggers and workflow automations (see Rebuilding Automations & Triggers section above)
  • Macros (Freshdesk canned responses → Pylon macros)
  • CSAT surveys
  • Views and assignment rules
  • Team structure and roles
  • Email routing and forwarding (see Email Routing & DNS Cutover section above)
  • Chat widget configuration
  • Integrations (Salesforce, HubSpot, Linear, Jira)

Monitoring (first 30 days):

  • Watch for orphaned issues (no account or contact)
  • Monitor Pylon's analytics for data gaps
  • Check knowledge base articles for broken links and missing images
  • Verify CSAT surveys are firing on new issues
  • Keep Freshdesk in read-only mode for at least 7 days while monitoring Pylon for inconsistencies
  • Verify email routing is stable — no emails arriving at Freshdesk after cutover

When to Use a Managed Migration Service

Build in-house when:

  • Fewer than 5,000 tickets
  • No attachments or inline images to preserve
  • Simple custom field structure
  • An engineer with a week to dedicate

Use a managed service when:

  • More than 10,000 tickets with attachments
  • Complex custom field and status mapping
  • You need zero downtime (parallel operation + delta sync)
  • Your engineering team cannot afford the distraction
  • Compliance requirements (HIPAA, SOC 2) demand auditable data handling
  • Multi-level account hierarchies, agency structures, or partner accounts

Building an in-house ETL pipeline for a one-time migration is rarely the best use of engineering resources. The Freshdesk API constraints covered in this guide — 429 throttling, the 30,000-ticket ceiling, temporary S3 attachment URLs, inline image rewriting — require dedicated backend expertise for a task your team will never repeat.

ClonePartner handles the rate-limit orchestration, attachment re-hosting, relationship integrity, and delta sync so your engineering team stays focused on product work. We run your Freshdesk and Pylon instances in parallel with continuous delta syncs, enabling a zero-downtime cutover. Most Freshdesk-to-Pylon migrations complete in days, not weeks.

Frequently Asked Questions

Can I migrate Freshdesk tickets to Pylon using CSV export?
Only partially. Freshdesk CSV exports include ticket metadata but not full conversation histories, attachments, or archived tickets. Pylon has no CSV import UI, so you still need the API to load data. For anything beyond a basic snapshot of fewer than 1,000 tickets, API-based migration is required.
What are the Freshdesk API rate limits for data migration?
Freshdesk enforces account-wide rate limits by plan: Growth gets 200 calls/min, Pro gets 400/min, and Enterprise gets 700/min. Individual endpoints have sublimits — ticket listing is roughly 20 calls/min on Growth. The limit is shared with all integrations, and even failed requests count. The List All Tickets endpoint is capped at 300 pages (30,000 tickets max).
What is Pylon's API rate limit for importing data?
Pylon enforces per-endpoint rate limits: 60 req/min for account and contact operations, 10 req/min for issue creation, 20 req/min for issue search, and 10 req/min for attachment uploads. This is significantly lower than Freshdesk's extraction limits, so your pipeline needs a write-side throttle.
How do I migrate Freshdesk ticket attachments to Pylon?
Freshdesk provides temporary S3 URLs for attachments in the API response. Download each file via authenticated GET requests during extraction (URLs expire), re-host them on your own storage (S3, GCS), then pass the new public URLs into Pylon's attachment_urls array when creating issues. Inline images in HTML descriptions also need to be parsed, downloaded, re-hosted, and URL-rewritten.
How do I prevent customer notifications during a Freshdesk to Pylon import?
Create historical Pylon issues with destination_metadata.destination set to 'internal'. This prevents Pylon from sending email or Slack notifications to customers for every imported ticket. Patch workflow data like status and tags after validation.

More from our Blog