Skip to content

Helpshift to Plain Migration: The Complete Technical Guide

A technical guide to migrating from Helpshift to Plain. Covers data model mapping, REST extraction, GraphQL import mutations, rate limits, and edge cases.

Wahab Wahab · · 22 min read
Helpshift to Plain Migration: The Complete 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

Helpshift to Plain Migration: The Complete Technical Guide

Info

TL;DR: Migrating from Helpshift to Plain means moving from a mobile-first, REST-based platform to a B2B, GraphQL-native customer support infrastructure. There is no native import path. You must extract data via Helpshift's REST API, transform the data model (Issues → Threads, Profiles → Customers, CIFs → Thread Fields, 7 states → 3), and load via Plain's importThread and importThreadMessages GraphQL mutations. The hard parts: cursor pagination past Helpshift's 50K row ceiling, attachment re-hosting, HTML sanitization, bot transcript aggregation, and staying under Plain's rate limits.

Helpshift is a mobile-first customer service platform built for apps and games. Plain is an API-first, GraphQL-native support platform designed for B2B SaaS engineering teams. Moving between them is not a lift-and-shift — it is a data model transformation.

Plain documents built-in importers for Zendesk, Freshdesk, Intercom, Front, and Help Scout (help.plain.com). Helpshift is not on that list. Every record — issues, users, messages, tags, custom fields, attachments — must be extracted via Helpshift's REST API, structurally transformed, and loaded through Plain's GraphQL import mutations.

This guide covers the full technical architecture: data model mapping, API constraints on both sides, extraction mechanics, loading sequence, edge cases, error recovery, and realistic timelines. All API behaviors are verified against Helpshift REST API v1 and Plain's GraphQL API as documented in their respective developer references.

Disclosure: This guide is published by ClonePartner, a company that offers managed migration services. We have a commercial interest in one of the migration approaches described below. We've aimed to provide technically accurate guidance regardless of which approach you choose.

Helpshift vs. Plain: Architecture Differences That Matter

Before writing any migration code, understand the structural gap you are bridging.

Helpshift centers support around Issues and app or player context. Its core model revolves around Issues (tickets), End Users (profiles created via SDK or API), Apps (each mobile application gets its own context), Agents, Queues, Tags, and Custom Issue Fields (CIFs). Helpshift captures rich device metadata automatically via its SDK — OS version, device model, battery level, carrier, app version — and supports Custom Data key-value pairs attached at the SDK level. Its newer User Hub model unifies identities across mobile, web, PC, and console. (support.helpshift.com)

Plain centers support around Threads, Customers, Companies, Tenants, Labels, and Thread Fields. Its GraphQL API is the primary interface — everything you do in the UI, you can do via the API. Thread statuses are TODO, SNOOZED, or DONE, each with a statusDetail sub-type. Threads have a timeline containing inbound messages, outbound replies, notes, and events. A customer can belong to one company and multiple tenants. (plain.mintlify.app)

Dimension Helpshift Plain
API type REST v1 GraphQL
Core unit Issue Thread
User model End User Profile (app-scoped) Customer (workspace-scoped)
Organization Apps Companies + Tenants
Custom fields CIFs (text, number, date, checkbox, dropdown) Thread Fields (string, boolean, number, datetime, enum)
Device metadata Native SDK collection (OS, device, battery, carrier) No native equivalent
Knowledge base Built-in FAQ sections and articles No built-in KB
Message origin end-user / helpshift INBOUND / OUTBOUND / NOTE
States 7 distinct states 3 states with sub-types (TODO, SNOOZED, DONE)
Routing Queues (agent groups) Labels + assignment rules
Bot support QuickSearch Bots, Custom Bots (native) External integration only

Context: When This Migration Makes Sense

Helpshift is optimized for high-volume mobile consumer support — gaming studios, ride-sharing apps, fintech apps with millions of end users filing issues through in-app SDKs. Plain is built for B2B SaaS teams that want deep API programmability, GraphQL flexibility, and multi-tenant account structures.

The migration is appropriate when your company is shifting from consumer-to-business support and needs: (1) per-tenant account visibility across a business customer's organization, (2) a fully programmable GraphQL API for custom AI agents and internal tooling, or (3) a support model that does not depend on mobile SDK instrumentation.

