---
title: "Freshservice to HaloITSM Migration: Data Mapping, API Limits & Methods"
slug: freshservice-to-haloitsm-migration-data-mapping-api-limits-methods
date: 2026-11-09
author: Roopendra Talekar
categories: [Freshservice, HaloITSM, Migration Guide]
excerpt: "Technical guide to migrating from Freshservice to HaloITSM — covering object-by-object data mapping, Freshservice API rate limits by plan, and methods that work at scale."
tldr: "Freshservice to HaloITSM migration requires mapping Freshservice's flat ticket model to HaloITSM's ITIL-typed objects, managing per-plan API rate limits (100–500 req/min), and loading data in strict referential order."
canonical: https://clonepartner.com/blog/freshservice-to-haloitsm-migration-data-mapping-api-limits-methods
---

# Freshservice to HaloITSM Migration: Data Mapping, API Limits & Methods


# Freshservice to HaloITSM Migration: Data Mapping, API Limits & Methods

A **Freshservice to HaloITSM migration** moves tickets (incidents, service requests), problems, changes, assets, CMDB configuration items, agents, requesters, groups, knowledge base articles, conversations, and attachments out of Freshservice and into HaloITSM. Both platforms expose REST APIs that cover all core objects, so the transport itself is straightforward. The hard part is translating Freshservice's relatively flat ticket model into HaloITSM's granular ITIL ticket types, managing Freshservice's per-plan rate limits during extraction, and loading data into HaloITSM in the right order so referential integrity holds.

This guide covers the object-by-object data mapping, API constraints on both sides, field-level translation, required fields by HaloITSM ticket type, error handling, and the migration methods that work at scale.

