Skip to content

SurveySparrow to Pylon Migration: The Technical Guide

Technical guide to migrating from SurveySparrow Ticket Management to Pylon. Covers API extraction, data model mapping, rate limits, and cutover.

Rishabh Rishabh · · 19 min read
SurveySparrow to Pylon Migration: The Technical Guide
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

SurveySparrow to Pylon Migration: The Technical Guide

Info

TL;DR — SurveySparrow Ticket Management to Pylon Migration

Migrating from SurveySparrow Ticket Management to Pylon is an API-led data model translation. There is no native migration path, no pre-built connector, and no CSV import on the Pylon side that handles tickets. Every migration requires extracting tickets and contacts from SurveySparrow's REST API (v3), transforming them into Pylon's Issue→Account→Contact→Message data model, and loading them through Pylon's REST API. The biggest design decision is mapping SurveySparrow's contact-centric tickets to Pylon's strict Account-based architecture. The biggest throughput bottleneck is Pylon's rate limit on issue creation: 10 requests per minute. Realistic end-to-end timeline: 5–12 business days depending on volume, custom field complexity, and whether you need to reconstruct comment threads as messages.

What Is a SurveySparrow to Pylon Migration?

A SurveySparrow Ticket Management to Pylon migration is the process of extracting Tickets, Contacts, Ticket Comments, Custom Fields, and Teams from SurveySparrow and loading them into Pylon as Issues, Contacts, Accounts, Messages, Custom Fields, and Teams.

SurveySparrow Ticket Management is a ticketing layer inside SurveySparrow's experience management platform. Its core objects are Tickets — discrete units of work with subject, description, priority, status, assignee, due date, and custom fields. Tickets are typically generated from survey responses, NPS detractors, form submissions, or email.

Pylon is an AI-native B2B support platform where the core object is the Issue — a conversational thread tied to an Account. Pylon supports Slack, Microsoft Teams, Discord, email, chat, SMS, WhatsApp, phone, and Telegram as customer communication channels. Every Issue belongs to an Account, giving agents full customer context including CRM data, health scores, and renewal dates.

The structural gap is substantial. SurveySparrow is feedback-first and contact-centric. Pylon is conversation-first and account-centric. Pushing SurveySparrow tickets into Pylon without first constructing the Account layer will result in rejected payloads or orphaned Issues with no account context.

Why Teams Move from SurveySparrow to Pylon

Three patterns drive this migration:

  • Outgrowing feedback-driven ticketing. SurveySparrow's ticketing closes the feedback loop — a bad survey response becomes an action item. When your support operation needs full omnichannel management with Slack, Teams, and Discord as primary channels, SurveySparrow's model doesn't scale.
  • Account-level visibility. Pylon lets you organize issues by status, owner, priority, account, SLA, or any custom field. SurveySparrow tickets are contact-centric, not account-centric. B2B teams that need to see all open issues for a customer account — across channels — hit a structural wall in SurveySparrow.
  • AI and automation depth. Pylon's Background Agents can investigate and perform triggered work on issues. Its Assist Agent helps teammates investigate and act. The Support Agent can resolve customer questions or escalate them with full context. SurveySparrow offers workflow automation, but it's designed around survey-triggered actions, not support triage.

Data Model Comparison

Before writing migration code, understand where the two models align and where they diverge.