Warning

If your business still depends on Helpshift's gaming-native support delivery — in-game SDK UI, cross-platform player identity, console-to-mobile handoff, or proactive engagement tied to player lifecycle events — this is not a like-for-like swap. Evaluate that architectural trade-off before any ETL work starts. (helpshift.com)

Data Model Mapping: Helpshift → Plain

Issues → Threads

Each Helpshift Issue becomes a Plain Thread. Use the Helpshift issue ID as Plain's externalId — both importThread and importThreadMessages are idempotent on externalId, so re-runs are safe.

The Issue title maps to the Thread title. The created_at timestamp (Unix milliseconds in Helpshift) must be converted to ISO 8601 for Plain's createdAt.

Status mapping is where most migrations go subtly wrong. Helpshift has 7 distinct issue states that collapse into Plain's 3-state model with sub-types:

Helpshift State Plain Status Plain StatusDetail
new TODO NEW_REPLY
new-for-agent TODO NEW_REPLY
pending-reassignment TODO NEW_REPLY
waiting-for-agent TODO IN_PROGRESS
agent-replied SNOOZED WAITING_FOR_CUSTOMER
resolved DONE DONE_MANUALLY_SET
rejected DONE IGNORED

This mapping is a policy choice, not a hard rule. Using SNOOZED for agent-replied works if you want waiting-on-customer to act as an explicit parked state. If you want all unresolved historical issues to land in active work queues, map more of them to TODO instead.

Warning

If your team relies on distinguishing between new, new-for-agent, and pending-reassignment, you lose that granularity in Plain. Store the original Helpshift state as a Thread Field or label for audit purposes.

End Users → Customers

Helpshift End User Profiles become Plain Customers. Use Plain's upsertCustomer mutation, mapping the Helpshift profile id to Plain's externalId for deduplication.

Helpshift end users can be scoped to specific apps — a single person may have different profiles across different Helpshift apps. In Plain, customers are workspace-scoped. Match on email address when possible, and use the Helpshift profile ID as the externalId for traceability.

In practice, anonymous mobile support accounts (those without an email address) account for a significant share of Helpshift profiles in gaming and consumer apps — teams migrating from these verticals commonly find 20–40% of their End User records lack email addresses. Decide before extraction whether to import these as anonymous customers (using a generated identifier) or exclude them. Do not defer that decision to the load stage.

Info

If email is unavailable, you will have orphaned profiles. Decide early whether to import these as anonymous customers or skip them — do not wait until the load stage to make that policy.

Apps → Tenants or Labels

Helpshift Apps represent distinct mobile applications (e.g., iOS app, Android app, web chat). Plain has no direct equivalent. Two mapping options:

  1. Tenants — if each Helpshift app represents a distinct product line or customer segment. Use tenants only for entities that are genuinely account- or workspace-like.
  2. Labels — if apps are channel distinctions (iOS vs. Android). Simpler for most migrations.

For most teams, Labels are the right default. Reserve Tenants for actual organizational boundaries. Plain explicitly requires that a customer must be a member of a tenant before you can import a thread referencing that tenantId — imports that violate this fail with an authorization or reference error. (plain.com)

Tags → Labels

Helpshift Tags map cleanly to Plain Labels. This is a 1:1 mapping. Create all Helpshift tags as Plain labels before importing threads, then reference them by key during the importThread call.

Custom Issue Fields → Thread Fields

Helpshift CIF types and their Plain targets:

Helpshift CIF Type Plain Thread Field Type Notes
Single-line text STRING Direct mapping
Multiline text STRING No multiline-specific type in Plain
Number NUMBER Direct mapping
Date DATETIME Serialize as ISO 8601
Checkbox BOOL Direct mapping
Dropdown ENUM Pre-create enum values in the Thread Field schema

Pre-creating enum Thread Fields: For dropdown CIFs, you must create the Thread Field schema including all enum option values before import. The mutation to create an enum Thread Field with options looks like this:

mutation CreateDropdownThreadField {
  createThreadField(input: {
    key: "issue_category"
    label: "Issue Category"
    type: ENUM
    isRequired: false
    enumValues: [
      { label: "Billing", value: "billing" }
      { label: "Technical", value: "technical" }
      { label: "Account", value: "account" }
    ]
  }) {
    threadField { id key }
    error { message }
  }
}

