Deskpro to Help Scout Migration: A Technical Guide
A technical guide to migrating from Deskpro to Help Scout — covering API extraction, data model mapping, the 100-thread limit, custom field constraints, and cutover strategy.
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
Deskpro to Help Scout Migration: A Technical Guide
TL;DR — Deskpro to Help Scout Migration
A Deskpro to Help Scout migration translates Deskpro's department-centric ticket model into Help Scout's mailbox-centric, shared-inbox architecture. Tickets become Conversations, Messages become Threads, People become Customers, and Departments map to Mailboxes. There is no built-in Deskpro importer in Help Scout — you need a custom ETL pipeline using the Deskpro REST API v2 and the Help Scout Mailbox API v2. The hard constraints that shape the project: Help Scout caps conversations at 100 threads, custom fields at 10 per inbox (Plus/Pro plans only), and rate limits are plan-tiered (200–800 RPM, with write requests counting double). Deskpro's Stat Builder CSV export tops out at ~2,500 tickets per query and strips message bodies, making API extraction mandatory for full-fidelity migrations. Realistic timeline: 3–5 days for under 5K tickets; 3–5 weeks for 50K+ datasets with knowledge base content and complex custom fields.
What Is a Deskpro to Help Scout Migration?
If you need full ticket history, internal notes, attachments, customer records, and help-center content to remain usable after cutover, this is not a simple export/import job. It's an API-to-API ETL project.
Deskpro is a department-centric helpdesk platform. Every interaction is a Ticket organized under a Department, containing Messages (agent replies, customer messages, notes). Tickets carry custom fields, SLA tracking, labels, and are linked to People and Organizations. Deskpro's architecture follows traditional ITSM patterns with strict ticket states (awaiting_user, awaiting_agent, resolved).
Help Scout is an email-first customer support platform built around shared inboxes (Mailboxes). Every customer interaction is a Conversation containing Threads — customer messages, agent replies, internal notes, and forwarded messages. Help Scout intentionally hides ticket numbers from customers and presents support interactions as normal email conversations.
The first question: do your agents need searchable thread history with full message bodies after cutover? If yes — and they almost always do — CSV exports won't cut it. Deskpro's Stat Builder CSV export is limited to roughly 2,500 tickets per query and doesn't include full message bodies or attachments (support.deskpro.com). Deskpro's own documentation points larger exports to the API.
Help Scout separates inbox data from Docs content, so most migrations need one extraction pipeline from Deskpro and two load paths: the Mailbox API v2 for conversations, customers, and users, and the Docs API v1 for knowledge base articles.
Data Model Mapping: Deskpro → Help Scout
This is the core translation layer. Every migration decision flows from how these objects map.
| Deskpro Object | Help Scout Object | Notes |
|---|---|---|
| Department | Mailbox | 1:1 or many:1. Decide whether each department gets its own mailbox or collapses into shared mailboxes with tags. |
| Ticket | Conversation | Subject, status, timestamps, assignee carry over. |
| Message (reply) | Thread (reply) | Agent replies → reply threads. |
| Message (note) | Thread (note) | Internal notes → note threads. |
| Message (customer) | Thread (customer) | Customer messages → customer threads. |
| Person | Customer | Name, email, phone, company. |
| Organization | Organization | Help Scout supports organizations with custom properties. |
| Agent | User | Can be created via API (admin/owner only) with sendInvite: false for staging (developer.helpscout.com). |
| Agent Team | Team | Must be pre-created in Help Scout. |
| Label | Tag | Direct mapping. Tags in Help Scout are account-wide. |
| Custom Field | Custom Field | Lossy. Deskpro allows unlimited; Help Scout caps at 10 per inbox. |
| KB Article | Docs Article | Requires Docs API v1 (separate auth from Mailbox API). |
| KB Category | Docs Collection/Category | Deskpro's flat category model → Help Scout's Site > Collection > Category hierarchy. |
| Trigger / Automation | Workflow | No export path — must be rebuilt manually. |
| SLA | — | No direct equivalent in Help Scout. |
| News / Community / Files | — | No 1:1 target in Help Scout Docs (support.deskpro.com). Archive separately. |
The Custom Field Problem
This is where most Deskpro-to-Help-Scout migrations hit friction. Deskpro supports effectively unlimited custom fields on tickets. Help Scout enforces a hard limit: 10 custom fields per inbox, with only 5 field types (dropdown, single line, multi line, number, date). Dropdown fields cap at 100 options. Custom fields are only available on Plus and Pro plans — the Standard plan gets zero.
Separately, Help Scout supports up to 50 contact properties and 50 organization properties at the account level (docs.helpscout.com). These are different from conversation custom fields and can absorb some of your Deskpro person/org field data.
If your Deskpro instance uses more than 10 custom fields per department, you need a triage plan:
- Audit field usage. Export fill rates from Deskpro using DPQL:
SELECT tickets.custom_data [#], COUNT(*) FROM tickets GROUP BY 1. Fields with <5% fill rate are candidates for elimination. - Consolidate fields. Merge related fields into a single multi-line text field (e.g., combine "Product Version" and "Build Number" into "Version Info").
- Archive to tags. Convert low-cardinality fields into tags (e.g., a "Priority" dropdown with 3 values becomes
priority-low,priority-medium,priority-high). - Store in notes. For historical fields agents won't actively filter on, inject values into a note thread on the migrated conversation using the format
[Field: Value]for post-migration searchability.
Verify your plan tier before designing the field mapping. Help Scout custom fields are only available on Plus and Pro plans. If you're on Standard, you get zero conversation custom fields. Help Scout Free doesn't include API access at all.
Help Scout Platform Constraints
The main reason these projects go sideways is that teams discover target-side constraints after the first test load.
100 Threads Per Conversation
Help Scout enforces a hard cap of 100 threads per conversation. If you try to add a thread to a conversation that already has 99+ threads, the API returns HTTP 412 Precondition Failed (developer.helpscout.com).
This affects long-running support cases. If a Deskpro ticket has 150 messages (including notes, forwards, and phone logs), you have two options:
- Split into linked conversations. Create a second conversation with a subject prefix like
[Continued] Original Subjectand add a note linking the two conversation IDs. - Truncate older threads. Keep the most recent 95 messages and consolidate the oldest messages into a single summary note at the beginning.
Neither option is ideal. In practice, fewer than 1% of tickets in a typical Deskpro dataset exceed 100 messages — but those are disproportionately your most important customer relationships. Pre-calculate thread counts across your entire dataset before designing the load pipeline:
# Pre-flight: identify tickets that will exceed the 100-thread cap
over_cap = [(t['id'], t['message_count'])
for t in all_tickets if t['message_count'] > 95]
print(f"{len(over_cap)} tickets require splitting ({len(over_cap)/len(all_tickets)*100:.1f}%)")Do not ignore the 100-thread ceiling. It's common to pass a small sample migration and then fail in production because only a few legacy tickets exceed 100 messages once you include private notes, forwards, and phone logs. Pre-calculate thread counts and split before load, not after.
Plan-Based Rate Limits
Help Scout's API rate limits are plan-tiered: Standard allows up to 200 requests per minute, Plus up to 400, and Pro up to 800. All users on the same account share the rate limit bucket, and write requests count as two requests (docs.helpscout.com).
Response headers X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset tell you where you stand. On HTTP 429, respect the Retry-After header.
For a dataset of 20,000 tickets averaging 5 messages each, that's roughly 120,000 API calls minimum (20K conversation creates + 80K thread adds + customer lookups). At 400 RPM on the Plus plan, write requests at double cost yields an effective throughput of ~200 net write operations per minute — approximately 10 hours of continuous import at full throughput, not counting retries or attachment processing. Attachments add roughly 1–3 additional requests per message containing files.
Rate limit math at scale: For 50K+ tickets, consider provisioning separate OAuth apps to parallelize the import across multiple tokens. Each OAuth application gets its own independent rate limit bucket.
Common API Error Codes and Recovery Actions
| HTTP Code | Help Scout Scenario | Recovery Action |
|---|---|---|
412 Precondition Failed |
Conversation has reached 100-thread limit | Split conversation before retrying; update ID mapping table |
429 Too Many Requests |
Rate limit bucket exhausted | Read Retry-After header; sleep and retry with exponential backoff |
422 Unprocessable Entity |
Invalid field value (wrong type, bad date format, unknown dropdown ID) | Log payload; fix transform layer; do not retry without fixing |
409 Conflict |
Duplicate customer email or conversation already exists | Check deduplication logic; use existing resource ID |
400 Bad Request |
Malformed JSON or missing required field | Validate payload schema; check for null values in required fields |
404 Not Found |
Referenced mailbox, user, or team ID doesn't exist | Verify prerequisite resources were created; check ID mapping |
403 Forbidden |
Token lacks required scope or resource ownership mismatch | Reissue OAuth token with correct scopes; check app permissions |
Attachment Size Limit
Help Scout expects attachment data as valid Base64, encoded inline in the thread creation payload. The original file size must be under 10MB (developer.helpscout.com). Large attachments significantly increase payload size — a 10MB file Base64-encodes to ~13.3MB of string data. If Deskpro contains files exceeding this limit, your script must catch HTTP 400 or 422, upload the file to external storage (S3, GCS), and append a direct link as a note in the conversation.
Email Identity Uniqueness
Help Scout requires email addresses to be globally unique across the account. Shared customer identities, merged aliases, and duplicate Deskpro people records must be normalized before import. Help Scout also notes that a customer can be locked for modification after exceeding 5,000 conversations (developer.helpscout.com).
Deskpro's person.merge_target pattern: Deskpro allows person records to be merged, leaving behind ghost IDs that point to a merge_target on the surviving record. During extraction, any person record with a non-null merge_target must be resolved to the canonical ID before building your customer mapping table — otherwise you'll create duplicate Help Scout customers and generate referential failures during conversation import.
Dual Authentication Requirement
Help Scout requires two different auth patterns for a full migration: OAuth 2.0 for the Mailbox API (conversations, customers, users) and Basic-auth API keys for the Docs API v1 (knowledge base content) (developer.helpscout.com). Teams often wire up conversation import first and only discover the Docs auth gap when article validation starts. OAuth access tokens expire after 48 hours, so your script must handle token refresh automatically.
Field Update Semantics
Help Scout's PUT /v2/conversations/{id}/fields endpoint replaces all custom fields — any field not included in the payload is removed. Tag updates have the same full-state behavior: if you send only the new tag, the old ones disappear. Date fields must be YYYY-MM-DD, dropdowns must use the option identifier (not the display label), and multiline conversation fields top out at 15,000 characters. Always send the complete field set and full tag list in every request.
Phase 1: Extract Data from Deskpro
Deskpro Export Options
Stat Builder CSV export — Deskpro's built-in reporting tool can export ticket data to CSV, but it's capped at roughly 2,500 tickets per query and approximately 55 fields. It gives you metadata (subject, status, dates, field values) but not full message bodies or attachments (support.deskpro.com). Use it for audit baselines and reconciliation counts, not as your migration backbone.
Deskpro's DPQL (its SQL-like query language) is useful for pre-migration auditing:
-- Count tickets by department and status
SELECT tickets.department, tickets.status, COUNT(tickets.id) as count
FROM tickets
GROUP BY tickets.department, tickets.status
-- Identify tickets with many messages (potential 100-thread candidates)
SELECT tickets.id, tickets.subject, COUNT(messages.id) as msg_count
FROM tickets
JOIN ticket_messages AS messages ON messages.ticket = tickets.id
GROUP BY tickets.id
HAVING msg_count > 90
ORDER BY msg_count DESC
-- Check custom field fill rates
SELECT tickets.custom_data[11], COUNT(*) as count
FROM tickets
WHERE tickets.custom_data[11] IS NOT NULLDPQL uses tickets.custom_data [#] for custom field values, where # is the field definition ID. Use these queries for baselines and triage decisions, then pull actual records through the API (support.deskpro.com).
Deskpro REST API v2 — Full access to tickets, messages, people, organizations, KB articles, and attachments. Authentication uses API keys created under Admin > Apps & Integrations > API Keys. For any migration that needs message history, the API is the only viable extraction path.
API Extraction Pipeline
Extract in dependency order — downstream objects need upstream references:
- Agents —
GET /api/v2/agents— Needed to resolve assignee mappings. - Agent Teams —
GET /api/v2/agent_teams— Team assignment mapping. - Departments —
GET /api/v2/departments— Maps to Help Scout mailboxes. - People —
GET /api/v2/people— Customer records with emails, phones, organization links. Resolvemerge_targetbefore building mapping table. - Organizations —
GET /api/v2/organizations— Company records. - Tickets —
GET /api/v2/tickets— Paginate through all tickets. Metadata only by default. - Messages —
GET /api/v2/tickets/{id}/messages— Full message content per ticket. This is the expensive step. - Attachments —
GET /api/v2/tickets/{id}/messages/{id}/attachments/{id}— Binary download per attachment. - KB Articles —
GET /api/v2/kb/articles— Article content and category associations. - Custom Fields — Field definitions come from the ticket schema; values are embedded in ticket responses.
Deskpro supports sideloading related data (e.g., ?with=messages,person) in a single request, which reduces network round-trips. The trade-off is massive JSON payloads that consume significant memory. For large datasets, fetching messages per-ticket gives you better control over memory and failure recovery.
Idempotent Extraction with Checkpoint Recovery
For datasets above 10K tickets, blind extraction without checkpointing means a mid-run failure forces a full restart. Build a checkpoint file or local database that records processed ticket IDs:
import requests
import time
import json
import os
DESKPRO_URL = "https://yourcompany.deskpro.com/api/v2"
HEADERS = {"Authorization": "key YOUR_API_KEY"}
CHECKPOINT_FILE = "migration_checkpoint.json"
def load_checkpoint():
if os.path.exists(CHECKPOINT_FILE):
with open(CHECKPOINT_FILE) as f:
return json.load(f)
return {"processed_ticket_ids": [], "last_page": 0}
def save_checkpoint(checkpoint):
with open(CHECKPOINT_FILE, "w") as f:
json.dump(checkpoint, f)
def extract_all_tickets():
checkpoint = load_checkpoint()
seen_ids = set(checkpoint["processed_ticket_ids"])
tickets = []
page = checkpoint["last_page"] + 1
while True:
resp = requests.get(
f"{DESKPRO_URL}/tickets",
headers=HEADERS,
params={"page": page, "count": 50, "orderBy": "id", "orderDir": "ASC"}
)
resp.raise_for_status()
data = resp.json()
batch = data.get("data", [])
if not batch:
break
for ticket in batch:
if ticket["id"] not in seen_ids:
tickets.append(ticket)
seen_ids.add(ticket["id"])
checkpoint["last_page"] = page
checkpoint["processed_ticket_ids"] = list(seen_ids)
save_checkpoint(checkpoint)
page += 1
time.sleep(0.2)
return tickets
def extract_messages(ticket_id):
resp = requests.get(
f"{DESKPRO_URL}/tickets/{ticket_id}/messages",
headers=HEADERS
)
resp.raise_for_status()
return resp.json().get("data", [])For very large datasets, standard offset pagination degrades as the database scans grow. Filter subsequent queries using id ranges — track the highest processed ticket ID and use id_greater_than parameters to maintain stable extraction speeds rather than relying on page offsets.
On-Premise Deskpro users: If you're running Deskpro On-Premise, you can extract directly from the database. The core tables are tickets (metadata), ticket_messages (message bodies, joined on ticket_messages.ticket = tickets.id), ticket_message_attachments (file metadata, joined on ticket_message_attachments.message = ticket_messages.id), and people (customer records). Attachment binary files live on the filesystem under the configured storage path. Direct database extraction bypasses API rate limits entirely and is significantly faster for datasets above 50K tickets — but verify your Deskpro version's schema before attempting, as table structures differ between major versions.
Phase 2: Transform Data for Help Scout
Transformation is where the real engineering work lives. You're translating between two fundamentally different data models.
Status Mapping
| Deskpro Status | Help Scout Status | Notes |
|---|---|---|
awaiting_user |
pending |
Or active — depends on your post-cutover workflow. |
awaiting_agent |
active |
|
resolved |
closed |
|
closed |
closed |
|
archived |
closed |
|
on_hold |
pending |
Help Scout supports three conversation statuses: active, pending, and closed. Deskpro's six-state model collapses lossily into three. If your team relies on the distinction between awaiting_user and awaiting_agent for workflow routing, encode the original status as a tag (e.g., deskpro-status:awaiting-user) to preserve it for post-migration reporting and filtering.
Payload Comparison: Deskpro Ticket → Help Scout Conversation
Understanding the shape of the data on both sides eliminates ambiguity in the transformation layer. A typical Deskpro ticket API response:
{
"id": 10042,
"ref": "TICKET-10042",
"subject": "Cannot log in after password reset",
"status": "awaiting_user",
"date_created": "2024-03-15T09:22:00+0000",
"date_last_activity": "2024-03-16T14:05:00+0000",
"department": { "id": 3, "title": "Customer Support" },
"agent": { "id": 7, "name": "Alice Chen" },
"person": { "id": 1892, "primary_email": { "email": "bob@example.com" }, "name": "Bob Smith" },
"labels": ["vip", "billing"],
"fields": [
{ "id": 11, "title": "Product", "value": "Enterprise" },
{ "id": 12, "title": "Account Tier", "value": "Gold" }
],
"messages_count": 4
}The target Help Scout conversation payload:
{
"subject": "Cannot log in after password reset",
"customer": { "email": "bob@example.com" },
"mailboxId": 85,
"type": "email",
"status": "pending",
"createdAt": "2024-03-15T09:22:00Z",
"imported": true,
"assignTo": 42,
"tags": ["vip", "billing", "deskpro-status:awaiting-user"],
"threads": [
{
"type": "customer",
"customer": { "email": "bob@example.com" },
"text": "<p>Hi, I'm locked out after the password reset...</p>",
"createdAt": "2024-03-15T09:22:00Z",
"imported": true
}
]
}Key transformations: department ID 3 → mailbox ID 85 (from your ID mapping table), Deskpro agent ID 7 → Help Scout user ID 42, status awaiting_user → pending plus the synthesized tag, and all field values queued for a subsequent PUT /v2/conversations/{id}/fields call.
The imported: true Flag
This is non-negotiable for migrations. When creating a conversation or adding a thread via the Help Scout API without this flag, Help Scout treats it as a live interaction — it sends email notifications to customers, triggers workflows, reopens closed conversations, and applies current timestamps instead of the historical ones you provide.
Setting "imported": true on every migrated conversation and thread instructs Help Scout to:
- Suppress all outgoing email notifications
- Bypass active workflows and auto-responders
- Respect the
createdAttimestamps you provide (ISO 8601 format with timezone)
Thread Ordering and Creation
When creating conversations via the Help Scout API, you pass an initial thread in the POST /v2/conversations payload. Remaining threads must be added sequentially via separate endpoint calls:
- Customer messages →
POST /v2/conversations/{id}/customer - Agent replies →
POST /v2/conversations/{id}/reply - Internal notes →
POST /v2/conversations/{id}/notes
Always add threads in chronological order and set imported: true on every call. This per-thread API call pattern is why rate limits matter so much — a single Deskpro ticket with 20 messages requires a minimum of 20 API calls to fully reconstruct in Help Scout, each counting double against the rate limit bucket.
Attachment Transformation
Attachments must be Base64-encoded and included inline in the thread creation payload:
{
"text": "Here is the file you requested.",
"customer": { "id": 456 },
"imported": true,
"attachments": [
{
"fileName": "report.pdf",
"mimeType": "application/pdf",
"data": "JVBERi0xLjQKMSAw..."
}
]
}Your migration script must: download each attachment from Deskpro, check file size before encoding (a 10MB file becomes ~13.3MB as Base64), Base64-encode files under the limit and embed inline, and handle oversized files by uploading to external storage and appending a link note. Attachment processing adds 1–3 API calls per message containing files — budget this separately from your base rate limit math.
Inactive Agent Handling
Over years of using Deskpro, you've likely deactivated agents who left the company. Help Scout will reject thread payloads if the user ID belongs to a non-existent user. Two options:
- Create stub users in Help Scout for former employees (with
sendInvite: false), migrate the data, then deactivate them. Preserves attribution integrity. - Map to a generic "Legacy Agent" account and prepend the original agent's name to the thread body (e.g.,
[Originally sent by Jane Doe]). Simpler but loses direct agent linking.
CC and BCC Handling
Deskpro manages CCs at the ticket level. Help Scout handles CCs and BCCs within the cc and bcc arrays on specific threads. Map Deskpro's ticket-level CCs into the thread-level arrays of the initial customer message to ensure those recipients appear on future replies.
Phase 3: Load Data into Help Scout
Load Sequence
Load order must respect Help Scout's referential constraints:
- Create Users — Via API (
POST /v2/users) with admin/owner access, usingsendInvite: falseduring staging. Alternatively, invite agents through the UI or SCIM provisioning (Pro plan only). - Create Mailboxes — Set up in the Help Scout UI to match Deskpro departments. Record the mailbox IDs for your mapping table.
- Create Teams — Configure via the Help Scout UI.
- Create Custom Fields — Up to 10 per mailbox in Help Scout settings. Record the field IDs for the transform layer.
- Create Customers —
POST /v2/customerswith email, name, phone, and organization. Pre-creating customers gives cleaner deduplication than relying on auto-creation during conversation import. Help Scout auto-deduplicates on email address. - Create Conversations —
POST /v2/conversationswith the initial thread (oldest message),createdAt,mailboxId,status,tags, andimported: true. Store the returnedResource-IDheader in your ID mapping table. - Add Threads — For each remaining message, POST to the appropriate thread endpoint in chronological order, always with
imported: true. - Set Custom Fields —
PUT /v2/conversations/{id}/fieldswith the complete mapped field set. - Migrate Docs — Use the Docs API v1 to create collections, categories, and articles.
Idempotent Load with Deduplication
For large migrations, a crash-resumable loader that tracks which conversations have already been created prevents duplicate records and saves hours of cleanup:
import requests
import sqlite3
HS_API = "https://api.helpscout.net/v2"
# Initialize local tracking database
def init_db(db_path="migration_state.db"):
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS conversation_map (
deskpro_ticket_id INTEGER PRIMARY KEY,
helpscout_conversation_id TEXT,
status TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
return conn
def get_or_create_token(client_id, client_secret):
"""Exchange client credentials for OAuth token; handle refresh."""
resp = requests.post("https://api.helpscout.net/v2/oauth2/token", data={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
})
resp.raise_for_status()
return resp.json()["access_token"]
def create_conversation(conn, token, mailbox_id, ticket):
# Check if already migrated
row = conn.execute(
"SELECT helpscout_conversation_id FROM conversation_map WHERE deskpro_ticket_id = ?",
(ticket["id"],)
).fetchone()
if row:
return row[0] # Already created; return existing ID
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {
"subject": ticket["subject"],
"customer": {"email": ticket["person"]["primary_email"]["email"]},
"mailboxId": mailbox_id,
"type": "email",
"status": map_status(ticket["status"]),
"createdAt": ticket["date_created"],
"imported": True,
"threads": [{
"type": "customer",
"customer": {"email": ticket["person"]["primary_email"]["email"]},
"text": ticket["first_message_body"],
"createdAt": ticket["date_created"],
"imported": True
}],
"tags": ticket.get("labels", [])
}
resp = requests.post(f"{HS_API}/conversations", headers=headers, json=payload)
resp.raise_for_status()
hs_id = resp.headers.get("Resource-ID")
conn.execute(
"INSERT INTO conversation_map (deskpro_ticket_id, helpscout_conversation_id, status) VALUES (?, ?, 'created')",
(ticket["id"], hs_id)
)
conn.commit()
return hs_id
def map_status(deskpro_status):
return {
"awaiting_user": "pending",
"awaiting_agent": "active",
"resolved": "closed",
"closed": "closed",
"archived": "closed",
"on_hold": "pending"
}.get(deskpro_status, "active")Watch for silent data loss. Help Scout's field update endpoint replaces all custom fields — any field not included in the payload is removed. Tag updates have the same full-state behavior. If your script updates fields in multiple passes, the second pass wipes the first. Always send the complete field set and full tag list in every request.
Delta Extraction for Cutover Sync
The cutover strategy requires a delta migration to catch tickets modified since the initial extraction. Use Deskpro's date_last_activity filter with the timestamp of your initial extraction run:
def extract_delta_tickets(since_timestamp):
"""Extract tickets modified after initial migration run."""
tickets = []
page = 1
while True:
resp = requests.get(
f"{DESKPRO_URL}/tickets",
headers=HEADERS,
params={
"page": page,
"count": 50,
"date_last_activity_start": since_timestamp,
"orderBy": "date_last_activity",
"orderDir": "ASC"
}
)
resp.raise_for_status()
batch = resp.json().get("data", [])
if not batch:
break
tickets.extend(batch)
page += 1
time.sleep(0.2)
return ticketsFor tickets that already exist in Help Scout (created during the initial run), update their status and add any new threads. For truly new tickets, run the full create path.
Knowledge Base Migration: Deskpro KB → Help Scout Docs
Deskpro's knowledge base uses a flat Category → Article structure. Help Scout Docs uses a three-tier hierarchy: Site → Collection → Category → Article.
The Deskpro KB API (GET /api/v2/kb/articles) returns article content as HTML. The Help Scout Docs API v1 accepts HTML in the text field of the Create Article endpoint.
Key differences to handle:
- Category mapping. If Deskpro has a flat list of categories, create one Help Scout Collection and map each Deskpro category to a Help Scout category within it.
- Inline images. Article images hosted on Deskpro will break once you decommission the platform. Your script must parse the HTML of every article, locate
<img>tags, download the source images, upload them as Docs article assets (or to a CDN), and rewrite thesrcattributes before pushing the article. - Article slugs. Won't transfer automatically, but you can specify a custom slug via the Docs API
slugfield. Preserving slugs maintains inbound links from search engines and existing documentation references. - Visibility. Deskpro supports per-category access groups. Help Scout Docs supports public and private collections but lacks per-article access control.
- Content types. Deskpro Publish supports news posts, files, and community content alongside KB articles. Help Scout Docs has no equivalent — archive these separately or migrate them to an external CMS (support.deskpro.com).
The Docs API v1 uses a separate API key (Basic auth, not OAuth), with its own rate limiting based on the number of Docs sites on your account, counted per 10-minute window. Use X-Ratelimit-Remaining-Minute response headers to track remaining capacity.
What Doesn't Migrate
Be explicit with stakeholders about what's lost before committing to the migration:
- SLAs — Deskpro has built-in SLA policies with escalation rules. Help Scout has no native SLA engine. Third-party apps or manual tracking are the only alternatives. If SLA compliance is a contractual obligation, this is a blocker.
- Triggers & Automations — Deskpro's trigger/escalation rules don't export in any portable format. They must be manually recreated as Help Scout Workflows.
- Satisfaction Surveys — Deskpro's historical feedback ratings don't carry over. Help Scout has its own happiness ratings, but historical scores stay in Deskpro.
- Chat Transcripts — Deskpro live chat transcripts can map to Help Scout's chat thread type, but the Beacon widget uses a different architecture.
- Ticket Followers — Deskpro's follower concept doesn't map directly to Help Scout's CC model.
- Saved Filters/Views — Must be rebuilt in Help Scout's UI.
- Historical Report Data — Deskpro's historical reporting data doesn't transfer. Export metrics separately before cutover — once you decommission Deskpro, this data is gone.
- Saved Replies / Snippets — Must be recreated manually.
- Deskpro On-Premise logs — Any server-level logs, audit trails, or access logs are local to your infrastructure and have no Help Scout equivalent.
Validation and Testing
Never run a migration without a structured validation pass. Validate against extracted source data, not from memory.
Validation Checklist
- Record counts — Total conversations in Help Scout should match total tickets extracted from Deskpro (minus intentional exclusions). A discrepancy of more than 0.1% warrants investigation.
- Thread counts — Spot-check 50–100 conversations. Thread count should match Deskpro message count (accounting for the 100-thread cap and splits). Include notes and phone log entries in the count.
- Timestamp integrity — Verify
createdAton migrated conversations matches original Deskpro creation dates. Sort by date in Help Scout and check boundary cases. - Customer linking — Search by email in Help Scout and verify conversation counts match. Pay special attention to customers whose Deskpro records had
merge_targetset. - Custom field values — Spot-check 20+ conversations across different departments. Remember the full-state replacement behavior — verify all 10 fields, not just the ones you recently updated.
- Attachment accessibility — Open attachments across 20–30 conversations in different mailboxes. Verify inline rendering and that oversized-file links resolve correctly.
- KB article rendering — Review migrated Docs articles for broken
<img>tags, malformed HTML entities, missing categories, and broken internal cross-links. - Tag completeness — Verify tags transferred correctly, including tags synthesized from Deskpro custom fields and status values.
- Merged person resolution — Verify that conversations previously owned by merged Deskpro person records are correctly attributed to the canonical customer in Help Scout.
Automated Validation with Help Scout Search API
Use Help Scout's conversation search API to build a QA harness instead of clicking through the UI:
GET /v2/conversations?status=all&mailbox=123
GET /v2/conversations?query=(email:"alice@example.com")
GET /v2/conversations?query=(createdAt:[2025-01-01T00:00:00Z TO 2025-12-31T23:59:59Z] AND tag:"vip")Use status=all during QA — the List Conversations endpoint defaults to active items only. URL-encode + characters when searching plus-addressed emails. Build reconciliation counts from the X-Total-Count response header and compare against your Deskpro DPQL baseline queries.
Cutover Strategy
The proven approach:
- T-minus 7 days: Run the full historical migration against a staging Help Scout account. Validate thoroughly using the checklist above.
- T-minus 3 days: Finalize Help Scout configuration — mailboxes, workflows, saved replies, Docs site, Beacon setup. Manually rebuild Deskpro triggers and automations as Help Scout Workflows.
- T-minus 1 day: Run a delta migration using
date_last_activity_startto catch tickets modified since the initial extraction. Record the exact timestamp of this run. - Cutover hour: Update email forwarding rules to point at Help Scout mailbox addresses. Disable Deskpro's email processing. Run the final delta sync using the T-minus 1 day timestamp.
- Post-cutover: Keep Deskpro in read-only mode for 30 days as a reference. Monitor Help Scout for missing data. Check agent-reported gaps daily for the first week.
Timeline Estimates
| Dataset Size | Complexity | Estimated Duration |
|---|---|---|
| < 5K tickets | No KB, few custom fields | 3–5 days |
| 5K–20K tickets | KB migration, moderate custom fields | 1–2 weeks |
| 20K–50K tickets | Complex field mapping, large KB | 2–3 weeks |
| 50K+ tickets | Multi-department, heavy attachments | 3–5 weeks |
These estimates include extraction, transformation, loading, and two validation passes. They assume a single engineer working full-time. Attachment-heavy datasets add 30–50% to load time due to download-encode-upload cycles. The dominant cost driver is usually the transformation layer — resolving custom field mapping decisions and handling edge cases in message threading.
When Help Scout Is Not the Right Target
Be honest about fit before committing engineering resources:
- You need granular SLA management. Help Scout has no native SLA engine. If SLA compliance reporting is a contractual obligation, you'll need a third-party solution or a different platform entirely.
- You have 10+ custom fields per department. The 10-field-per-inbox limit is hard. If your workflow depends on structured data capture at that density, Help Scout will feel constraining from day one.
- You need ITIL workflows. Help Scout is deliberately opinionated toward lightweight, email-first support. If your team runs formal incident/problem/change management, Deskpro is a better architectural fit.
- You rely on Deskpro's reporting depth. Help Scout's reporting is improving but still less flexible than Deskpro's DPQL-based Stat Builder. Teams with complex cross-department reporting requirements will hit limits quickly.
- You have high attachment volume. If a significant percentage of your tickets contain attachments near or above 10MB, attachment processing time and external storage complexity add substantially to migration scope.
Making the Call
A Deskpro to Help Scout migration is architecturally tractable — both platforms have mature REST APIs and the data model mapping is well-defined. The risk isn't in getting data out of Deskpro. It's in loading it into Help Scout in a shape that agents can actually work with on day one.
The three constraints that trip up most teams: the 100-thread limit, the 10-custom-field cap, and plan-based rate limits that turn a seemingly simple import into a multi-day pipeline operation.
The technical prerequisites for a successful migration: audit your thread counts before writing a single line of load code, confirm your Help Scout plan covers the custom fields you actually need, and instrument your loader with idempotency from the start — not as an afterthought after the first failed run.
For accounts under 10K tickets with a dedicated engineer, DIY is viable with this guide and our Help Scout migration checklist. For larger datasets, complex field mapping, or teams that can't afford weeks of engineering time — that's what we do at ClonePartner. We handle the extraction, transformation, loading, and validation with zero downtime and full data integrity.
If you're evaluating the reverse direction, our Help Scout to Deskpro migration guide covers that path. For general migration planning, see our help desk migration best practices.
Frequently Asked Questions
- Can I migrate from Deskpro to Help Scout using CSV export?
- Not for a full-fidelity migration. Deskpro's Stat Builder CSV export is limited to roughly 2,500 tickets per query and doesn't include full message bodies or attachments. You need the Deskpro REST API for extraction and the Help Scout Mailbox API v2 for loading.
- Does Help Scout have a built-in Deskpro importer?
- No. Help Scout does not provide a native Deskpro import tool. You must build a custom ETL pipeline using the Deskpro REST API for extraction and the Help Scout Mailbox API v2 for loading, or use a managed migration service.
- What is the maximum number of threads per conversation in Help Scout?
- Help Scout enforces a hard limit of 100 threads per conversation. The API returns HTTP 412 Precondition Failed if you exceed it. Deskpro tickets with more than 100 messages must be split into linked conversations or truncated before import.
- How do I stop Help Scout from emailing customers during migration?
- Set the imported: true flag on every conversation and thread payload you create via the Help Scout API. This suppresses outgoing emails, bypasses active workflows, and lets you preserve original createdAt timestamps.
- How long does a Deskpro to Help Scout migration take?
- For under 5K tickets with no knowledge base: 3–5 days. For 5K–20K with moderate custom fields: 1–2 weeks. For 20K–50K with complex mapping: 2–3 weeks. For 50K+: 3–5 weeks. These estimates assume one dedicated engineer and include validation passes.