SurveySparrow Object Pylon Object Notes
Ticket Issue 1:1 mapping. Subject → Issue title. Description → first message body.
Ticket Comment (public) Message (reply) Each comment becomes a Message. Preserve internal vs. public distinction.
Ticket Comment (private) Message (internal note) Keep visibility markers.
Contact (requester) Contact Email is the natural dedup key.
Company Account SurveySparrow may not always have a Company on every ticket.
Team Team Recreate in Pylon before loading issues.
Agent (user) User Must exist in Pylon before issue assignment.
Custom Field Custom Field Pylon supports text, select, date, user, boolean. Type mismatches require transformation.
Priority Priority or Custom Field Map to Pylon's priority levels.
Status Issue State Pylon uses its own state model. Explicit mapping required.
Ticket Template Ticket Form Conceptual equivalent. Must be recreated manually.
SLA Configuration SLA Policy Must be recreated. No migration path for SLA rules.
Source (Survey/Email/Call) Custom Field or Tag Preserve source metadata explicitly.
Related Ticket (parent-child) Custom Field + crosswalk No direct parent-child issue import in Pylon's public API.
Survey Response (linked) No equivalent Survey context is lost. Preserve source metadata in a custom field if needed.
Ticket Watchers/Followers Issue Followers Map follower user IDs to Pylon user IDs. Followers not in Pylon must be created first or skipped.
No equivalent Account Critical gap. Every Pylon Issue requires an Account.
Warning

The Account gap is the biggest design decision in this migration. SurveySparrow tickets have a requester (Contact) but not always an Account. Pylon requires every Issue to be associated with an Account. You must either: (1) derive Accounts from contact email domains, (2) import Accounts from your CRM, or (3) create a catch-all Account for unattributed tickets. Option 1 is the most common approach for B2B teams. Details and transformation code are in Step 3.

Step 1: Inventory the Source Model

Before exporting anything, document all active ticket fields, ticket templates, statuses, teams, business hours, SLA rules, email behavior, workflows, and filter logic that depends on survey, contact, or review properties. This is where teams discover that a field they assumed was reporting metadata is actually driving escalations or routing behavior.

Pull field definitions from the SurveySparrow API:

GET /v3/ticket_fields

Document the internal_name, type, mandatory flag, and options for every custom field. This inventory becomes your field mapping blueprint for Step 5.

Step 2: Export Data from SurveySparrow

SurveySparrow offers multiple extraction paths. However, whether you are migrating to Pylon, Zendesk, or a collaborative inbox like Missive, native UI exports do not preserve complete threaded replies or attachment binaries. For a full migration, only the REST API is viable.

Path A: REST API (v3) — Required for Migration

SurveySparrow uses OAuth 2.0 for authentication. Generate an access token via Settings → Apps & Integrations.

curl --request GET \
  --url 'https://api.surveysparrow.com/v3/tickets?page=1' \
  --header 'Authorization: Bearer YOUR_TOKEN'

Key endpoints:

  • GET /v3/tickets — All tickets, paginated
  • GET /v3/tickets/:id — Single ticket with full detail
  • GET /v3/tickets/:id/comments?private=false — Public comments
  • GET /v3/tickets/:id/comments?private=true — Private comments
  • GET /v3/ticket-fields — Custom field definitions
  • GET /v3/contacts — Contact records
  • GET /v3/users — Agent records
  • GET /v3/teams?type=TICKET — Ticket teams
Info

SurveySparrow operates across multiple data centers. The correct API base URL depends on which data center your account is hosted on. Using the wrong base URL returns authentication errors that appear to be token issues. Contact [email protected] to confirm your data center before starting extraction.

Rate limits: SurveySparrow's documented API rate limit is 120 calls per hour on baseline plans. Confirm your plan's actual limit with SurveySparrow support before starting — higher-tier plans may have elevated ceilings. Implement exponential backoff for HTTP 429 responses with an initial delay of 2 seconds, doubling on each retry up to a 60-second ceiling.

Pagination: SurveySparrow uses page and per_page query parameters. Tickets, comments, and ticket fields support up to 100 records per page. Users and contacts max out at 50 per page. For large datasets (10K+ tickets), sort by created_at ascending and use time-based windowing to avoid deep-pagination performance degradation.

Extract in dependency order:

GET /v3/ticket_fields    # Field definitions first
GET /v3/users            # Agent records
GET /v3/teams?type=TICKET
GET /v3/contacts
GET /v3/tickets          # All tickets, paginated
GET /v3/tickets/{id}/comments?private=false  # Per ticket
GET /v3/tickets/{id}/comments?private=true   # Per ticket