If an enum value referenced during import does not exist in the schema, Plain will reject the thread import with a validation error. Collect all distinct dropdown values from your CIF export before running the schema creation step.

Warning

Plain Thread Fields must be configured in Settings → Thread fields before import. If a Thread Field is marked required, Plain enforces it before a thread can be marked done. For migrations, either disable required-field enforcement during import or ensure every closed thread has that field populated in your source data. Historical DONE imports will fail for threads where required CIF data is absent — this is one of the most common causes of partial import failure. (plain.mintlify.app)

Helpshift allows up to 250 active and archived CIFs and up to 1,000 options in a dropdown CIF. Migrate the fields agents still use, the fields needed for reporting, and the fields needed for routing in Plain. Archive the rest in the raw export. (support.helpshift.com)

Metadata and Custom Data

Helpshift's SDK-collected metadata (device model, OS version, battery level, carrier, app version) and Custom Data (developer-defined key-value pairs) have no native equivalent in Plain. Three options:

  1. Serialize into a note — Add an internal NOTE message to each imported thread containing the metadata as formatted text. Agents can see it but cannot filter on it.
  2. Map critical fields to Thread Fields — If you frequently filter by app_version or platform, create those as Thread Fields. Works for a handful of fields but does not scale to all metadata.
  3. Surface via Customer Cards — Keep fast-changing or high-cardinality context in your own system and surface it in Plain through Customer Cards, avoiding over-modeling in imported history.

Option 1 for archival completeness, combined with option 2 or 3 for fields your team actively uses, is the practical approach.

FAQs → External System

Helpshift has a built-in FAQ system with sections and articles. Plain does not include a knowledge base. Export FAQ content via Helpshift's GET /faqs and GET /faq-sections endpoints and migrate it to a separate system — a docs site, Notion, or a dedicated KB tool.

Queues → Labels and Assignment Rules

Helpshift Queues (agent groups for routing) have no direct Plain equivalent. Map queue names to Plain Labels and use Plain's assignment rules or workflow automation to replicate routing logic.

CSAT Feedback

Helpshift captures feedback_rating (1–5) and feedback_comment on resolved issues. Plain has no native CSAT field. Serialize into Thread Fields (feedback_score as NUMBER, feedback_comment as STRING) or append as a note on the thread.

Extraction: Getting Data Out of Helpshift

Helpshift gives you several export paths. No single one covers everything.

REST API Extraction

The primary path for full issue history with messages is the REST API: https://api.helpshift.com/v1/{domain}/issues.

Authentication: HTTP Basic Auth with the API key as the username and an empty password. Find your API key under Settings → APIs in the Helpshift dashboard.

Pagination constraints are the first trap. The default page size is 100 issues, maximum is 1,000. But page × page-size must be ≤ 50,000 — you cannot paginate beyond 50,000 issues in a single query, even if your dataset is larger. For datasets exceeding 50K issues, use cursor-based pagination by filtering on created_since and created_until timestamps.

# Helpshift cursor-based extraction for large datasets
import requests
import time
import datetime
import json
import os
 
CHECKPOINT_FILE = "extraction_checkpoint.json"
 
def get_timestamp_ms(date_str):
    return str(int(time.mktime(
        datetime.datetime.strptime(date_str, "%d/%m/%Y").timetuple()
    ) * 1e3))
 
def load_checkpoint():
    if os.path.exists(CHECKPOINT_FILE):
        with open(CHECKPOINT_FILE) as f:
            return json.load(f)
    return {"last_created_at": None, "issues_processed": 0}
 
def save_checkpoint(last_created_at, issues_processed):
    with open(CHECKPOINT_FILE, "w") as f:
        json.dump({"last_created_at": last_created_at, "issues_processed": issues_processed}, f)
 
checkpoint = load_checkpoint()
start_ts = checkpoint["last_created_at"] or get_timestamp_ms("01/01/2020")
end_ts = get_timestamp_ms("01/01/2025")
issues_processed = checkpoint["issues_processed"]
 