For a broader comparison of how these platforms differ at an architectural level, see our [Freshservice vs HaloITSM: Architecture, TCO, and Migration](https://clonepartner.com/blog/blog/freshservice-vs-haloitsm-architecture-tco-and-migration).

## What data moves from Freshservice to HaloITSM?

Every migration-grade transfer covers more than tickets. Here is the full object map between the two platforms:

| Freshservice object | API endpoint | HaloITSM target | Notes |
|---|---|---|---|
| Tickets (Incidents) | `/api/v2/tickets` | Tickets (type: Incident) | Map Freshservice `type` field value |
| Tickets (Service Requests) | `/api/v2/tickets` | Tickets (type: Service Request) | Requested items need separate extraction |
| Problems | `/api/v2/problems` | Problems | Separate ITIL object in both platforms |
| Changes | `/api/v2/changes` | Changes | HaloITSM supports standard, normal, and emergency change types |
| Assets / CMDB CIs | `/api/v2/assets` | Assets / CMDB CIs | Both platforms use a unified asset-CI model |
| CMDB relationships | `/api/v2/relationships` (account-wide) or `/api/v2/assets/[display_id]/relationships` (one asset) | CI relationships | Remap IDs after assets load |
| Agents | `/api/v2/agents` | Agents | Role mapping required |
| Requesters | `/api/v2/requesters` | Users | Deduplicate by email before loading |
| Groups | `/api/v2/groups` | Teams | Assignment routing may need reconfiguration |
| Departments | `/api/v2/departments` | Departments / Sites | HaloITSM uses a Customer → Site → User hierarchy |
| Knowledge base articles | `/api/v2/solutions/articles` | Knowledge base articles | Folder structure maps to KB categories |
| Conversations | `/api/v2/tickets/[id]/conversations` | Actions (ticket updates) | Thread order must be preserved by `created_at` |
| Attachments | Embedded in conversation objects | Attachments | Returned as `attachment_url` links — download during extraction |
| Service catalog items | `/api/v2/service_catalog/items` | Service catalogue items | Use as an inventory; rebuild the catalog in HaloITSM |

### Where the data models diverge

Freshservice and HaloITSM both claim ITIL alignment, but their internal models differ in ways that directly affect mapping and load logic.

**Ticket typing.** Freshservice tickets are a single object with a `type` property distinguishing Incidents from Service Requests. Problems, Changes, and Releases are separate API resources. HaloITSM treats every ITIL process — incidents, service requests, changes, problems — as a distinct ticket type with its own required fields and workflow paths. Each Freshservice record must be routed to the correct HaloITSM ticket type, and the required fields for that type must be populated or the API rejects the write.

**CMDB and asset model.** Both platforms use a unified asset-CI record — an asset becomes a configuration item when a service depends on it. The mapping here is closer to 1:1 than in [migrations to platforms like ServiceNow](https://clonepartner.com/blog/blog/how-to-migrate-from-haloitsm-to-servicenow-technical-guide) (which separates them). The main challenge is remapping CMDB relationships. Freshservice lists every relationship in the account at `/api/v2/relationships`, and each one references its primary and secondary items by Freshservice ID ([api.freshservice.com](https://api.freshservice.com/)). Every referenced asset, requester and agent must exist in HaloITSM first. Asset types, customers, and sites must also exist before you import asset records.

**Conversation model.** Freshservice stores replies and notes as "conversations" on a ticket. HaloITSM stores them as "actions" — an append-only history on the ticket record. The mapping is conceptually simple but operationally expensive: each ticket's conversations require a separate paginated API call to extract from Freshservice.

**Custom fields.** Freshservice supports custom fields on tickets, problems, changes, releases, and assets. The `custom_fields` JSON object acts as a flat key-value store. HaloITSM custom fields enforce data types at write time — push a string into a field typed as a date or number and the API rejects it. If a custom field is a dropdown in HaloITSM, the exact string value from Freshservice must exist in the HaloITSM custom field dictionary, or the payload is rejected. Map and validate every custom field type before loading.

## What are the Freshservice API rate limits for migration?

**Freshservice rate limits are account-wide, enforced per minute, and vary by plan.** These limits are the primary throughput constraint during extraction.

| Plan | Overall limit (req/min) | List All Tickets (req/min) | List All Assets (req/min) |
|---|---|---|---|
| Starter | 100 | 40 | 40 |
| Growth | 200 | 70 | 70 |
| Pro | 400 | 120 | 120 |
| Enterprise | 500 | 140 | 140 |

These limits apply to the entire account, irrespective of the number of agents or IP addresses making the calls ([api.freshservice.com](https://api.freshservice.com/)). Every integration, custom app, and script calling the API on the account draws from the same pool, and invalid requests count too. Exceeding the limit returns HTTP 429 with a `Retry-After` header giving the seconds to wait. Your extraction script must read this header and pause.

Freshservice authenticates with an API key over basic auth (key as username, "X" as password) ([api.freshservice.com](https://api.freshservice.com/)).

> [!WARNING]
> Freshservice's filter endpoint (`/api/v2/tickets/filter`) returns a fixed 30 records per page. For migration-scale extraction, use the standard list endpoint (`/api/v2/tickets`) with `updated_since` time windows instead. Always pass `updated_since`: without it, the list endpoint returns only tickets created in the past 30 days ([api.freshservice.com](https://api.freshservice.com/)).

### Pagination rules

Freshservice v2 API uses page-number pagination with `page` (starting at 1) and `per_page` (maximum 100). The response includes a `link` header pointing to the next page, and the header is absent on the last page ([api.freshservice.com](https://api.freshservice.com/)). Always follow that header rather than guessing sequential pages — deleted records can cause pagination drift. An empty results array signals the last page.

```python
import requests
import time

BASE_URL = "https://yourdomain.freshservice.com/api/v2"
API_KEY = "your_api_key"

def extract_tickets(updated_since):
    page = 1
    all_tickets = []
    while True:
        resp = requests.get(
            f"{BASE_URL}/tickets",
            auth=(API_KEY, "X"),
            params={"per_page": 100, "page": page, "updated_since": updated_since}
        )
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 60))
            time.sleep(wait)
            continue
        resp.raise_for_status()
        tickets = resp.json().get("tickets", [])
        if not tickets:
            break
        all_tickets.extend(tickets)
        page += 1
    return all_tickets
```

### Practical throughput math

On a **Pro plan** (120 List Tickets calls/min, 100 records/page), you can extract roughly 12,000 ticket records per minute from the list endpoint alone. But each ticket's conversations require a separate call — and with 400 total requests/min shared across all operations, conversation extraction is where the bottleneck hits. A 10,000-ticket backlog means at least 10,000 additional API calls just for conversation history, one per ticket, plus one more for each extra page on long threads.

Freshservice offers **rate limit add-on packs** on Pro and Enterprise plans: Add-on Pack 1 raises the overall limit to 1,000 req/min, and Pack 2 to 2,000 req/min ([api.freshservice.com](https://api.freshservice.com/)). For large extractions, these add-ons can cut migration time significantly.

### `updated_since` coverage across object types

The `updated_since` parameter is available on tickets but **not uniformly supported across all Freshservice object types**. For the standard list endpoint:

- **Tickets** (`/api/v2/tickets`): `updated_since` supported — use for delta syncs
- **Problems** (`/api/v2/problems`): no documented `updated_since` filter — paginate the full list and compare `updated_at`
- **Changes** (`/api/v2/changes`): supports `updated_since`
- **Assets** (`/api/v2/assets`): filter by `updated_at` using the `filter` query parameter, not `updated_since`
- **Requesters / Agents**: no updated-since filter — paginate the full list and compare against a local snapshot

For assets specifically, use `/api/v2/assets?filter="updated_at:>'YYYY-MM-DD'"` (URL-encoded) to scope delta extractions. The asset filter works at date granularity, returns 30 records per page, and returns at most 40 pages ([api.freshservice.com](https://api.freshservice.com/)). Failing to account for per-object filter support differences causes delta syncs to miss changed asset records while correctly capturing ticket changes.

## How does HaloITSM's API handle data import?

**HaloITSM exposes a REST API secured with OAuth 2.0 (Client Credentials flow) that accepts and returns JSON only.** Each HaloITSM instance publishes its own API reference as a Swagger page on the instance itself. Confirm endpoint names, authentication methods, and any instance-specific constraints there before building import scripts.

To set up API access:

1. Navigate to **Configuration → Integrations → HaloITSM API**
2. Click **View Applications** and create a new application
3. Select **Client ID and Secret (Services)** as the authentication method
4. Note your **Resource Server**, **Authorization Server**, and **Tenant** values
5. Use these credentials to obtain a bearer token, then call endpoints like `POST /api/Tickets` or `POST /api/Asset`

> [!NOTE]
> This guide does not quote a HaloITSM rate limit. Confirm any write limits for your own instance on its Swagger page or with Halo, then measure real throughput during the sample migration. On-premise instances are also bounded by the host server's capacity.

### HaloITSM import order

HaloITSM enforces referential integrity at write time. Importing a ticket assigned to an agent who doesn't exist yet results in a rejected write or an orphaned record. Load data in this order:

1. **Customers and sites** — HaloITSM uses a Customer → Site → User hierarchy. These must exist first.
2. **Agents and users** — Must be linked to an existing customer/site.
3. **Teams** — Map from Freshservice groups.
4. **Asset types** — Must exist before importing assets.
5. **Assets and CMDB CIs** — Require customer, site, and asset type to already exist.
6. **Tickets** (incidents, service requests, problems, changes) — Reference agents, users, teams, and optionally assets.
7. **Actions (conversations)** — Appended to existing tickets, preserving chronological order.
8. **Attachments** — Uploaded and linked to the relevant ticket or action.
9. **Knowledge base articles** — Independent of ticket data; can be loaded in parallel.

### HaloITSM API error codes and common failure modes

Status codes and error text vary by HaloITSM version and configuration. Log the full response body for every failed write during the sample migration, and check expected responses on your instance's Swagger page. These failure modes come up most often in bulk loads:

| Failure | Cause | Fix |
|---|---|---|
| Missing mandatory field | A field the target ticket type requires is empty | Check required fields per ticket type (see section below) |
| Invalid field value | Type mismatch — string in a date/number field, or unrecognized dropdown value | Validate and cast all custom field values before POST |
| Missing customer or agent | Ticket or asset references a customer, site, or agent not yet imported | Load in the order above |
| Expired token | Bearer token reached the end of its lifetime mid-run | Implement token refresh; re-authenticate and retry |
| Missing parent ticket | Posting an action to a ticket that was never created | Confirm ticket was successfully created before posting actions |
| Duplicate on re-run | A retry creates the same record twice | Check for the stored Freshservice ID before each create |
| Throttling or timeouts | Write throughput too high for the instance | Back off, honour `Retry-After` when present, reduce batch concurrency |

**Handling HTML bodies:** Malformed tags, unescaped special characters, and very large inline base64-encoded images are common causes of failed writes to the `details` field. Sanitize ticket description HTML through a parser (e.g., Python's `html.parser` or BeautifulSoup) before loading, and strip or replace malformed tags.

## Required fields by HaloITSM ticket type

HaloITSM enforces different mandatory fields depending on the ticket type, and administrators set which fields are mandatory. Missing a required field rejects the write. The lists below are a starting point for the fields a migration usually has to populate. Confirm field names and mandatory flags on your instance's Swagger page and in each ticket type's configuration.

### Incident (default required fields)
- `summary` — ticket subject/title
- `details` — description body
- `tickettype_id` — must match the Incident type ID in your HaloITSM instance
- `user_id` or `reportedby` — requester identity
- `status_id` — must be a valid status for the Incident type

### Service Request (default required fields)
- `summary`
- `details`
- `tickettype_id` — Service Request type ID
- `user_id`
- `status_id`
- `catalogue_id` (if the request maps to a service catalog item) — optional depending on configuration

### Change (default required fields)
- `summary`
- `details`
- `tickettype_id` — Change type ID; HaloITSM distinguishes Standard, Normal, and Emergency by type ID
- `status_id` — Change statuses are a separate list from Incident statuses
- `user_id`

### Problem (default required fields)
- `summary`
- `details`
- `tickettype_id` — Problem type ID
- `status_id`
- `user_id`

> [!WARNING]
> Administrators often add mandatory fields to HaloITSM Change ticket types during ITIL configuration, such as risk assessment, impact, implementation plan, and backout plan. These fields will cause rejections if your source data doesn't map to them. Audit your HaloITSM Change form configuration before migration and decide whether to populate these from Freshservice's change description or leave them as placeholder text.

## How to map Freshservice fields to HaloITSM

The HaloITSM field names in this guide's mapping tables are common API names. Confirm each one on your instance's Swagger page, because names and required flags vary by version and configuration.

### Ticket field mapping

| Freshservice field | HaloITSM field | Mapping notes |
|---|---|---|
| `id` | `thirdpartyid` or custom field | Store the original Freshservice ID for audit trail |
| `subject` | `summary` | Direct map |
| `description` / `description_html` | `details` | HaloITSM accepts HTML in the details field; sanitize before POST |
| `type` (Incident / Service Request) | `tickettype_id` | Route to the correct HaloITSM ticket type ID |
| `status` | `status_id` | Map status integers to HaloITSM status IDs (configurable per type) |
| `priority` | `priority_id` | Freshservice uses 1–4 (Low to Urgent); map to HaloITSM priority IDs |
| `source` | Custom field or drop | HaloITSM doesn't have an equivalent "source" field by default |
| `group_id` | `team_id` | Requires a lookup table built during team import |
| `responder_id` | `agent_id` | Requires agent ID mapping |
| `requester_id` | `user_id` | Requires user ID mapping |
| `department_id` | `department_id` or `site_id` | Depends on your HaloITSM hierarchy setup |
| `category` / `sub_category` / `item_category` | `category_1` / `category_2` / `category_3` | HaloITSM supports multi-level categorization |
| `created_at` | `dateoccurred` | Preserve original timestamps |
| `updated_at` | `lastupdate` | Preserve original timestamps |
| `due_by` | `targetdate` | SLA due date |
| `custom_fields.*` | Custom fields (by API name) | Validate data types match; HaloITSM rejects type mismatches |

### Priority mapping

Freshservice uses numeric priority values (1 = Low, 2 = Medium, 3 = High, 4 = Urgent) ([api.freshservice.com](https://api.freshservice.com/)). HaloITSM priorities are configurable and referenced by ID. The numeric ordering may be inverted — Freshservice uses ascending priority (1 = lowest) while many HaloITSM instances default to descending (1 = highest). Confirm your HaloITSM priority configuration before loading.

```json
{
  "priority_map": {
    "1": {"halo_id": 4, "label": "Low"},
    "2": {"halo_id": 3, "label": "Medium"},
    "3": {"halo_id": 2, "label": "High"},
    "4": {"halo_id": 1, "label": "Urgent"}
  }
}
```

### Status mapping

Freshservice uses hardcoded integers for default statuses. HaloITSM allows administrators to create custom statuses with custom IDs, configured per ticket type — meaning Incidents and Changes have separate, independent status lists. There is no universal map, but the common defaults:

| Freshservice status (integer) | Typical HaloITSM equivalent | Notes |
|---|---|---|
| Open (2) | New / Open | HaloITSM status ID varies by instance |
| Pending (3) | Awaiting Information | No direct equivalent by default; may need a custom status |
| Resolved (4) | Resolved | |
| Closed (5) | Closed | |

Build a translation table in your middleware that maps the integer `2` from Freshservice to the specific `status_id` representing "Open" in your target HaloITSM ticket type. Repeat this for every ticket type — a status ID of `3` on an Incident may mean something entirely different from status ID `3` on a Change.

### Conversations and actions

Freshservice stores ticket history as **Conversations**, split into **Notes** (private) and **Replies** (public). HaloITSM stores these as **Actions**.

When mapping a Freshservice private Note to a HaloITSM Action, set the action's hidden-from-user flag (confirm its field name on your Swagger page) — otherwise internal technician notes are exposed to end-users on the HaloITSM self-service portal. The timestamp of the original reply must be explicitly set in the API payload; if you omit it, HaloITSM stamps every historical action with the migration date.

## Service catalog and requested items

Freshservice Service Requests can contain multiple **Requested Items** — line items from the service catalog with their own custom fields, accessed through `/api/v2/tickets/[id]/requested_items`. These represent the specific hardware, software, or access that was requested.

In HaloITSM, these map to specific child tickets, linked assets, or service request custom fields. Extract requested items separately and map them to the appropriate HaloITSM structure. Plan to rebuild the catalog hierarchy itself in HaloITSM. Freshservice's API lists catalog items and categories (`/api/v2/service_catalog/items`, `/api/v2/service_catalog/categories`), which gives you a source inventory ([api.freshservice.com](https://api.freshservice.com/)).

## Knowledge base migration

Freshservice structures knowledge as **Categories → Folders → Articles**. HaloITSM uses a similar hierarchical structure but allows for more granular permission controls.

When migrating articles, map the `status` (Draft vs. Published) and the `approval_status`. The bigger challenge is internal links: links between articles in Freshservice use internal Freshservice URLs. These URLs break post-migration. Your middleware must build a mapping table of old Article IDs to new HaloITSM Article IDs, then parse the HTML body of every article to rewrite internal hyperlinks to point to the new HaloITSM portal URLs.

## CMDB and asset mapping

Freshservice stores configuration items in a unified asset model via `/api/v2/assets`. When extracting, pay attention to the `asset_type_id` — a laptop has different fields than a software license. You must recreate these asset types in HaloITSM before importing records.

### Asset field mapping (example: hardware asset)

| Freshservice asset field | HaloITSM asset field | Notes |
|---|---|---|
| `name` | `name` | Direct map |
| `asset_type_id` | `assettype_id` | Requires pre-created asset type in HaloITSM |
| `display_id` | `thirdpartyid` or custom field | Preserve for audit trail |
| `description` | `comment` | |
| `department_id` | `site_id` | Map via department→site lookup |
| `assigned_on` | `dateassigned` | |
| `created_at` | `dateinstalled` | |
| `custom_fields.*` | Custom asset fields | Type enforcement applies; validate before POST |
| `ci_type` | Asset → CI flag | HaloITSM promotes asset to CI via service dependency configuration |

Extract relationships through the account-wide endpoint (`/api/v2/relationships`) rather than one call per asset. Keep the per-asset endpoint (`/api/v2/assets/[display_id]/relationships`) for spot checks during validation ([api.freshservice.com](https://api.freshservice.com/)).

Store the Freshservice `display_id` on each HaloITSM asset and check for it before each create. This prevents duplicate records on re-runs.

## Attachments and inline images

Attachments are the most bandwidth-intensive part of an ITSM migration. Freshservice returns attachments as URLs (`attachment_url`), not file contents. Download attachments inline during conversation extraction, not as a separate phase, and re-fetch the ticket or conversation if a stored URL no longer downloads.

To migrate an attachment:

1. Parse the Freshservice ticket or conversation JSON for the `attachments` array
2. Issue a GET request to the `attachment_url`
3. Download the binary payload to a secure staging environment
4. Base64-encode the payload or use multipart/form-data to POST it to the HaloITSM `/api/Attachment` endpoint
5. Associate the returned HaloITSM attachment ID with the corresponding ticket or action

**Inline images** embedded in HTML bodies require an extra step. Download the image, upload it to HaloITSM, then programmatically rewrite the `src` attribute in the HTML body to point to the new HaloITSM hosting URL. Failing to do this results in broken image icons when Freshservice decommissions your old instance.

**Attachment size limits:** In Freshservice, the total size of all of a ticket's attachments cannot exceed 40 MB ([api.freshservice.com](https://api.freshservice.com/)). Check your HaloITSM instance's attachment size setting before the load, and validate files near that limit before upload.

## What are the migration methods?

Three approaches, ranked by fidelity:

### 1. API-to-API (recommended)

Extract from Freshservice's REST API v2, transform in a middleware layer, and load into HaloITSM's REST API. This is the only method that preserves conversations, attachments, CMDB relationships, and ticket threading with full fidelity.

**Best for:** Any migration where conversation history, attachments, or CMDB data matters — which is most production ITSM environments.

**Trade-off:** Requires scripting, rate limit management, and error handling. Freshservice's extraction rate limits are typically the bottleneck during the extraction phase. HaloITSM's write throughput depends on your instance, so measure it during the sample migration. API-based migrations allow for multi-stage validation with checksums, record-count parity, field-level validation, and referential integrity checks.

### 2. CSV import

Export from Freshservice's UI (ticket list view or Analytics), transform in a spreadsheet, and import via HaloITSM's built-in import tools. Confirm which objects your HaloITSM version imports from CSV before you plan around this method.

**Best for:** Small datasets under 5,000 records where conversation history is not required.

**What you lose:** Freshservice's UI export does not include conversations, internal notes, or attachments. You also lose CMDB relationships, service catalog structure, and multi-line HTML bodies with inline images. CSV imports struggle with referential integrity, often resulting in orphaned records. Customers, sites, and asset types must already exist in HaloITSM before asset rows can reference them.

### 3. Managed migration service

A dedicated migration engineer handles the extraction, mapping, transformation, loading, validation, and delta sync. This gives you API-to-API fidelity without building and maintaining the tooling yourself.

## How long does a Freshservice to HaloITSM migration take?

Migration duration is determined by three independent factors: Freshservice extraction throughput (rate-limited by plan tier), HaloITSM write throughput (instance-dependent), and data complexity (conversation density, CMDB relationship depth, custom field volume).

Most migrations are completed within 1–5 business days, depending on data volume, complexity, and API rate limits. Small datasets (under 10,000 records) can finish in under 24 hours.

**Key variables that extend timelines:**
- Plans without rate limit add-on packs: conversation extraction becomes the dominant time cost
- Deep CMDB relationship graphs (many assets with many-to-many CI relationships): every relationship needs ID remapping after the assets load
- Large attachment volumes: download + re-upload is network-bound and cannot be parallelized beyond API limits
- Custom fields requiring data type remediation discovered during sample migration

These figures describe the data migration ClonePartner performs, not your HaloITSM implementation. Platform configuration — workflow setup, SLA configuration, approval processes, portal customization — is a separate project that your team or your HaloITSM partner runs on its own timeline.

## Edge cases and gotchas

### Freshservice's shifting window during extraction

If tickets are created or updated while you're paginating through the list endpoint, the result set can shift — causing duplicates or missed records. Mitigate this by extracting within a strict `updated_since` time window and sorting by `updated_at` ascending. Run a delta sync afterward to catch anything that changed during extraction.

### HaloITSM custom field type enforcement

HaloITSM custom fields enforce strict data types at write time. A field configured as a date rejects a string that doesn't parse as a valid ISO 8601 date. A number field rejects any non-numeric text, including formatted numbers with commas (e.g., `"1,500"` fails; `1500` succeeds). Boolean fields reject `"Yes"/"No"` strings — they require `true`/`false`. Build a type-casting layer in your transformation scripts and validate every custom field mapping against HaloITSM's type constraints during sample migration.

### Disable automations before import

Before loading data into HaloITSM, disable approval processes, SLA timers, email notifications, and any AI/automation rules. Imported historical tickets should not trigger new SLA breaches, send emails to requesters, or fire approval workflows. Re-enable them after validation.

### Duplicate detection

Both platforms identify users primarily by email address. If a user exists as both a requester and an agent in Freshservice (different records, same email), decide how to handle the merge before loading into HaloITSM.

### Deleted requester handling

Freshservice tickets may reference requesters who have since been deleted from the account. The API returns these tickets with a `requester_id` that resolves to `null` or to a deleted user record. Before loading into HaloITSM, decide on a default user (e.g., a "Migrated Records" placeholder account) to assign these tickets to — HaloITSM requires a valid `user_id` on every ticket.

### Inline HTML link rewriting

Article bodies, ticket descriptions, and conversation HTML may contain internal Freshservice URLs — article links, ticket references, inline images. All of these break after decommissioning. Your middleware needs a URL rewriting pass that maps old Freshservice URLs to new HaloITSM equivalents. This includes:
- `freshservice.com/a/solutions/articles/[id]` → new HaloITSM KB article URL
- `freshservice.com/a/tickets/[id]` → new HaloITSM ticket URL
- Inline image `src` attributes pointing to Freshservice CDN → HaloITSM hosted attachment URLs

### HaloITSM on-premise vs. cloud behavioral differences

On-premise HaloITSM instances differ from cloud in two relevant ways: (1) write throughput is bounded by host server specs rather than platform throttling, meaning both higher sustained throughput and unpredictable degradation under load are possible; (2) the instance's Swagger API documentation may reflect an older version than the cloud release, and some endpoint behaviors documented here (particularly OAuth token TTL and attachment size limits) may differ. Test your specific on-premise instance behavior during sample migration.

## Validation after migration

Data migration is only complete when the target data passes validation:

- **Record-count parity** — Total tickets, assets, users, and articles in HaloITSM should match Freshservice source counts by type.
- **Field-level spot checks** — Sample 50–100 records across each object type. Verify custom fields, statuses, priorities, and assignments mapped correctly.
- **Conversation integrity** — For sampled tickets, confirm all conversations (replies and notes) are present in correct chronological order with correct author attribution and correct private/public flag.
- **Attachment verification** — Confirm attachments are downloadable and linked to the correct ticket/conversation in HaloITSM.
- **CMDB relationship verification** — Spot-check that CI-to-CI and asset-to-ticket relationships survived the migration.
- **Referential integrity** — Every ticket should reference a valid agent, user, and team. No orphaned foreign keys.
- **Custom field type validation** — Confirm custom field values rendered correctly in HaloITSM's UI, not just that the write succeeded without error.

## Conversation extraction with checkpointing

Conversation extraction is the most API-intensive part of the migration. The pattern below extracts all conversations for a list of tickets with exponential backoff on rate limits and checkpoint recovery:

```python
import requests
import time
import json

BASE_URL = "https://yourdomain.freshservice.com/api/v2"
API_KEY = "your_api_key"

def extract_conversations(ticket_id, max_retries=5):
    conversations = []
    page = 1
    while True:
        for attempt in range(max_retries):
            resp = requests.get(
                f"{BASE_URL}/tickets/{ticket_id}/conversations",
                auth=(API_KEY, "X"),
                params={"page": page, "per_page": 100}
            )
            if resp.status_code == 429:
                wait = int(resp.headers.get("Retry-After", 60))
                time.sleep(wait + (2 ** attempt))  # exponential backoff
                continue
            resp.raise_for_status()
            break
        page_data = resp.json().get("conversations", [])
        if not page_data:
            break
        conversations.extend(page_data)
        page += 1
    return conversations

def migrate_with_checkpoint(ticket_ids, checkpoint_file="checkpoint.json"):
    try:
        with open(checkpoint_file) as f:
            completed = set(json.load(f))
    except FileNotFoundError:
        completed = set()
    
    for tid in ticket_ids:
        if tid in completed:
            continue
        convos = extract_conversations(tid)
        # ... transform and load into HaloITSM ...
        completed.add(tid)
        with open(checkpoint_file, "w") as f:
            json.dump(list(completed), f)
```

Checkpointing is critical because a 10,000-ticket extraction can take hours when rate-limited. Without it, a single failure at ticket 8,000 means restarting from scratch.

## Cutover and delta sync

Run the bulk historical load during the working days before go-live, while your team keeps working in Freshservice. Plan go-live for a Monday morning. Before the working day starts, freeze Freshservice and run a final **delta sync** of the records created or modified since the bulk extraction. That pass takes about 30 minutes, and agents start Monday in HaloITSM.

Delta sync scope by object type:

| Object | Delta sync method | Notes |
|---|---|---|
| Tickets | `updated_since` parameter on `/api/v2/tickets` | Most reliable; captures creates and edits |
| Problems | Full re-paginate and compare `updated_at` | No documented `updated_since` filter |
| Changes | `updated_since` parameter | Supported |
| Assets | `filter` on `updated_at` at `/api/v2/assets` | Date granularity; 30 per page, 40 pages max |
| Conversations | Re-fetch conversations for all tickets in delta window | No direct endpoint for changed conversations only |
| Agents / Users | Full re-paginate and diff against local snapshot | No time-window filter available |
| Knowledge base | Re-fetch and diff by `updated_at` | Manual comparison required |

Validate the delta results, then re-enable HaloITSM automations.

## The ClonePartner process

ClonePartner is an engineer-led migration service. We charge one fixed price per project, starting at $500 — no hourly billing, no per-record metering, no change orders. Pricing reflects data volume, object complexity, and the platforms involved.

Our process:

1. **Connect accounts**
2. **Migration checklist** — scope the mapping, identify edge cases
3. **Sample migration** — unlimited runs until the data is right
4. **Full migration**
5. **Delta sync** — catch records created during the migration window

A dedicated migration engineer owns each project — you work directly with one person.

There is no free demo migration. A free 30-minute scoping call produces a fixed-price proposal.

### Security and compliance

We maintain **SOC 2 Type II** and **ISO 27001** certifications (independently audited), alongside GDPR, CCPA, and HIPAA safeguards. Data is encrypted using AES-256 at rest and TLS 1.2+ in transit. Staging databases auto-purge no later than 30 days after project close. NDAs and DPAs are available on request; a BAA is required before handling any PHI.

For more on exporting your Freshservice data, see [How to Export Data from Freshservice: API Limits, Methods & Portability](https://clonepartner.com/blog/blog/how-to-export-data-from-freshservice-api-limits-methods-portability). For HaloITSM's export and portability story, see [How to Export Data from HaloITSM: API Limits, Methods & Portability](https://clonepartner.com/blog/blog/how-to-export-data-from-haloitsm-api-limits-methods-portability).

> Book a free 30-minute scoping call and get a fixed-price proposal for your Freshservice to HaloITSM migration — no hourly billing, no change orders.
>
> [Talk to us](https://clonepartner.com/talk-to-us?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## Frequently asked questions

### How long does a Freshservice to HaloITSM migration take?

The data migration itself typically completes within 1–5 business days, depending on data volume, complexity, and API rate limits. Small datasets under 10,000 records can finish in under 24 hours. This covers the data transfer only — not the broader HaloITSM implementation or configuration project, which is a separate effort.

### What are the Freshservice API rate limits for migration?

Freshservice rate limits are account-wide and per-minute: Starter 100 req/min, Growth 200, Pro 400, Enterprise 500. Sub-limits apply — List All Tickets is capped at 40, 70, 120, or 140 req/min depending on plan. Exceeding limits returns HTTP 429 with a Retry-After header.

### Does HaloITSM have a built-in Freshservice migration tool?

No. Neither platform offers a built-in migration tool for this specific path. A full-fidelity Freshservice migration requires API-to-API extraction and transformation via custom scripts or a managed migration service.

### What data is lost in a CSV-only Freshservice to HaloITSM migration?

A CSV-only migration loses ticket conversations, internal notes, attachments, CMDB relationships, service catalog structure, and ticket threading. Freshservice's UI export includes ticket metadata only. For full-fidelity migration, use the API.

### Can I migrate my Freshservice CMDB to HaloITSM?

Yes. Freshservice assets and configuration items can be extracted via the API and mapped to HaloITSM's asset model, preserving relationships and custom fields. Both platforms use a unified asset-CI model, making the mapping closer to 1:1 than migrations to platforms like ServiceNow.