Store raw JSON payloads in a local database (PostgreSQL, MongoDB) or cloud storage (S3) before transformation. This decouples extraction from transformation, letting you re-run transforms without hitting SurveySparrow's rate limits again.

Path B: UI Export (Excel/JSON)

Under Ticket Management, click Export Data. The data can be exported in Excel (xlsx) or JSON format. Select the format and choose which fields to include. Ticket information is categorized as Ticket fields, Assignee details, and Requester details.

Useful for pre-migration auditing and record counts. This export does not include full comment threads, and attachment URLs may not be present.

Path C: CSV from Tickets List View

The CSV export gives ticket information based on your current view's selected columns and applied filters. Data outside the active column selection will be missing. Use it for quick record-count validation only.

For a deeper walkthrough of SurveySparrow export options, see How to Export Data from SurveySparrow Ticket Management.

Step 3: Build the Account Layer in Pylon

This step has no SurveySparrow equivalent and is unique to Pylon-targeted migrations.

Pylon's data model is account-centric. Every Issue must belong to an Account, and every Account can have channels, domains, CRM settings, custom fields, tags, and an owner. There is no bypass — issuing a POST /issues without a valid account_id will return a validation error.

Account Derivation Strategies

Option 1: Domain-based derivation (most common for B2B teams)

Extract the email domain from each SurveySparrow contact, deduplicate, and create one Account per unique domain.

from urllib.parse import urlparse
import re
 
def extract_domain(email: str) -> str | None:
    """Extract the registrable domain from an email address."""
    if not email or "@" not in email:
        return None
    domain = email.strip().lower().split("@")[-1]
    # Exclude common free/consumer domains
    consumer_domains = {"gmail.com", "yahoo.com", "hotmail.com", "outlook.com"}
    if domain in consumer_domains:
        return None
    return domain
 
def build_account_map(contacts: list[dict]) -> dict[str, dict]:
    """
    Returns a dict mapping domain -> account payload.
    Skips consumer email domains.
    """
    accounts = {}
    for contact in contacts:
        domain = extract_domain(contact.get("email", ""))
        if domain and domain not in accounts:
            accounts[domain] = {
                "name": domain.split(".")[0].title(),  # e.g., acme.com → Acme
                "domain": domain,
                "account_type": "customer",
            }
    return accounts

Option 2: CRM-based derivation (highest accuracy)

If you already have Salesforce or HubSpot connected to Pylon, sync accounts through Pylon's native CRM integration before migrating tickets. Then, during contact migration, match each SurveySparrow contact to the correct Pylon Account using the CRM's company field or domain. This avoids creating orphan accounts that diverge from your CRM's source of truth.

Option 3: Catch-all Account (last resort)

Create a single Account named "Unattributed" in Pylon and assign all domain-less or consumer-email contacts to it. This is only appropriate for B2C support operations or for tickets with no meaningful company context.

Creating Accounts via API

curl --request POST \
  --url 'https://api.usepylon.com/accounts' \
  --header 'Authorization: Bearer YOUR_PYLON_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Acme Corp",
    "domain": "acme.com",
    "account_type": "customer"
  }'

Pylon's Accounts endpoint has a rate limit of 60 requests per minute. For 500 accounts, that's under 9 minutes at maximum throughput.

Store the resulting Pylon account_id alongside the source domain in a crosswalk table — you will reference this table in every subsequent step.

Tip

If you already have account data in a CRM (Salesforce, HubSpot), consider syncing accounts through Pylon's native CRM integration first, then mapping contacts to those accounts during the migration. This avoids creating orphan accounts that diverge from your CRM.

Step 4: Migrate Contacts

SurveySparrow contacts map cleanly to Pylon contacts. Use email as the dedup key. Before creating any contacts, look up whether a Pylon contact with that email already exists to prevent duplicates.