while True:
    r = requests.get(
        f'https://api.helpshift.com/v1/{domain}/issues'
        f'?sort-by=creation-time&sort-order=asc'
        f'&created_since={start_ts}&created_until={end_ts}'
        f'&page-size=1000',
        auth=(api_key, '')
    )
    r.raise_for_status()
    data = r.json()
    issues = data['issues']
 
    if not issues:
        break
 
    if len(issues) == 1:
        process_issues(issues)
        issues_processed += 1
        save_checkpoint(str(issues[-1]['created_at']), issues_processed)
        break
 
    # Process all but last (last becomes cursor for next query)
    process_issues(issues[:-1])
    issues_processed += len(issues) - 1
    save_checkpoint(str(issues[-1]['created_at']), issues_processed)
    start_ts = str(issues[-1]['created_at'])

The checkpoint file pattern above is critical for large datasets. If extraction fails at 80,000 of 200,000 issues, you resume from the last saved timestamp rather than re-querying from the beginning.

Tip

Sort from oldest to newest (sort-order=asc) when paginating. New issues added during extraction only affect the last pages, minimizing duplicate risk.

To include metadata and custom fields, add the includes parameter:

GET /issues?includes=["meta","custom_fields","feedback"]

Use GET /agents to pull the agent list — you need this to map Helpshift agent IDs to Plain User IDs for outbound messages. Use GET /apps to retrieve all registered apps and their IDs.

Data Portability API

For full-fidelity data including messages and attachments, Helpshift offers a data portability API. The POST /hs-data flow identifies records by user_id, hs_user_id, issue_id, or email, returning the user's profile plus associated issues, messages, attachments, and Custom Issue Fields. The catch: portability requests are processed weekly, and results are delivered as time-sensitive download links. Excellent for completeness, poor for tight cutover windows. (support.helpshift.com)

Attachment URL Expiry

Helpshift attachment URLs are signed and time-limited. In migrations from accounts with significant history, attachment URLs on issues older than approximately 12–18 months are frequently already expired at extraction time — the exact TTL depends on your Helpshift plan and storage configuration. When downloading attachments, treat any HTTP 403 or 404 response on an attachment URL as an expected condition, log it, and continue rather than failing the extraction job.

Issuewise Action Log API

When you need lifecycle detail beyond the issue snapshot — reassignments, resolve/reopen behavior, tag changes — use the Issuewise Action Log API. It exposes issue action history with actor, timestamps, and before/after data. Paginated with a page_size between 1 and 500 (default 200). (support.helpshift.com)

CSV Export

The dashboard CSV export is useful for filtered issue-level extraction but caps at 10,000 issues per export and lacks full message histories, inline attachments, and granular metadata. Treat it as a supplementary validation tool, not your primary extraction method. (support.helpshift.com)

Archived Issues

Helpshift automatically archives issues in Resolved or Rejected state after 12 months. Archived issues may still be accessible via the API, but teams often undercount history because they validate exports only against active queues. Confirm archived data is included in your API responses before starting extraction. (support.helpshift.com)

A practical export plan: Use the REST API for bulk issue extraction with messages, the portability API when you need full attachment fidelity, the action log API for slices where audit history matters, and CSV for quick validation. For more detail on Helpshift exports, see our guide on exporting data from Helpshift.

Write raw JSON payloads to intermediate files or a local database. Separating extraction from loading ensures that if your script fails midway, you do not have to re-query the Helpshift API from scratch.

Loading: Getting Data Into Plain

Plain provides dedicated import mutations designed specifically for migrations — a significant advantage over platforms that force you to use their regular CRUD endpoints.

Import API Overview

Plain's import flow is a two-step process per thread:

  1. importThread — Creates the thread with metadata (title, status, priority, labels, thread fields) and the original creation timestamp.
  2. importThreadMessages — Adds conversation history (inbound, outbound, and note messages) in batches of up to 25 messages per call.

Both mutations are idempotent on externalId. Call them again with the same external ID and duplicates are skipped (result: NOOP). You can safely re-run your migration script without creating duplicate records. This idempotency is the foundation of the resume-from-failure pattern described below.

Info

Imported threads do not trigger SLAs or autoresponders and are marked with import provenance tracking. No false alerts, no auto-replies to customers during migration. (plain.mintlify.app)

Plain Workspace Pre-Configuration

