Skip to content

Migrating from Zendesk Sell to Microsoft Dynamics 365 Sales: A Technical Deep Dive

Moving from Zendesk Sell to Microsoft Dynamics 365 Sales means translating a unified data model into Dataverse’s relational structure. This guide shows how to migrate all the entities, relationships and historical records via their APIs for a seamless, zero-downtime transition.

Tejas Mondeeri Tejas Mondeeri · · 16 min read
Migrating from Zendesk Sell to Microsoft Dynamics 365 Sales: A Technical Deep Dive
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

With Zendesk Sell approaching its end of life (confirmed shutdown date: confirm directly with Zendesk support, as the timeline has shifted), many teams are now faced with the challenge of safely moving years of sales data, deals, and activities into a more scalable platform. Microsoft Dynamics 365 Sales, powered by the Microsoft Dataverse, offers a robust and extensible foundation for managing customer relationships, analytics, and automation. However, migrating from Zendesk Sell to Microsoft Dynamics 365 Sales isn't a one-click process. It requires careful planning, precise data mapping, and a deep understanding of both platforms' APIs. This guide walks you through a technical, step-by-step process of migrating from Zendesk Sell to Microsoft Dynamics 365 Sales to ensure a smooth, API-driven migration that preserves every entity, relationship, activity, and historical record.

Info

API versions and deprecation timelines: This guide references Dataverse Web API v9.2 and the Zendesk Sell Core API (api.getbase.com/v2). Both APIs evolve; verify against the official Zendesk Sell API reference and Dataverse Web API documentation before implementing. Zendesk Sell is approaching end of life — confirm your data export window directly with Zendesk support.

Pre-Migration Environment Checklist

Before writing a single API call, complete the following environment setup. Skipping these steps is the most common cause of mid-migration failures.

  1. Sandbox first. Always run the full migration against a Dataverse sandbox environment before touching production. Validate record counts, relationships, and BPF activation in the sandbox. Only promote to production after a clean sandbox run.
  2. Audit duplicate detection rules. Identify all active duplicate detection rules in your Dataverse org (Settings > Data Management > Duplicate Detection Rules). Decide in advance whether to disable them during the migration window or handle 412 responses explicitly in your script. Undecided = data loss risk.
  3. Audit field-level security profiles. Identify any FLS profiles applied to columns you plan to write to. The migrating service account's application registration must be assigned those profiles, or writes will silently fail with no error.
  4. Create a dedicated Dataverse solution. All custom columns and entity changes must be created inside a named solution, not as unmanaged customizations. This enables clean versioning and rollback.
  5. Verify Dataverse org timezone setting. Check whether your org is configured for UserLocal or TimeZoneIndependent timestamps before importing any activity records. See Step 8 for details.

Migration Approach: Script, ETL Tool, or Service?

Before diving into the API steps, decide which approach fits your migration.

Approach Best For Trade-offs
Custom API script (Python, Node, etc.) Engineers comfortable with REST APIs; full control over mapping logic Highest flexibility; also highest implementation cost and failure surface
ETL tool (Azure Data Factory, SSIS + KingswaySoft) Teams with existing Microsoft data platform investment ADF has native Dataverse connectors; KingswaySoft handles Dataverse batching well; both require configuration expertise
Commercial migration service Non-technical teams; tight timelines; large record volumes Reduces engineering burden; cost varies; verify the service handles polymorphic lookups and BPF activation

Volume thresholds as a rough guide:

  • Under ~5,000 records with minimal custom fields: a simple script is manageable.
  • 5,000–100,000 records with multiple pipelines and custom fields: an ETL tool or migration service reduces risk significantly.
  • Over 100,000 records, or migrations with large file attachments: a purpose-built service with batching, checkpointing, and retry logic is strongly recommended. Single-threaded scripts will saturate Zendesk's 10 RPS rate limit before completing and will fail without explicit backoff logic.

Data Mapping

The following table outlines the essential mapping required for a complete migration, including solutions for non-direct mapping scenarios.