SurveySparrow Contact Field Pylon Contact Field
email email
first_name + last_name name (combined)
phone phone
custom properties custom_fields
def transform_contact(ss_contact: dict, account_id: str) -> dict:
    """Transform a SurveySparrow contact payload to a Pylon contact payload."""
    first = ss_contact.get("first_name", "") or ""
    last = ss_contact.get("last_name", "") or ""
    return {
        "email": ss_contact["email"].strip().lower(),
        "name": f"{first} {last}".strip() or ss_contact["email"],
        "phone": ss_contact.get("phone"),
        "account_id": account_id,
    }
curl --request POST \
  --url 'https://api.usepylon.com/contacts' \
  --header 'Authorization: Bearer YOUR_PYLON_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "email": "john@acme.com",
    "name": "John Doe",
    "account_id": "ACME_ACCOUNT_ID"
  }'

Contacts without email. SurveySparrow's CSV importer requires email for every contact. The API may allow contacts without it, but Pylon also expects a contact identifier. If you have contacts with only phone numbers, decide upfront whether to skip them or generate placeholder emails (e.g., unknown+{phone}@placeholder.migration).

Portal roles. Pylon makes portal permissions explicit with no_access, member, admin, and custom roles. If you import everyone with the same role, you risk overexposing ticket history across an account. Import roles deliberately and validate with real end-user accounts before cutover.

Ticket watchers/followers. SurveySparrow tickets may have CC'd contacts or watchers. Map these to Pylon issue followers after issue creation. Any watcher whose email does not match a Pylon contact must be created first or explicitly skipped. Document the decision — silently dropping watchers is a common post-migration complaint.

Step 5: Map and Create Custom Fields

Custom fields must exist in Pylon before loading issues. Creating fields after issues are loaded requires a backfill pass that doubles your load time.

Pylon lets you add any field type to issues: text, select, date, user, boolean. Fields can be autofilled with AI, synced from your CRM, collected from customers at submission, or set via API and triggers.

SurveySparrow Field Type Pylon Equivalent Transformation Notes
Text (single-line) Text Direct map
Text (multiline) Text Pylon text fields handle multiline
Dropdown Select Option values must match exactly — case-sensitive
Nested Dropdown Two separate Select fields Pylon has no nested dropdown. Flatten to category_l1, category_l2. SurveySparrow nested fields have two levels by default; the second level is dependent on the first-level selection.
Number Text Store as string, or use a custom object for numeric operations
Date Date Normalize to ISO 8601 (YYYY-MM-DD)
Checkbox Boolean Direct map

HTML sanitization for text fields. SurveySparrow stores ticket descriptions and comments in HTML. Pylon accepts body_html, so direct HTML ingestion is possible. Before loading, strip unnecessary div tags, inline styles, and proprietary font declarations. If content will render in Slack-like contexts, convert to Markdown using Turndown (Node.js) or html2text (Python):

import html2text
 
def html_to_markdown(html_content: str) -> str:
    converter = html2text.HTML2Text()
    converter.ignore_links = False
    converter.body_width = 0  # Disable line wrapping
    return converter.handle(html_content).strip()

Encoding. Normalize all text fields to UTF-8 before transformation. SurveySparrow may store responses in mixed encodings if data was collected from legacy survey forms.

Create fields via the Pylon Settings UI or API before running the migration. Fields are referenced by their slug when setting values via POST /issues or PATCH /issues/{id}.

Do not migrate every legacy field blindly. Survey name, survey type, NPS rating, and review platform metadata are often better stored as a structured JSON blob in a single Pylon custom field named ss_source_metadata, rather than as individual top-level fields that clutter the issue view.

Step 6: Load Issues and Messages into Pylon

This is the core of the migration and the most rate-limit-constrained step.

Transforming a SurveySparrow Ticket to a Pylon Issue