Before running any import mutations, configure the Plain workspace:

  • API token scopes required: Ensure your Plain API token has thread:create, customer:create, thread-field:read, label:read, and attachment:create permissions. Tokens with insufficient scope fail silently on some mutations or return permission errors that are easy to misattribute to data issues.
  • Deactivate webhooks: If you have outbound webhooks configured (e.g., to Slack or PagerDuty), deactivate them before import. Importing 50,000 threads will fire 50,000 thread.created events, which will flood downstream systems.
  • Disable SLA rules: Confirm SLA enforcement is disabled for the import period. Even though import mutations suppress autoresponders, SLA breach timers on historical threads can generate internal alerts.

Step-by-Step Loading Sequence

Step 1: Pre-create the schema

Before any data import:

  • Create all Labels (one per Helpshift tag, plus one per app if using label-based mapping)
  • Create Thread Field schemas (for each CIF you are migrating, including all enum values for dropdown types)
  • Create Tenants (if using tenant-based app mapping)
  • Invite or create Users (agents) in Plain and record their Plain user IDs mapped to Helpshift agent IDs

Step 2: Upsert customers

For each unique end user, call upsertCustomer:

mutation {
  upsertCustomer(input: {
    identifier: { emailAddress: "user@example.com" }
    onCreate: {
      fullName: "Jane Doe"
      externalId: "helpshift_profile_abc123"
      email: { email: "user@example.com", isVerified: true }
    }
    onUpdate: {
      externalId: { value: "helpshift_profile_abc123" }
    }
  }) {
    customer { id }
    error { message type }
  }
}

Store the returned Plain customer ID mapped to the Helpshift profile ID. If using tenants, create tenant memberships before importing threads — Plain rejects a thread import with tenantId if the customer is not already a member. The error returned in this case is a validation error on the tenantId field referencing a missing membership relationship.

Step 3: Import threads

For each Helpshift issue, call importThread:

mutation {
  importThread(input: {
    externalId: "helpshift_issue_12345"
    customerIdentifier: { emailAddress: "user@example.com" }
    title: "Coupon Code Issue"
    status: DONE
    statusDetail: { type: DONE_MANUALLY_SET }
    createdAt: "2024-06-15T10:30:00Z"
    labelTypeIds: ["lt_tag_billing", "lt_app_ios"]
  }) {
    result
    thread { id }
    error { message type }
  }
}

Step 4: Import messages

For each thread, call importThreadMessages in batches of 25:

mutation {
  importThreadMessages(input: {
    threadId: "th_01ABC..."
    messages: [
      {
        externalId: "helpshift_msg_001"
        type: INBOUND
        text: "What happened to my coupon code?"
        createdAt: "2024-06-15T10:30:00Z"
        author: { customerId: "c_01XYZ..." }
      },
      {
        externalId: "helpshift_msg_002"
        type: OUTBOUND
        text: "Let me look into that for you."
        createdAt: "2024-06-15T10:45:00Z"
        author: { userId: "u_agent_01..." }
      }
    ]
  }) {
    results { result error { message type } }
  }
}

Step 5: Handle attachments

For messages with attachments:

  1. Download the binary file from the Helpshift URL. If the URL returns 403/404, log and skip — do not abort the message import.
  2. Call Plain's createAttachmentUploadUrl to generate a secure upload destination. Upload URLs expire after 2 hours. (plain.com)
  3. PUT the binary file to the provided URL.
  4. Reference the resulting attachment ID in the message import.
mutation {
  createAttachmentUploadUrl(input: {
    fileName: "screenshot.png"
    fileSizeBytes: 204800
    attachmentType: THREAD_MESSAGE_ATTACHMENT
  }) {
    attachmentUploadUrl {
      attachmentId
      uploadFormUrl
      uploadFormFields { name value }
    }
    error { message type }
  }
}
Warning

Attachments uploaded but not referenced by any message are deleted after 24 hours. Run attachment uploads and message imports in tight sequence. Helpshift attachment URLs are signed and time-limited — copying the URL string into a Plain message without re-hosting will break within days. For older issues (12–18+ months), treat expired Helpshift attachment URLs as an expected condition, not an error.

Process attachments in a separate async queue to avoid blocking the main migration loop, but ensure the upload-to-reference gap stays within the 24-hour window.