Zendesk Sell Entities Microsoft Dynamics 365 Sales Entities Mapping Notes
User User (systemusers) Retrieve Zendesk User IDs via GET /v2/users. These IDs must be mapped to existing Microsoft Dynamics 365 User IDs (systemuserid or Microsoft Entra ID Object ID) for setting ownership and managing access.
Contact (is_organization: true) (Organization) Account (accounts) Retrieve filtered by is_organization=true. Map Zendesk name and address details to the Account entity.
Contact (is_organization: false) (Person) Contact (contacts) Retrieve filtered by is_organization=false. Map individual fields (first_name, last_name, email). Link to parent Account using the parentcustomerid_account lookup column.
Lead Lead (leads) Direct entity mapping. Use POST [URI]/leads. Leads are pre-qualification records and exist independently in Dynamics 365 — association to a Contact or Account happens at the point of qualification, not creation.
Pipelines & Stages Business Process Flow (BPF) or Status Reason (Option Sets) See the Pipeline Mapping Decision Rule below for when to use each approach.
Deal Opportunity (opportunities) Map primary fields like name, value, currency, and estimated_close_date. Link to the correct Account/Contact IDs using @odata.bind.
Product Product (products) Map core product details (name, sku, description). Zendesk's prices array requires mapping to Microsoft Dynamics 365 Price Lists/Price List Items, which is complex metadata setup.
Order and Line Item Sales Order (salesorders) and Order Product (salesorderdetails) Workaround: Zendesk explicitly uses separate Order and Line Item objects attached to a Deal. In Microsoft Dynamics 365 Sales, migrate these as Sales Orders and associate Line Items (Order Products) to the newly created Sales Order.
Task Task (tasks) Tasks are an Activity entity. Map using polymorphic lookups (e.g., regardingobjectid_lead_task, regardingobjectid_contact_task, regardingobjectid_account_task) to link to the correct parent record.
Call Phone Call (phonecalls) Calls are also an Activity entity. Map properties like summary, duration, phone_number, and made_at. Map Zendesk Call Outcomes (/v2/call_outcomes) to an appropriate custom Option Set in Microsoft Dynamics 365.
Notes Annotation (annotations or notes) Notes are saved as Annotation records in Dataverse. Map content and associate via the polymorphic lookup to the parent record (lead, contact, or deal).
Document Annotation (annotations) or SharePoint/OneDrive integration Zendesk provides expiring download_url links. Files must be downloaded and then uploaded to Microsoft Dynamics 365. Use Annotation records with the file content encoded if migrating files directly, or leverage built-in Microsoft Dynamics 365 Document Management integrations (SharePoint/OneDrive).
Custom Fields Custom Properties (Columns) Fetch custom field definitions (GET /v2/:resource_type/custom_fields). Recreate matching columns in Dataverse using metadata APIs, maintaining type consistency where possible (e.g., Zendesk number maps to Microsoft Dynamics 365 Decimal or Integer columns).

Pipeline Mapping Decision Rule

The mapping table above defers this decision intentionally — it is the hardest mapping problem in this migration and deserves its own treatment.

Zendesk Sell pipelines have stages with a likelihood (probability percentage) and a position (sort order). Dynamics 365 Sales offers two mechanisms for representing pipeline stages:

Use Business Process Flows (BPF) when:

  • Your sales process follows a defined, sequential path that reps must move through in order.
  • You need BPF-driven process guidance (UI step completion, required fields per stage).
  • The number of pipelines is small (BPFs are a metadata object; each pipeline becomes a separate BPF definition).

Use Status Reason (Option Sets) when:

  • Your pipelines are primarily reporting constructs, not process enforcement tools.
  • You have many pipelines or pipelines that vary significantly by deal type.
  • You want simpler migration logic: Status Reason values are just Option Set integers and can be written directly via the API without per-record BPF activation.

In practice: If you can model your stages as Status Reason values, do it. It is significantly simpler to migrate, audit, and maintain. BPFs are the right choice when your post-migration Dynamics 365 users will rely on the guided process UI — but they require per-record activation after Opportunity creation (see Step 7 warning), which adds complexity and a second API pass.

Migrate likelihood to the Opportunity's closeprobability field regardless of which approach you use.

Auth

Authentication for both platforms must be set up to ensure secure and uninterrupted data transfer.

Zendesk Sell Authentication