def transform_ticket(ss_ticket: dict, account_id: str, contact_id: str,
                     field_map: dict, status_map: dict, priority_map: dict) -> dict:
    """
    Map a SurveySparrow ticket JSON payload to a Pylon issue creation payload.
 
    Args:
        ss_ticket:    Raw ticket dict from SurveySparrow API
        account_id:   Pylon account ID (resolved in Step 3)
        contact_id:   Pylon contact ID (resolved in Step 4)
        field_map:    {ss_field_internal_name: pylon_field_slug}
        status_map:   {ss_status_label: pylon_state_slug}
        priority_map: {ss_priority_label: pylon_priority_value}
    """
    custom_fields = {}
    for ss_field_name, pylon_slug in field_map.items():
        value = ss_ticket.get("custom_fields", {}).get(ss_field_name)
        if value is not None:
            custom_fields[pylon_slug] = value
 
    # Preserve migration provenance
    custom_fields["ss_ticket_id"] = str(ss_ticket["id"])
    custom_fields["ss_source_type"] = ss_ticket.get("source", "unknown")
    custom_fields["ss_requester_email"] = ss_ticket.get("requester", {}).get("email")
 
    return {
        "title": ss_ticket.get("subject", f"Migrated Ticket #{ss_ticket['id']}"),
        "body_html": ss_ticket.get("description", ""),
        "account_id": account_id,
        "contact_id": contact_id,
        "state": status_map.get(ss_ticket.get("status", ""), "new"),
        "priority": priority_map.get(ss_ticket.get("priority", ""), "medium"),
        "created_at": ss_ticket.get("created_at"),  # Preserve historical timestamp
        "custom_fields": custom_fields,
    }

For migrated tickets, always pass contact_id explicitly so the Issue is attributed to the original requester, not the API token's admin user. Per Pylon's API behavior, only one of user_id or contact_id can be provided — use contact_id for customer-originated tickets.

Replaying Comments as Messages

Each SurveySparrow ticket comment becomes a Message on the corresponding Pylon Issue. Preserve the internal vs. public distinction — SurveySparrow internal notes must become Pylon internal notes, not customer-facing replies.

def transform_comment(ss_comment: dict, is_private: bool,
                      agent_id_map: dict) -> dict:
    """Map a SurveySparrow comment to a Pylon message payload."""
    author_ss_id = ss_comment.get("author", {}).get("id")
    return {
        "body_html": ss_comment.get("body", ""),
        "is_internal": is_private,
        "created_at": ss_comment.get("created_at"),
        "author_id": agent_id_map.get(str(author_ss_id)),  # Pylon user ID
    }
Warning

Validate the historical message backfill path with Pylon before committing to 1:1 thread replay. Specifically confirm: (1) whether POST /issues/:id/messages accepts a created_at parameter for backdating, and (2) whether the author_id field can be set to a user other than the API token holder. If backdated message creation is not supported, import conversation history as a structured HTML timeline inside the issue's body_html field — ordered by timestamp, with internal/external labels and agent names preserved. This fallback is readable and auditable, and works better than most teams expect.

Preserving Timestamps

Historical accuracy is non-negotiable for SLA reporting and dispute resolution. Pylon's issue creation endpoint accepts a created_at parameter. Test timestamp preservation with 5 tickets before the full run — if the API ignores created_at and stamps every imported ticket with server time, your historical SLA data is permanently corrupted.

Rate Limit Math