Step 6: Add metadata as notes

For issues with rich Helpshift metadata, add a NOTE type message to each thread:

[Helpshift Metadata]
Device: ONEPLUS A5000
OS: Android 7.1.1
App Version: 1.0
Battery: 57%
Carrier: Jio 4G
Original Issue ID: 5696
Original State: agent-replied

Including the original Helpshift state in the metadata note preserves audit context for the state granularity that Plain's 3-state model cannot represent.

Rate Limits

Plain's API rate limits vary by plan: 450, 600, or 1,000 requests per minute per workspace. Response headers (x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset) tell you exactly where you stand. When x-ratelimit-remaining reaches 0, back off until x-ratelimit-reset.

For a dataset of 10,000 issues averaging 5 messages each:

  • 10,000 customer upserts
  • 10,000 importThread calls
  • ~2,000 importThreadMessages calls (at 25 messages per batch)
  • ~500 attachment uploads (estimated)
  • Total: ~22,500 API calls
  • At 450 requests/minute: ~50 minutes of pure API time

For 50,000+ issues, expect 4–8 hours of API runtime at the 450 rpm tier. Build retry logic and exponential backoff into your pipeline.

Error Recovery and Resume Strategy

Because both importThread and importThreadMessages are idempotent on externalId, the resume pattern is straightforward:

  1. Maintain a local checkpoint file (or database table) tracking which externalId values have been successfully imported.
  2. Before each importThread call, check the local checkpoint. If already present, skip.
  3. On failure (network error, rate limit exhaustion, validation error), log the failing externalId and the error payload to a separate failures log.
  4. After the main import run, replay the failures log as a secondary pass. Persistent failures indicate data transformation issues, not transient errors.

A thread import that fails due to a missing tenant membership (tenantId reference with no prior customer membership) is a data ordering error — fix by ensuring all upsertCustomer + tenant membership calls complete before the importThread pass. A thread import that fails due to a missing required Thread Field is a schema gap — fix by backfilling the field in your transformation layer and re-running (the idempotent importThread will update the existing thread rather than create a duplicate).

Rollback: Plain does not provide a bulk delete API for imported threads. If you need to fully roll back a partial import, you must delete threads individually via the deleteThread mutation or contact Plain support for workspace-level import reversal. This makes pre-production validation against a staging workspace critical before running against production.

Handling the Cutover Window

The migration extraction has a cutoff timestamp. Issues created in Helpshift after that timestamp will not be in your import dataset. Two approaches:

Option A: Freeze Helpshift intake. For a defined cutover window (typically a weekend), redirect new support requests to Plain directly and let the final Helpshift extraction complete. Any issues filed in Helpshift during the freeze period are manually transferred.

Option B: Delta extraction. After the main import completes, run a second extraction filtered to created_since={main_extraction_cutoff} and import the delta. Because importThread is idempotent, any overlap between the main and delta extractions is handled automatically (overlapping records result in NOOP).

For most teams, Option B is lower-risk. The delta window is typically 3–5 days for a dataset under 50K issues.

Webhook handling during migration: If Helpshift webhooks are active and feeding downstream systems (Slack channels, internal dashboards, CRMs), document which webhooks should be deactivated before cutover and which should be re-pointed to Plain's equivalent events post-migration. Failing to do this results in either duplicate notifications (old Helpshift events + new Plain events) or dropped notifications.

Edge Cases and Failure Modes

HTML Message Bodies

Helpshift message bodies can contain HTML formatting (<strong>, <br/>, etc.) and sometimes proprietary markup. Plain's import accepts plain text in the text field. Strip HTML tags or convert to Markdown before import. Raw HTML renders poorly in Plain's UI, and unsupported markup may cause validation errors during ingestion.

Use a library like bleach (Python) or sanitize-html (Node.js) rather than regex-based stripping. Regex HTML parsing breaks on nested tags and encoded entities, which are common in Helpshift message bodies generated by rich-text editors.

Bot Transcript Noise

Helpshift deployments often use QuickSearch Bots and Custom Bots that generate dozens of micro-interactions ("Did this article help?", "Yes", "No", "Connecting you to an agent"). Plain is designed for high-signal communication.