Access to the Zendesk Sell Core API requires authentication via a valid Access Token.

  • For migration scripts, it is typically easiest to generate a Single-User Access Token with an unlimited lifetime via the application dashboard, provided your user has administration management privileges.
  • The token must grant sufficient scopes, primarily read and write access, to cover all necessary resources.
  • All requests must include the token in the Authorization header: Authorization: Bearer $ACCESS_TOKEN.

Zendesk API URL Structure:

  1. https://api.getbase.com/v2/{resource}

Microsoft Dynamics 365 Sales (Dataverse) Authentication

The Dataverse Web API uses OData v4 and relies on OAuth 2.0 via Microsoft Entra ID (formerly Azure Active Directory) for authentication.

  1. Application Registration: Register a client application in your Microsoft Entra ID tenant. This registration defines the necessary permissions.
  2. Permissions and Scopes: The application must be granted the "Access Common Data Service as organization users" delegated permission, typically referenced by the user_impersonation scope, to act on behalf of the user running the migration.
  3. Token Acquisition: The client application uses the Microsoft Authentication Library (MSAL) to acquire an access token. This token is then included in every Dataverse API request header:
  4. Authorization: Bearer
  5. OData-MaxVersion: 4.0
  6. OData-Version: 4.0
  7. Accept: application/json

Dataverse Web API URL Structure: [Organization URI]/api/data/v9.2/{entitySetName}

The Migration Process

The migration must be executed in phases based on dependency constraints: migrate foundational data (Users, Pipelines, Custom Fields) first, followed by core entities (Accounts/Contacts, Leads, Products), and finally transactional records and activities (Deals, Orders, Tasks, Notes). Use batch processing where feasible in Dataverse to manage performance and respect API limits.

Step 1: Migrate Users (Owners)

Start by establishing the user mapping, as correct ownership is crucial for setting up records in Dataverse.

  1. Fetch Zendesk Users: Retrieve all users and their metadata.
  • Zendesk Endpoint: GET /v2/users
  1. Map IDs: Match the Zendesk id to the corresponding Microsoft Dynamics 365 systemuserid or Entra ID Object ID. This map will populate the _owninguser_value field on records in subsequent steps.

Step 2: Migrate Pipelines and Stages

These are structural elements that must exist before importing Deals.

  1. Fetch Zendesk Pipelines & Stages:
  • Zendesk Endpoints:
    • Pipelines: GET /v2/pipelines
    • Stages: GET /v2/stages?pipeline_id={id}
  1. Create Microsoft Dynamics 365 Pipelines (Configuration): Manually or programmatically create corresponding Pipelines and Stages in Dynamics 365 Sales using your chosen approach (BPF or Status Reason — see Pipeline Mapping Decision Rule above).
  2. Map Stage Metadata: Capture the numerical ID assigned to each stage in Microsoft Dynamics 365. The Zendesk likelihood (percentage probability) should be mapped to the Microsoft Dynamics 365 Opportunity closeprobability field.
Warning

Business Process Flow activation: Creating a Business Process Flow definition in Dynamics 365 is not sufficient. Each migrated Opportunity record requires the BPF to be explicitly activated at the record level. If you skip this step, records will appear in the pipeline UI without a valid BPF stage, causing display and automation failures. Activate BPFs per record programmatically after Opportunities are created in Step 7.

Step 3: Create Custom Fields (Columns)

Custom fields must be recreated as columns in Dataverse before data import.

  1. Fetch Zendesk Custom Field Definitions:
  • Zendesk Endpoint: GET /v2/:resource_type/custom_fields (for lead, contact, deal)
  1. Create Microsoft Dynamics 365 Custom Columns: Use the Metadata API to create matching columns (e.g., String, Decimal, Choice) on the corresponding Microsoft Dynamics 365 tables (account, contact, lead, opportunity).
  • Dataverse Endpoint (Example: Create Decimal Field): POST [URI]/EntityDefinitions(LogicalName='account')/Attributes
Warning

Dataverse solution layers: Custom columns created outside a Dataverse solution are unmanaged. Unmanaged customizations are ungoverned and cannot be cleanly deployed, versioned, or rolled back. Create all custom columns inside a dedicated solution before running the migration. This also makes cleanup straightforward if the migration needs to be re-run.

