---
title: "Deskpro to Help Scout Migration: A Technical Guide"
slug: deskpro-to-help-scout-migration-a-technical-guide
date: 2026-08-12
author: Abdul
categories: [Help Scout, Migration Guide, Help Desk]
excerpt: "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."
tldr: "Deskpro to Help Scout is an API-to-API ETL job. Key constraints: 100 threads per conversation, 10 custom fields per inbox, plan-tiered rate limits. Budget 1–5 weeks depending on dataset size."
canonical: https://clonepartner.com/blog/deskpro-to-help-scout-migration-a-technical-guide/
---

# Deskpro to Help Scout Migration: A Technical Guide


# Deskpro to Help Scout Migration: A Technical Guide

> [!NOTE]
> **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](https://support.deskpro.com/en/kb/articles/how-do-i-export-my-tickets)). 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](https://developer.helpscout.com/mailbox-api/endpoints/users/create/)). |
| 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](https://support.deskpro.com/en-US/kb/articles/overview-of-publish-1)). 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](https://docs.helpscout.com/article/1385-customer-properties)). 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:

1. **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.
2. **Consolidate fields.** Merge related fields into a single multi-line text field (e.g., combine "Product Version" and "Build Number" into "Version Info").
3. **Archive to tags.** Convert low-cardinality fields into tags (e.g., a "Priority" dropdown with 3 values becomes `priority-low`, `priority-medium`, `priority-high`).
4. **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.

> [!WARNING]
> **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](https://developer.helpscout.com/mailbox-api/endpoints/conversations/create/)).

This affects long-running support cases. If a Deskpro ticket has 150 messages (including notes, forwards, and phone logs), you have two options:

1. **Split into linked conversations.** Create a second conversation with a subject prefix like `[Continued] Original Subject` and add a note linking the two conversation IDs.
2. **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:

```python
# 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}%)")
```

> [!CAUTION]
> **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](https://docs.helpscout.com/article/1140-mailbox-api)).

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.

> [!NOTE]
> **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](https://developer.helpscout.com/mailbox-api/overview/errors/)). 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](https://developer.helpscout.com/mailbox-api/overview/errors/)).

**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](https://developer.helpscout.com/mailbox-api/overview/authentication/)). 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](https://support.deskpro.com/en/kb/articles/how-do-i-export-my-tickets)). 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:

```sql
-- 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 NULL
```

DPQL 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](https://support.deskpro.com/en-US/kb/articles/pdf/querying-custom-fields)).

**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:

1. **Agents** — `GET /api/v2/agents` — Needed to resolve assignee mappings.
2. **Agent Teams** — `GET /api/v2/agent_teams` — Team assignment mapping.
3. **Departments** — `GET /api/v2/departments` — Maps to Help Scout mailboxes.
4. **People** — `GET /api/v2/people` — Customer records with emails, phones, organization links. Resolve `merge_target` before building mapping table.
5. **Organizations** — `GET /api/v2/organizations` — Company records.
6. **Tickets** — `GET /api/v2/tickets` — Paginate through all tickets. Metadata only by default.
7. **Messages** — `GET /api/v2/tickets/{id}/messages` — Full message content per ticket. This is the expensive step.
8. **Attachments** — `GET /api/v2/tickets/{id}/messages/{id}/attachments/{id}` — Binary download per attachment.
9. **KB Articles** — `GET /api/v2/kb/articles` — Article content and category associations.
10. **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:

```python
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.

> [!TIP]
> **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:

```json
{
  "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:

```json
{
  "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 `createdAt` timestamps 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:

```json
{
  "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:

1. **Create stub users** in Help Scout for former employees (with `sendInvite: false`), migrate the data, then deactivate them. Preserves attribution integrity.
2. **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:

1. **Create Users** — Via API (`POST /v2/users`) with admin/owner access, using `sendInvite: false` during staging. Alternatively, invite agents through the UI or SCIM provisioning (Pro plan only).
2. **Create Mailboxes** — Set up in the Help Scout UI to match Deskpro departments. Record the mailbox IDs for your mapping table.
3. **Create Teams** — Configure via the Help Scout UI.
4. **Create Custom Fields** — Up to 10 per mailbox in Help Scout settings. Record the field IDs for the transform layer.
5. **Create Customers** — `POST /v2/customers` with 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.
6. **Create Conversations** — `POST /v2/conversations` with the initial thread (oldest message), `createdAt`, `mailboxId`, `status`, `tags`, and `imported: true`. Store the returned `Resource-ID` header in your ID mapping table.
7. **Add Threads** — For each remaining message, POST to the appropriate thread endpoint in chronological order, always with `imported: true`.
8. **Set Custom Fields** — `PUT /v2/conversations/{id}/fields` with the complete mapped field set.
9. **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:

```python
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")
```

> [!CAUTION]
> **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:

```python
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 tickets
```

For 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 the `src` attributes before pushing the article.
- **Article slugs.** Won't transfer automatically, but you can specify a custom slug via the Docs API `slug` field. 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](https://support.deskpro.com/en-US/kb/articles/overview-of-publish-1)).

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

1. **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.
2. **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.
3. **Timestamp integrity** — Verify `createdAt` on migrated conversations matches original Deskpro creation dates. Sort by date in Help Scout and check boundary cases.
4. **Customer linking** — Search by email in Help Scout and verify conversation counts match. Pay special attention to customers whose Deskpro records had `merge_target` set.
5. **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.
6. **Attachment accessibility** — Open attachments across 20–30 conversations in different mailboxes. Verify inline rendering and that oversized-file links resolve correctly.
7. **KB article rendering** — Review migrated Docs articles for broken `<img>` tags, malformed HTML entities, missing categories, and broken internal cross-links.
8. **Tag completeness** — Verify tags transferred correctly, including tags synthesized from Deskpro custom fields and status values.
9. **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:

```text
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:

1. **T-minus 7 days:** Run the full historical migration against a staging Help Scout account. Validate thoroughly using the checklist above.
2. **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.
3. **T-minus 1 day:** Run a delta migration using `date_last_activity_start` to catch tickets modified since the initial extraction. Record the exact timestamp of this run.
4. **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.
5. **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](https://clonepartner.com/blog/blog/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](https://clonepartner.com/blog/blog/help-scout-to-deskpro-migration-a-technical-guide/) covers that path. For general migration planning, see our [help desk migration best practices](https://clonepartner.com/blog/blog/best-practices-for-help-desk-data-migration/).

> Need help migrating from Deskpro to Help Scout? Our engineers handle the full ETL pipeline — field mapping, thread splitting, attachment handling, and delta sync — so your team stays focused on supporting customers. Book a free 30-minute consultation.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## 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.