Best practice: filter bot messages during transformation. Concatenate the bot transcript into a single block and inject it as one NOTE at the beginning of the thread. This preserves context without polluting the timeline.

Identifying bot messages: In Helpshift's API response, bot messages typically have an author.type of bot or carry a specific author.id matching registered bot configurations. Cross-reference against your GET /apps bot configuration data during the transformation stage.

Message Author Detection

Helpshift does not expose a clean origin field on every message in the REST API response. Determine whether a message is from an end user or an agent by cross-referencing the message author.id against your extracted agent list. Messages from automations (system bots) should be imported as NOTE messages or as OUTBOUND with a designated system user.

If preserving per-agent authorship matters, map historical agent identities to Plain users before import. If historical agents no longer have Plain accounts (common after team turnover), create a synthetic "Historical Agent" Plain user and attribute their messages to it, noting the original agent identity in the message text or a thread note.

Duplicate End Users Across Apps

A single person may have multiple Helpshift profiles across different apps. When upserting into Plain, match on email address. If email is unavailable, you will have orphaned profiles — use a generated external ID (e.g., helpshift_anon_{hs_user_id}) to keep them traceable without blocking the import.

The 50,000 Row Pagination Ceiling

page × page-size cannot exceed 50,000 in Helpshift's API. If you have 200,000 issues, standard page-number pagination fails. Use the cursor-based approach with created_since as a sliding window. This is the most common extraction failure on Helpshift migrations. The checkpoint-based extraction script above handles this correctly.

Required Thread Fields During Import

Plain Thread Fields marked as required must be set before a thread can be marked done. For historical imports, DONE thread imports fail if the corresponding CIF data is missing from the source. Turn off required-field enforcement during migration or backfill every closed thread before enabling it.

Migration Approaches

Custom Script (Full DIY)

Build a Python or TypeScript ETL pipeline that extracts from Helpshift's REST API, transforms the data model, and loads via Plain's GraphQL import mutations.

Best for: Engineering teams with API experience and fewer than 20K issues. Budget 80–120 engineer-hours for a mid-size dataset, not including edge case debugging and validation.

Risk: Underestimating edge cases — HTML bodies, attachment handling, cursor pagination, author detection, required field enforcement, cutover delta extraction.

Plain's Built-in Importers

Plain has built-in importers for Zendesk, Freshdesk, Intercom, Front, and Help Scout. As of this writing, Helpshift is not on that list. You will need the custom import mutations described in this guide.

Managed Migration Service

Hire a team that specializes in helpdesk data migrations. ClonePartner handles the cursor pagination, HTML sanitization, author mapping, attachment pipeline, cutover delta extraction, and validation so your engineering team stays focused on product work.

Selective Migration (Active Issues Only)

Only migrate open and active issues. Keep everything else in Helpshift with read-only access. This dramatically reduces scope and risk. Filter by state during Helpshift extraction. Works best when historical ticket access is a nice-to-have, not a compliance requirement.

Pre-Migration Checklist

Helpshift side:

  • Confirm API key is active with read access to Issues, Agents, Apps, FAQs
  • Count total issues including archived to estimate extraction and load time
  • Inventory all Custom Issue Fields, types, and dropdown values
  • Inventory all Tags
  • Identify bot configurations to filter during transformation
  • Confirm attachment URL accessibility for older issues (spot-check 12–18 month-old issues)
  • Plan FAQ migration to external system

Plain side:

  • Create Plain workspace and invite all agents; record agent ID mapping
  • Configure API token with required scopes (thread:create, customer:create, thread-field:read, label:read, attachment:create)
  • Deactivate outbound webhooks before import
  • Disable SLA rules for import period
  • Create all Labels (one per Helpshift tag, one per app if using label-based app mapping)
  • Create Thread Field schemas including all enum values for dropdown CIFs
  • Create Tenants if using tenant-based app mapping
  • Confirm required Thread Fields are set to non-required for migration period

Process:

  • Decide on App → Tenant vs. App → Label mapping
  • Decide on metadata handling (notes, Thread Fields, Customer Cards)
  • Decide on anonymous end user handling
  • Build and test extraction script with checkpoint support against a small date range
  • Run full extraction to staging files; validate issue count against Helpshift dashboard
  • Run import against Plain staging workspace in batches with idempotent re-run capability
  • Post-migration validation: compare thread counts, verify attachment accessibility, spot-check conversation history
  • Plan cutover delta extraction window
  • Document which Helpshift webhooks to deactivate and which to re-point to Plain post-cutover