Step 4: Migrate Accounts and Contacts

This critical step separates Zendesk's unified Contact object into Microsoft Dynamics 365's distinct Account and Contact records.

  1. Migrate Accounts (Zendesk Contacts with is_organization: true):
  • Zendesk Endpoint: GET /v2/contacts?is_organization=true
  • Dataverse Endpoint (Create Account): Use POST [URI]/accounts. Store all Zendesk IDs and the resulting Microsoft Dynamics 365 Account IDs for later association.
  1. Migrate Contacts (Zendesk Contacts with is_organization: false):
  • Zendesk Endpoint: GET /v2/contacts?is_organization=false
  • Dataverse Endpoint (Create Contact): Use POST [URI]/contacts
  1. Establish Contact to Account Relationship: For contacts linked to an organization in Zendesk (contact_id field on the contact), establish the parent relationship in Microsoft Dynamics 365 using the primary contact field.

Dataverse Endpoint (Create Contact with Association): Perform a POST to /contacts using the @odata.bind annotation on the single-valued navigation property:

{

"firstname": "Mark",

"lastname": "Johnson",

"parentcustomerid_account@odata.bind": "accounts(<Microsoft Dynamics 365_ACCOUNT_GUID>)"

}

Warning

Ambiguous Contact records: A Zendesk Contact can have is_organization=true while also being the parent of person contacts. When you encounter a Contact record that is both an organization and directly linked to person contacts, migrate it as an Account first, then resolve the person contacts against that Account GUID. Do not attempt to create the person Contact records before the parent Account exists — the @odata.bind will fail with a missing GUID error.

Step 5: Migrate Leads

Leads map directly to the Microsoft Dynamics 365 Lead entity set.

  1. Fetch Zendesk Leads:
  • Zendesk Endpoint: GET /v2/leads
  1. Create Microsoft Dynamics 365 Leads:
  • Dataverse Endpoint: Use POST [URI]/leads. Link the lead owner using the mapped owninguser GUID, and map the status.

Step 6: Migrate Products, Orders, and Line Items

Migrate the product catalog before migrating orders, which depend on them.

  1. Migrate Products:
  • Zendesk Endpoint: GET /v2/products
  • Dataverse Endpoint (Create Product): Use POST [URI]/products.
  1. Migrate Orders:
  • Zendesk Endpoint (Retrieve Orders associated with a Deal): GET /v2/orders?deal_id={dealId}
  • Dataverse Endpoint (Create Sales Order): Use POST [URI]/salesorders. Link the new Sales Order to the corresponding Microsoft Dynamics 365 Opportunity (Deal) ID via @odata.bind.
  1. Migrate Line Items (Order Products):
  • Zendesk Endpoint (Retrieve Line Items): GET /v2/orders/:order_id/line_items
  • Dataverse Endpoint (Create Order Product): Use POST [URI]/salesorderdetails. This record links the Microsoft Dynamics 365 Product ID and specifies quantity/price, associating it back to the newly created Sales Order ID.

Step 7: Migrate Deals (Opportunities)

Deals tie together many previously migrated entities (Owner, Account, Contacts, Products).

  1. Fetch Zendesk Deals:
  • Zendesk Endpoint: GET /v2/deals
  1. Create Microsoft Dynamics 365 Opportunities:
  • Dataverse Endpoint (Create Opportunity): UsePOST [URI]/opportunities. Map the primary associated Account/Contact IDs using the @odata.bind annotation.
  1. Handle Loss/Unqualified Reasons: Retrieve the text reasons from Zendesk (e.g., GET /v2/loss_reasons/:id) and map them to the Opportunity's statuscode or a dedicated custom field in Microsoft Dynamics 365.

Step 8: Migrate Activities (Tasks, Calls, Notes)

Activities often relate to Leads, Contacts, or Deals (Opportunities). In Dataverse, all these map to the polymorphic regardingobjectid lookup.

  1. Tasks:

Zendesk Endpoint: GET /v2/tasks 2. Dataverse Endpoint (Create Task): Use POST [URI]/tasks. Link the Task to the parent record (Lead, Contact, or Opportunity) using the appropriate navigation property and @odata.bind:

{ "subject": "Contact Tom", "regardingobjectid_contact_task@odata.bind": "contacts(<Microsoft Dynamics 365_CONTACT_GUID>)"}

  1. Calls:
  • Zendesk Endpoint: GET /v2/calls
  • Dataverse Endpoint (Create Phone Call): Use POST [URI]/phonecalls. Map fields like summary, duration, and call time (made_at field in Zendesk maps to Dataverse activity time field).
  1. Notes:
  • Zendesk Endpoint: GET /v2/notes
  • Dataverse Endpoint (Create Annotation/Note): Use POST [URI]/annotations. Map the note content and link using the appropriate regardingobjectid_ lookup.
Warning

Timezone normalization: Zendesk Sell stores timestamps in UTC. Dataverse time handling depends on your organization's timezone settings and the column type (UserLocal vs. TimeZoneIndependent). If your Dataverse org is configured for UserLocal, timestamps written as UTC may be shifted on read. Before bulk-importing activity timestamps (made_at, created_at, updated_at), confirm your Dataverse org timezone setting and normalize all source timestamps accordingly. Silent timestamp drift is one of the harder data quality issues to detect post-migration.

Step 9: Migrate Documents and Files

  1. Fetch Zendesk Documents: Retrieve the metadata for documents attached to resources.
  • Zendesk Endpoint: GET /v2/documents?resource_type={type}&resource_id={id}
  1. Download Files: Use the temporary download_url returned by Zendesk to fetch the file content.
  2. Upload to Dataverse: Create Annotation records (annotations) in Microsoft Dynamics 365, setting the file content in the documentbody field and associating the annotation back to the parent record (Account, Contact, Opportunity) using the regardingobjectid.

Pagination

Zendesk Sell uses cursor-based pagination, not offset pagination. Every list endpoint returns a meta.next_page_url (or equivalent cursor token) when additional records exist. Migration scripts that stop after the first page will silently miss records — there is no error, just incomplete data.

For each entity fetch, your loop must:

  1. Request the first page.
  2. Check for a next cursor or next_page_url in the response metadata.
  3. Continue fetching until no next page is returned.

Do not assume a fixed page size corresponds to your total record count. Always drive pagination from the API response, not from a pre-calculated expected count.

Handling Zendesk Soft-Deleted Records

Zendesk Sell marks deleted records as inactive rather than removing them from the API immediately. Depending on the endpoint and your account configuration, soft-deleted records may appear in list responses.

Before migrating each entity type, decide explicitly:

  • Exclude soft-deleted records (recommended for most migrations): filter on is_deleted=false or equivalent status fields where available.
  • Include soft-deleted records as archived: if your compliance or audit requirements demand a full history, create a corresponding Dynamics 365 record in an inactive/cancelled state.

Not making this decision before the migration runs means you may import deleted contacts, leads, or deals alongside live records, which corrupts your pipeline data and inflates record counts.

Common Migration Failures and How to Resolve Them

The steps above describe the happy path. The following failure modes appear consistently across Dataverse migrations and are worth handling explicitly before they surface in production.

409 Conflict on Create

A 409 response means a record with matching key fields already exists. This typically occurs when a migration run is retried after a partial failure. Rather than treating 409 as a hard error, catch it, extract the existing record GUID from the error response, and add it to your ID mapping table. Then continue. Do not re-create the record.

Failed @odata.bind Due to Missing GUID

If an @odata.bind reference points to a GUID that does not exist in Dataverse — because the parent record failed silently or was skipped — the create request will fail with a 400 error referencing an invalid navigation property value. The fix is to validate your ID mapping table before submitting dependent records. Any source ID without a resolved target GUID should be flagged and queued for manual review, not silently dropped.

Dataverse Duplicate Detection Rules

Dataverse has native duplicate detection rules that run on record creation. When triggered, they silently block the import — the API returns a 412 Precondition Failed response with a DuplicateRule error code, not a 200 or 201. If you do not handle this response code explicitly, your migration script will treat blocked records as successful. Before starting bulk import, either disable duplicate detection rules temporarily (with appropriate sign-off) or handle 412 responses by logging the blocked record for post-migration manual review.

Field-Level Security Blocking Writes