Published Pylon rate limits as of the time of writing (verify against Pylon's API documentation before starting):

Endpoint Rate Limit Use
POST /issues 10 req/min Creating issues
POST /issues/:id/messages 20 req/min Adding comments
POST /accounts 60 req/min Creating accounts
POST /contacts 60 req/min Creating contacts
GET /issues 10 req/min Verification reads

Pylon's per-endpoint ceilings — as low as 10 req/min for issue endpoints — mean you cannot treat the REST API as a high-throughput data pipe.

Worked example — 5,000 tickets, 4 comments each:

  • Issue creation: 5,000 ÷ 10/min = 500 minutes (~8.3 hours)
  • Comment replay: 20,000 ÷ 20/min = 1,000 minutes (~16.7 hours)
  • Total API time: ~25 hours (sequential, no failures)

With parallelization you can overlap account and contact creation (both at 60 req/min) with issue loading, but issue creation and message creation are the fixed bottleneck. For 10K+ tickets, plan for the load phase to run over a weekend with overnight windows.

Rate limit queue implementation:

import time
import requests
from collections import deque
 
class RateLimitedClient:
    """
    Sliding-window rate limiter for Pylon API endpoints.
    Tracks request timestamps per endpoint and sleeps as needed.
    """
    def __init__(self, token: str):
        self.token = token
        self.base_url = "https://api.usepylon.com"
        self.windows: dict[str, deque] = {}
        self.limits = {
            "POST /issues": (10, 60),          # 10 req per 60 seconds
            "POST /issues/messages": (20, 60),  # 20 req per 60 seconds
            "POST /accounts": (60, 60),
            "POST /contacts": (60, 60),
        }
 
    def _wait_if_needed(self, endpoint_key: str):
        limit, window = self.limits.get(endpoint_key, (30, 60))
        if endpoint_key not in self.windows:
            self.windows[endpoint_key] = deque()
        queue = self.windows[endpoint_key]
        now = time.time()
        # Remove timestamps outside current window
        while queue and queue[0] < now - window:
            queue.popleft()
        if len(queue) >= limit:
            sleep_time = window - (now - queue[0]) + 0.1
            time.sleep(max(0, sleep_time))
        queue.append(time.time())
 
    def post(self, path: str, payload: dict, endpoint_key: str) -> dict:
        self._wait_if_needed(endpoint_key)
        resp = requests.post(
            f"{self.base_url}{path}",
            json=payload,
            headers={"Authorization": f"Bearer {self.token}",
                     "Content-Type": "application/json"},
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            return self.post(path, payload, endpoint_key)
        resp.raise_for_status()
        return resp.json()

Idempotency

Network timeouts will happen. Before creating an Issue, query Pylon for issues where custom_fields.ss_ticket_id = {id}. If found, skip creation or issue an update. This prevents duplicates if the script crashes mid-run and is restarted.

Tip

Preserve four values on every migrated issue: the original SurveySparrow ticket ID, the original source type (Survey, Email, Call), the legacy requester email, and the legacy ticket URL. These four fields make post-cutover validation and support handoffs substantially faster.

Step 7: Handle Attachments

SurveySparrow ticket attachments must be downloaded and re-uploaded to Pylon. Pylon's Attachments API handles file uploads separately from issue creation.

The workflow:

  1. Extract attachment URLs from SurveySparrow ticket and comment payloads during the extraction phase.
  2. Download immediately — SurveySparrow attachment URLs are signed and time-limited. Do not defer download to the load phase.
  3. Validate file size — SurveySparrow enforces a 15 MB max upload size per file. Files above this limit were accepted via SurveySparrow's importer at different thresholds; audit for oversized files before re-uploading.
  4. Upload to Pylon's Attachments endpoint and capture the returned Pylon attachment ID.
  5. Reference the Pylon attachment ID when creating the corresponding message or issue body.

Alternative: S3 hosting. Download files to an S3 bucket you control. Generate long-lived signed URLs (1–5 year expiry). Replace original SurveySparrow attachment URLs in ticket bodies with the new S3 URLs. This approach is simpler to implement and gives you a permanent audit trail of original attachments independent of both platforms.

import boto3
import requests
 
def rehost_attachment(ss_url: str, bucket: str, key_prefix: str) -> str:
    """Download from SurveySparrow, upload to S3, return new URL."""
    s3 = boto3.client("s3")
    response = requests.get(ss_url, stream=True, timeout=30)
    response.raise_for_status()
    filename = ss_url.split("/")[-1].split("?")[0]
    s3_key = f"{key_prefix}/{filename}"
    s3.upload_fileobj(response.raw, bucket, s3_key,
                      ExtraArgs={"ContentType": response.headers.get("Content-Type",
                                                                      "application/octet-stream")})
    return f"https://{bucket}.s3.amazonaws.com/{s3_key}"

Step 8: Delta Sync and Cutover

A static migration is rarely sufficient for an active support team. You need a delta sync strategy to capture tickets created or updated in SurveySparrow during the migration window.

  1. Historical sync: Migrate all closed tickets and historical data first. Agents continue working in SurveySparrow.
  2. Delta sync: Build a secondary script that queries SurveySparrow for tickets where updated_at > last_sync_timestamp. Run this hourly during the migration window. Store last_sync_timestamp durably so a script restart doesn't lose track of position.
  3. Final cutover: Pause incoming channels to SurveySparrow. Run the last delta sync. Update DNS records, email forwarding rules, and channel routing to point to Pylon. Agents log into Pylon the next morning.

SurveySparrow webhook strategy. SurveySparrow's REST webhook object is survey-scoped and requires a survey_id. Ticket Management workflows expose webhook actions that send ticket ID, contact ID, and source details — this is the more useful mechanism for live delta capture. Configure a ticket workflow webhook to fire on ticket creation and update, and use it to feed your delta sync queue rather than polling the REST API hourly.

For a broader checklist on keeping support running during migration, see Zero-Downtime Help Desk Data Migration.

Edge Cases and Failure Modes

Status mapping. SurveySparrow allows custom statuses beyond the defaults. Pylon accepts built-in states (new, waiting_on_you, waiting_on_customer, on_hold, closed) plus custom status slugs. Decide early which legacy statuses collapse into a smaller target model. A many-to-one mapping (e.g., SurveySparrow's pending-review and pending-approval both become Pylon's on_hold) must be documented and agreed upon with stakeholders before migration starts.

Inactive agents. If an agent has left the company and does not exist in Pylon, the migration script will fail on the assignment field. Create a "Legacy Agent" or "System User" in Pylon and map all inactive agents to this fallback. Store the original agent's name in a custom field or internal note (e.g., [Originally assigned to: John Doe]).

Related ticket hierarchies. SurveySparrow parent tickets cannot close until all child tickets are closed. Pylon's public API does not expose a direct parent-child issue relationship. Preserve these relationships in a crosswalk table and surface them with tags or custom fields (e.g., ss_parent_ticket_id).

Survey-to-ticket linkage. The bidirectional link between a SurveySparrow ticket and its originating survey response has no Pylon equivalent. Store the original survey ID and response ID in a Pylon custom field if this context matters for your team.

Source-specific analytics. SurveySparrow tickets can be filtered using survey, contact, and rating/review properties. If you don't materialize that context into Pylon fields before cutover, you lose it permanently — the ticket body will contain the content, but you can't filter or report on it.

The 30-day window on GET /issues. Pylon's GET /issues endpoint returns a paginated list of issues within a required time range with a maximum window of 30 days. This directly impacts post-migration verification: you cannot pull all migrated issues in a single query. Structure your verification script to iterate in 30-day windows from the earliest created_at in your dataset. Count issues per window and compare against your SurveySparrow source counts.

Portal role drift. Pylon portal roles (no_access, member, admin, custom) control which ticket history contacts can see. Bulk-importing contacts as member without validating access scope is a common mistake that exposes one customer's ticket history to another's portal user.

Character encoding. SurveySparrow survey responses may arrive in mixed encodings if data was collected from legacy integrations. Normalize all field values to UTF-8 explicitly during the transformation step — do not rely on implicit encoding handling in your HTTP client.

What Cannot Be Migrated

Be explicit with stakeholders before migration begins:

  • Survey response linkages — no Pylon equivalent; preserve IDs as custom fields
  • SLA configurations — must be recreated in Pylon manually
  • Workflow automations — SurveySparrow workflows don't export; recreate as Pylon triggers
  • Ticket templates — recreate as Pylon Ticket Forms
  • Embedded survey widgets — no equivalent in Pylon
  • Reporting dashboards — rebuild in Pylon's analytics or export data to your data warehouse
  • Related ticket parent-child enforcement logic — metadata only; the closure dependency rules cannot be replicated
  • Per-ticket SLA breach history — historical SLA performance data stays in SurveySparrow

Pre-Migration Checklist

  • Audit SurveySparrow ticket count, comment volume, and total attachment size
  • Confirm SurveySparrow data center and correct API base URL
  • Confirm SurveySparrow API rate limit for your plan tier (contact support if unclear)
  • Generate SurveySparrow OAuth token with ticket read scopes
  • Generate Pylon API token (Admin role required)
  • Verify Pylon API rate limits against current documentation before building queue logic
  • Map all SurveySparrow custom fields to Pylon field types; document type mismatches
  • Map all SurveySparrow statuses to Pylon issue states; get stakeholder sign-off on the mapping
  • Map all inactive SurveySparrow agents to a Pylon fallback user
  • Decide and document Account derivation strategy (domain-based, CRM-based, or catch-all)
  • Confirm with Pylon whether POST /issues accepts created_at for historical timestamps
  • Confirm whether backdated message creation is supported on POST /issues/:id/messages
  • Create Accounts, custom fields, and teams in Pylon before loading issues
  • Confirm Pylon's ss_ticket_id custom field exists for idempotency checks
  • Download all SurveySparrow attachments during extraction (signed URLs expire)
  • Test full pipeline with 10–20 representative tickets before running the full migration
  • Plan maintenance window for cutover; notify customers of expected downtime
  • Prepare post-migration verification queries: ticket counts, comment counts, field spot-checks, 30-day window iteration

When to Self-Serve vs. Use a Managed Migration

Self-serve is viable if:

  • Fewer than 5,000 tickets with low comment density (under 3 per ticket on average)
  • Custom fields are limited to text and single-level dropdown
  • Attachments are minimal in count and size
  • Engineering bandwidth available for 1–2 weeks of focused work
  • You have capacity to validate output rigorously after load

A managed service is worth considering when:

  • Volume exceeds 5,000 tickets with substantial comment threads
  • Complex custom field mappings including nested dropdowns requiring flattening
  • Attachments are numerous or large, requiring re-hosting infrastructure
  • Accuracy and auditability are contractually required (e.g., regulated industries)
  • The migration must complete within a fixed window shorter than internal development would allow

Frequently Asked Questions

Can I import SurveySparrow tickets into Pylon with a CSV?
No. Pylon does not offer a CSV ticket import. SurveySparrow's CSV and Excel exports are useful for auditing but cannot be directly loaded into Pylon. You must use the SurveySparrow REST API for extraction and Pylon's REST API for loading.
How long does a SurveySparrow to Pylon migration take?
For a mid-size dataset (5K–10K tickets with comments), expect 5–12 business days end-to-end. The Pylon API rate limit of 10 issue creations per minute is the primary bottleneck — 5,000 tickets alone take over 8 hours of API time. A managed migration service can compress this with optimized pipelines.
What SurveySparrow data cannot be migrated to Pylon?
Survey response linkages, SLA configurations, workflow automations, ticket templates, embedded survey widgets, reporting dashboards, and related ticket parent-child enforcement rules do not transfer. These must be recreated manually in Pylon.
How do I handle SurveySparrow contacts without an Account in Pylon?
Pylon requires every Issue to belong to an Account. The most common approach is deriving Accounts from contact email domains (e.g., john@acme.com → Acme Corp). Alternatively, import Accounts from your CRM or create a catch-all Account for unattributed contacts.
Can I preserve SurveySparrow comment threads in Pylon?
SurveySparrow exposes comment bodies, visibility, timestamps, and attachment URLs via the API. Confirm with Pylon whether their API supports creating backdated messages on issues. If it does not, import the conversation history as a structured HTML timeline inside the Pylon issue body, grouped by visibility and timestamp.

More from our Blog