Realistic Timelines

Dataset Size Extraction Transformation Loading Total Elapsed
<5K issues 1–2 hours 4–8 hours 1–2 hours 1–2 weeks
5K–25K issues 2–6 hours 1–2 days 4–12 hours 2–3 weeks
25K–100K issues 6–24 hours 2–4 days 1–3 days 3–5 weeks
100K+ issues 1–3 days 3–5 days 3–7 days 5–8 weeks

These timelines include script development, testing, edge case handling, and validation — not just raw API runtime. The transformation phase dominates because it includes HTML sanitization, bot message aggregation, author identity resolution, and CIF-to-Thread-Field mapping decisions.

What Will Not Survive the Migration

Be explicit with stakeholders before migration starts:

  • Device metadata granularity: Battery levels, carrier info, and hardware details will not be queryable in Plain (preserved only as notes).
  • In-app messaging context: Helpshift's deep mobile SDK integration (screenshot requests, breadcrumbs, in-app conversation history) has no Plain equivalent.
  • FAQ content: Must move to a separate system. Plain has no built-in knowledge base.
  • CSAT as a first-class field: Becomes a custom Thread Field or note, not a native metric.
  • Queue-based routing: Must be rebuilt using Plain's labels and assignment automation.
  • Issue state granularity: 7 states collapse to 3 with sub-types. The new, new-for-agent, and pending-reassignment distinction is lost unless preserved as a Thread Field.
  • Attachment URLs: Helpshift signed attachment URLs embedded in any external system will break. Only re-hosted attachments (uploaded to Plain during migration) remain accessible.
  • User Hub identity graph: Not a direct field copy — requires a redesign exercise based on Plain's customer/tenant/company model.
  • Bot interaction history: Collapsed into note summaries; individual bot turn data is not preserved in the thread timeline.

If any of these are dealbreakers for your team or compliance requirements, evaluate the target platform choice before starting migration work.

Summary: The Migration Decision

The migration itself is not the risky part. The risky part is pretending Helpshift and Plain share a data model when they do not. Answer three questions before writing any ETL code:

  1. Is the user model compatible? Plain's workspace-scoped customers work for B2B; they are a structural mismatch for anonymous mobile users at scale.
  2. Which lost capabilities matter? Device metadata, in-app SDK features, and bot history loss are architectural, not fixable with custom fields.
  3. What is the compliance requirement for historical data? If you need 5 years of fully searchable ticket history in Plain, plan for the full migration. If read-only access is acceptable, selective migration (active issues only) is substantially simpler.

Get those answers right first, export more source data than you think you need, and only then write the loader.

For a related migration path, see our Zendesk to Plain migration guide.

Frequently Asked Questions

Does Plain have a native Helpshift importer?
Not as of this writing. Plain documents built-in importers for Zendesk, Freshdesk, Intercom, Front, and Help Scout. Helpshift requires custom API extraction via its REST API and loading through Plain's importThread and importThreadMessages GraphQL mutations.
How long does a Helpshift to Plain migration take?
For a mid-size dataset (5K–25K issues), expect 2–3 weeks elapsed time including script development, extraction, transformation, loading, and validation. Datasets over 100K issues can take 5–8 weeks.
What Helpshift data cannot be migrated to Plain?
Plain lacks native equivalents for Helpshift's device metadata (battery level, carrier, OS details), in-app FAQ system, CSAT rating fields, and 7-state issue lifecycle. Metadata can be serialized into notes, FAQs must move to an external system, and issue states collapse from 7 to 3 with sub-types.
Does Helpshift have an API export limit?
Yes. Helpshift's REST API enforces a page × page-size ceiling of 50,000. To export more than 50K issues, you must use cursor-based pagination with created_since timestamps as a sliding window rather than standard page-number pagination.
Does Plain's import API trigger automations on imported threads?
No. Plain's importThread and importThreadMessages mutations do not trigger SLAs or autoresponders. Imported threads are marked with import provenance tracking, so there is no risk of auto-replies reaching customers during migration.

More from our Blog