Dataverse field-level security profiles can silently block writes to specific columns even when the migrating service account has full entity-level access. If custom columns are returning no error but values are not populating, check whether field-level security profiles are applied to those columns. The migrating application registration must be assigned the relevant field security profile, or column-level restrictions must be temporarily relaxed during the migration window.

Orphaned regardingobjectid Lookups

If an activity (Task, Call, Note) is created with a regardingobjectid pointing to a record that was subsequently deleted or was never successfully created, the activity will exist in Dataverse but be unreachable from the parent record's timeline. Validate regardingobjectid targets exist before creating activity records, and run a post-migration query to identify any annotations or tasks where the regardingobjectid GUID resolves to no active record.

Post Migration and Best Practices

Completing the technical transfer is only half the battle. Thorough validation and a solid plan for cutover are necessary for a seamless transition.

Data Validation and Reconciliation

  1. Count Verification: Confirm record counts for core entities (Accounts, Contacts, Leads, Opportunities) match the counts retrieved from Zendesk Sell to ensure no loss occurred.
  2. Spot Checks: Manually verify complex records (e.g., an Opportunity linked to an Account, multiple Contacts, and containing activities) to ensure all relationships were correctly established using the new Microsoft Dynamics 365 GUIDs.
  3. Owner and Role Verification: Confirm that ownership fields (owninguser) are correctly populated. Also verify that migrated users have been assigned the appropriate Dynamics 365 security roles. Record ownership does not automatically grant the owning user access — security role assignment is a separate step performed in Settings > Security > Users.

Rollback and Idempotency

Design every migration step to be safe to re-run from a checkpoint. The key mechanism is alternate keys and upsert:

  • Alternate keys: Define an alternate key on each Dataverse table using the Zendesk source ID (e.g., a custom string column new_zendeskid). This allows you to use the Upsert pattern — PATCH [URI]/accounts(new_zendeskid='ZD-12345') — which creates the record if it does not exist and updates it if it does. This makes every step idempotent.
  • Checkpoint logging: After each successful batch, write the last-processed Zendesk record ID to a log. On re-run, skip records below that checkpoint. This prevents duplication without requiring a full re-query of Dataverse.
  • Rollback scope: If a step fails mid-run (e.g., Step 7 Opportunities partially created), do not manually delete partially created records. Instead, re-run from the checkpoint using upsert. Manual deletes risk leaving orphaned activity records that reference GUIDs you just removed.

Handling Deltas and Cutover

For a zero-downtime cutover, capture and migrate changes that occurred during the bulk migration phase:

  1. Initial Snapshot Timestamp: Record the timestamp (last_updated from Zendesk) just before the bulk migration began.
  2. Delta Synchronization: After the bulk migration, perform a final fetch from Zendesk using the updated_at filter to retrieve only recently modified or new records.
  3. Upsert in Microsoft Dynamics 365: Use the Dataverse PATCH method with alternate keys or the Upsert pattern to update existing records or create new ones identified in the delta sync. The Upsert functionality checks if the record exists based on keys; if so, it updates (PATCH); otherwise, it creates (POST).

Managing API Rate Limits

Migrating large datasets requires respecting API limits imposed by both platforms:

  • Zendesk Rate Limits: Zendesk Sell API limits are typically high (up to 36,000 requests per hour, or 10 requests per second per token). Verify current limits against the Zendesk Sell API rate limit documentation before running bulk operations, as these figures can change.
  • Dataverse Limits: While Dataverse typically handles requests efficiently, standard API service protection limits apply. Note that aggregate query operations using $count are subject to a 50,000 record cap — use these only for validation spot checks, not as the primary mechanism for driving pagination or migration logic.
  • Batch Operations: Use the Dataverse $batch endpoint for executing up to 1,000 individual operations (like creating activities or updating records) within a single HTTP request, significantly improving efficiency and managing connection overhead.
    • Dataverse Endpoint (Batch Processing): POST [URI]/api/data/v9.2/$batch
  • Throttling Implementation: If you encounter HTTP 429 Too Many Requests errors from either API, implement logic to pause the processing immediately, respecting any Retry-After header provided in the response before attempting a retry. This ensures that your migration script does not trigger service protection limits.

More from our Blog