Skip to content

Salesflare to Copper Migration: A Technical Guide

A technical guide to migrating from Salesflare to Copper. Covers object mapping, API rate limits, email sync traps, import order, and edge cases that break most migrations.

Raajshekhar Rajan Raajshekhar Rajan · · 22 min read
Salesflare to Copper Migration: A Technical Guide
TALK TO AN ENGINEER

Planning a migration?

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

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

Salesflare to Copper Migration: A Technical Guide

Migrating from Salesflare to Copper is a schema-translation problem, not a CSV shuffle. Salesflare builds its value on automated data capture — scraping email signatures, social profiles, and meeting data to construct a timeline without manual entry. Copper relies on a tight native integration with Google Workspace to achieve similar automation from the Gmail sidebar. Both platforms target small-to-medium B2B sales teams, but their underlying schemas handle email tracking, contact associations, and activity logging in fundamentally different ways. A basic CSV export will flatten relationships, duplicate contacts, and permanently orphan historical sales activities.

This guide covers the object-mapping decisions you need to make, API constraints on both sides, the three viable migration methods and their trade-offs, the exact order of operations, and the edge cases that quietly corrupt your data if you ignore them.

API versions referenced: Copper REST API v1 (https://api.copper.com/developer_api/v1/). Salesflare REST API (https://api.salesflare.com), unversioned as of the last update to this guide.

For a platform-agnostic migration framework, see The Ultimate CRM Data Migration Checklist.

Why Teams Move from Salesflare to Copper

The decision usually comes down to a few structural factors:

  • Google Workspace commitment. Copper was built as a Google Workspace-native CRM. If your team lives in Gmail, Google Calendar, and Google Drive, Copper's Chrome extension and sidebar integration reduce context-switching more than Salesflare's IMAP-based email tracking.
  • Lead qualification workflow. Copper has a dedicated Leads object that sits upstream of People and Opportunities, giving teams a formal pre-qualification stage. Salesflare has no separate Lead entity — unqualified contacts live alongside qualified ones in the same Contacts list.
  • Project tracking post-sale. Copper supports Projects and Tasks as distinct record types, giving account management teams a native workspace. Salesflare's task system is lighter and lacks a project container.
  • Scaling beyond Salesflare's ceiling. Salesflare is designed for B2B teams of roughly 5–50 people. It does not support custom objects, territory management, or complex approval workflows. Teams outgrowing that envelope sometimes move to Copper's Professional or Business tiers for deeper reporting and workflow automation.
Info

Tier check before migrating: Copper's Professional and Business tiers differ in feature access — including workflow automation availability and API access scope. Confirm which tier your team is provisioning before migration, as some features referenced in this guide (workflow rules, certain custom field types) are tier-gated.

How Salesflare and Copper Model Data Differently

Salesflare's data model centers on four core entity types: Contacts (individual people), Accounts (companies), Opportunities (deals), and Tasks. Salesflare does not support custom objects. Its distinguishing feature is automated data enrichment — it pulls contact details, company information, and interaction history from connected email accounts, calendars, and social profiles without manual entry. Activities like emails, meetings, and calls are first-class citizens that automatically link to multiple records based on email addresses.

Copper's data model uses six core record types: People, Companies, Opportunities, Leads, Tasks, and Projects. Like Salesflare, Copper does not support custom objects. Its distinguishing feature is deep Google Workspace integration — email logging, calendar sync, and contact creation all happen from the Gmail sidebar via a Chrome extension and backend Google integration, rather than IMAP scraping.

The architectural gap between the two platforms dictates your migration strategy. You cannot map Salesflare's automated activity feed directly into Copper's database without understanding how Copper handles Google Workspace sync.

Salesflare Entity Copper Entity Key Differences
Contacts People Salesflare auto-enriches from email signatures; Copper uses Gmail integration. Different default field sets.
Accounts Companies Salesflare uses domain as account identifier; Copper uses email_domain as dedup key.
Opportunities Opportunities Both are pipeline-based. Stage names won't match — must be remapped.
Tasks Tasks Salesflare tasks link to accounts/contacts; Copper tasks can link to any record type including Leads and Projects.
Internal Notes Activities (Note type) User-generated context. Map to Copper's Activity endpoint.
(no equivalent) Leads Salesflare has no Lead object. Decide pre-migration which Contacts become Leads vs People.
(no equivalent) Projects No Salesflare equivalent. Projects are a Copper-only construct.

Object-by-Object Mapping: Salesflare → Copper

Contacts → People

Salesflare Contact records contain names, email addresses, phone numbers, social profile URLs, tags, positions (with role and organisation), and custom fields. Copper People records expect a similar shape, but the field names differ:

Salesflare Field Copper Field Notes
firstname / lastname First Name / Last Name Copper combines into Full Name on display
email Email Copper uses email as a dedup key for People
phone_numbers Phone Number Copper supports multiple phones via separate CSV columns (e.g., "Work Phone", "Mobile Phone")
tags Tags Each tag needs its own column in CSV, headed "Tag". Copper tags have a 50-character limit.
positions [].role Title Flatten to a single title string; Copper's Title field has a 255-character limit
positions [].organisation Company Name Used to link People to Companies during import
addresses Address fields Copper expects Street, City, State, Postal Code, Country in separate columns
Custom fields Custom fields Must be pre-created in Copper with matching data types

Data quality threshold: If more than 5% of your Salesflare contacts lack an email address, deduplicate and clean before exporting. Copper People records imported without an email address cannot be deduplicated later — they accumulate as unresolvable duplicates.

Info

Deduplication key: Copper uses the email address as the primary dedup key for People records. If you import a Person with an email that already exists, Copper will update the existing record rather than create a duplicate. Clean your Salesflare contact list for duplicate emails before export.

Accounts → Companies

Salesflare Account records map to Copper Company records. The critical difference is the dedup key: Copper uses the email domain to deduplicate Companies. If two Company records share the same domain, Copper treats them as the same entity.

Salesflare Accounts often contain auto-enriched data like social links, employee count, and industry. These have no default Copper equivalents — map them to corresponding custom fields.

Salesflare Field Copper Field Notes
name Company Name Direct mapping
domain Email Domain Primary dedup key in Copper
phone_numbers Phone Number Same multi-column approach as People
addresses Address fields Separate columns per address component
tags Tags Same tag-per-column format; 50-character limit per tag
Custom fields Custom fields Pre-create in Copper; match data types

Copper's Enrich feature interacts with domain data: Once Google Workspace is connected, Copper attempts to auto-enrich Company records using domain-derived signals. Companies imported without a valid email domain will not enrich. Standardize domain formatting (example.com, not www.example.com or https://example.com) before migration.

Opportunities → Opportunities

Both CRMs model deals as Opportunities tied to a pipeline with stages. You must create your pipeline and stages in Copper before importing, and the stage names in your import data must exactly match the stage names configured in Copper.

Salesflare Field Copper Field Notes
name Opportunity Name Direct mapping
value Monetary Value Check currency formatting; Copper uses the account's default currency
stage Pipeline Stage Must match Copper stage names exactly — case-sensitive
close_date Close Date Copper accepts DD/MM/YYYY or MM/DD/YYYY — must be consistent within a single file
assignee Owner Map to a valid Copper user email; invalid owner emails cause record rejection
Related contact Primary Contact Link via email address or person name
Related account Company Name Link via company name or domain

Tasks and Notes

Salesflare Tasks map directly to Copper Tasks. In Salesflare, tasks associate with accounts or contacts. In Copper, tasks can link to any record type including Leads and Projects. Each task must be associated with a parent record and an assignee (User) in Copper.

Salesflare Internal Notes map to Copper Activity records (Type: Note). These map cleanly to Copper's Activity endpoint (POST /v1/activities), but each note must be explicitly linked to a parent Person, Company, or Opportunity. Copper requires strict parent-child relationships — a note without a parent link is silently dropped.

The Leads and Projects Gap

Salesflare has no Lead object. Before migration, decide which Salesflare Contacts should enter Copper as Leads (pre-qualification) versus People (qualified contacts). This classification doesn't exist in your source data — it's a business decision you need to make explicitly and flag in your export before running any transformation scripts.

Copper Projects have no Salesflare equivalent. If you need project tracking post-migration, you'll build that from scratch in Copper.

Custom Fields: Types, Scope, and Character Limits

Both platforms support custom fields on core objects. Salesflare uses the naming convention custom__fieldname (double underscore) in API responses. Copper supports the following custom field types: Checkbox, Currency, Date, Dropdown, Float, MultiSelect, Percentage, String, Text, and URL — each field is scoped to specific record types (lead, person, opportunity, company, project, task).

Known character limits in Copper beyond tags:

  • Tags: 50 characters per tag
  • Title (People): 255 characters
  • Opportunity Name: 255 characters
  • String custom fields: 255 characters
  • Text custom fields: up to 32,000 characters

Salesflare free-text fields that exceed 255 characters must map to Copper's Text type (not String) to avoid silent truncation.

Warning

Create all custom fields in Copper before importing. Copper's import tool can only map to fields that already exist. If you upload a CSV with columns that don't match an existing Copper field, those columns will be silently ignored or flagged with a red underline during the mapping step. You cannot create custom fields during import.

The Historical Email Sync Trap

Historical email duplication is the most common failure mode in a Salesflare to Copper migration, and most guides don't mention it.

Salesflare automatically logs emails from connected accounts. When you export Salesflare data, those email logs are included in the activity timeline. When you provision Copper and connect your team's Google Workspace accounts, Copper will automatically sync recent email history natively from Gmail — typically covering the previous 30 days of activity, though the exact sync window depends on account configuration.

If you migrate Salesflare's email logs as activities into Copper while simultaneously connecting the same Google Workspace accounts, you will double-log every email that falls within Copper's automatic sync window.

The solution:

  1. Identify the exact date you will connect Google Workspace to Copper.
  2. Filter your Salesflare email export to exclude any emails that fall within Copper's automatic Gmail sync window (use a conservative buffer — exclude the prior 45 days to be safe).
  3. Migrate only older emails into Copper as Activity records using a custom activity type called "Legacy Email" so they're clearly distinguished from natively-synced emails.

This approach preserves your full email history without duplication. The "Legacy Email" activity type lets your team immediately see which emails were migrated versus which were captured by Copper natively.

What Happens to Salesflare's Auto-Enriched Data?

Salesflare automatically pulls contact details, company info, and interaction timelines from connected email accounts and social profiles. Most of this enriched data exports via CSV or API. But there are caveats:

  • Email interaction history (opens, clicks, threads) does not transfer. Copper rebuilds interaction history from Gmail once connected, but only going forward from the connection date. Historical email timelines from Salesflare are lost unless you export them as activity logs via the API and import them with the "Legacy Email" strategy described above.
  • Social profile URLs may export from Salesflare but have no default field in Copper. You'll need a custom URL field in Copper or accept losing that data.
  • Auto-detected company data (employee count, industry) that Salesflare enriched automatically will need corresponding custom fields in Copper to preserve.
  • Copper's Enrich feature will attempt to re-enrich People and Company records after Google Workspace is connected, but only for records that have valid email addresses or domains. Records migrated without these identifiers will not enrich.

The practical approach: export everything from Salesflare, map what Copper accepts natively, and create custom fields for anything you can't afford to lose before running any import.

API Constraints That Shape Your Migration

The API rate limits on both sides determine whether you can run a fast automated migration or need to throttle carefully.

Salesflare API (source)

Salesflare provides a REST API at api.salesflare.com with endpoints for Contacts (/contacts), Accounts (/accounts), Opportunities (/opportunities), Tasks (/tasks), and Custom Fields (/customfields/{entity}). The API supports pagination via limit and offset parameters and sorting via order_by. Authentication uses an API key passed as Authorization: Bearer {api_key} in the request header.

Salesflare's API documentation does not publish a specific requests-per-minute rate limit. Implement exponential backoff on 429 responses and keep concurrent requests modest — start at 2–3 per second and adjust based on observed throttling behavior.

For extraction, use GET /contacts?limit=250&offset=0 and increment offset by limit until the response returns fewer records than the limit value.

Copper API (target)

Copper's REST API v1 has a published rate limit of 180 requests per minute on a rolling window basis, returning HTTP 429 when exceeded. Bulk API endpoints carry an additional limit of 3 requests per second. Source: Copper Developer Documentation.

Authentication requires three headers on every request:

  • X-PW-Application: developer_api
  • X-PW-AccessToken: {your_api_token}
  • X-PW-UserEmail: {email_of_token_owner}

Copper does not return rate-limit response headers showing remaining quota (no X-RateLimit-Remaining equivalent), so you must track request counts client-side. This means you cannot dynamically adjust speed mid-batch — you need a fixed throttle from the start.

Pagination uses page_size (max 200) and page_number parameters on list endpoints.

Constraint Salesflare (Source) Copper (Target)
Auth method Authorization: Bearer {key} X-PW-Application, X-PW-AccessToken, X-PW-UserEmail headers
Rate limit Undocumented; use exponential backoff 180 requests/min (rolling window)
Bulk API limit N/A 3 requests/sec
Pagination limit / offset page_size (max 200) / page_number
Rate-limit headers returned Not documented Not returned
Max records per CSV batch N/A (use API) 10,000 records; 3MB file size limit

Throughput math: At 180 requests per minute with one record per request, writing 10,000 People records takes a minimum of 56 minutes under ideal conditions. With retry overhead from 429 responses (even with exponential backoff), expect 75–90 minutes for a clean 10,000-record write. For datasets of 50,000+ records across all object types, plan for 8–12 hours of API write time.

Exponential Backoff Implementation

A naive loop pushing thousands of records will hit 429 errors within the first minute. The following Python implementation handles rate limiting with exponential backoff against the Copper API:

import time
import requests
 
COPPER_BASE_URL = "https://api.copper.com/developer_api/v1"
 
COPPER_HEADERS = {
    "X-PW-Application": "developer_api",
    "X-PW-AccessToken": "your_api_token_here",
    "X-PW-UserEmail": "your_email@domain.com",
    "Content-Type": "application/json"
}
 
def copper_post(endpoint, payload, retries=5):
    """
    POST a record to Copper API with exponential backoff on 429.
    
    Args:
        endpoint: API path, e.g. "/people" or "/companies"
        payload: dict of record data
        retries: max retry attempts on rate limit
    
    Returns:
        dict: Copper API response body
    
    Raises:
        Exception: if max retries exceeded or non-429 error
    """
    url = f"{COPPER_BASE_URL}{endpoint}"
    
    for attempt in range(retries):
        response = requests.post(url, json=payload, headers=COPPER_HEADERS)
        
        if response.status_code == 429:
            sleep_time = (2 ** attempt) + 1  # 2, 3, 5, 9, 17 seconds
            print(f"Rate limited on attempt {attempt + 1}. Sleeping {sleep_time}s.")
            time.sleep(sleep_time)
            continue
        
        if response.status_code == 422:
            # Validation error — log and skip, don't retry
            print(f"Validation error for payload: {payload}")
            print(f"Copper response: {response.json()}")
            return None
            
        response.raise_for_status()
        return response.json()
    
    raise Exception(f"Max retries exceeded for {endpoint}")
 
 
def copper_search(endpoint, search_payload, retries=5):
    """
    Search Copper records (POST to search endpoint).
    Used to look up records by Salesflare_ID custom field.
    """
    url = f"{COPPER_BASE_URL}{endpoint}/search"
    
    for attempt in range(retries):
        response = requests.post(url, json=search_payload, headers=COPPER_HEADERS)
        
        if response.status_code == 429:
            sleep_time = (2 ** attempt) + 1
            time.sleep(sleep_time)
            continue
            
        response.raise_for_status()
        return response.json()
    
    raise Exception(f"Max retries exceeded for {endpoint}/search")

Managing Legacy IDs

To reconstruct relationships in Copper, you need a way to reference the original Salesflare IDs. Create a String custom field in Copper on Company, Person, and Opportunity objects called Salesflare_ID before running any migration.

When you insert a Company into Copper, store its original Salesflare Account ID in the Salesflare_ID field. When you later insert a Person that belongs to that Company, your script searches Copper for the Company with that specific Salesflare_ID value to retrieve the new Copper Company ID for the association.

This legacy ID strategy also enables rollback: if your migration fails validation after inserting records, you can search Copper for all records where Salesflare_ID is populated and bulk-delete them, then re-run from a clean state.

Three Migration Methods Compared

Method 1: CSV Export → Manual Import

Best for: Teams with fewer than 5,000 total records across all objects, no complex custom field mapping, and no requirement to transfer activity history.

Export Contacts, Accounts, and Opportunities as separate CSVs from Salesflare. Reformat column headers to match Copper's expected field names. Create custom fields in Copper first. Import in dependency order (Companies before People before Opportunities).

Salesflare export path: Settings → Data → Export. The UI export generates CSVs for Contacts and Accounts. Opportunities export separately. Note that the UI export may not include all custom field data — verify against the API export before relying on CSV output for custom field values.

Trade-offs:

  • Free and requires no code
  • Loses relational links between objects — relies on Copper's name/email matching logic to re-link records, which fails when names or emails are inconsistent
  • Activity history, email logs, and notes do not transfer via CSV
  • Multi-select custom fields require semicolon-separated values with no spaces (option1;option2;option3) — a leading space creates a new option permanently
  • CSV imports are limited to 3MB file size and 10,000 records per batch
  • Copper's "Connect" fields (which link records to other records) cannot be populated via CSV import — this is the single strongest reason to prefer API migration for relational data

Method 2: API-to-API Scripted Migration

Best for: Teams with more than 5,000 records, complex custom field mappings, or a requirement to preserve relational links (Contact → Account → Opportunity chains).

Write a script that reads from the Salesflare API and writes to the Copper API, maintaining object relationships by creating records in dependency order and storing ID mappings in a local lookup table.

# Full migration script structure with real endpoints
 
import time
import json
 
# -----------------------------------------------
# STEP 1: Extract and map custom field definitions
# -----------------------------------------------
 
def get_salesflare_custom_fields(entity):
    """entity: 'contacts', 'accounts', or 'opportunities'"""
    return salesflare_get(f"/customfields/{entity}")
 
def create_copper_custom_field(name, data_type, record_type):
    """
    data_type options: 'String', 'Text', 'Float', 'Date', 
                       'Dropdown', 'MultiSelect', 'Checkbox', 
                       'Currency', 'Percentage', 'URL'
    record_type options: 'person', 'company', 'opportunity', 
                         'lead', 'project', 'task'
    """
    payload = {
        "name": name,
        "data_type": data_type,
        "available_on": [record_type]
    }
    return copper_post("/custom_field_definitions", payload)
 
# -----------------------------------------------
# STEP 2: Migrate Accounts → Companies
# -----------------------------------------------
 
account_id_map = {}  # {salesflare_account_id: copper_company_id}
 
def migrate_companies():
    offset = 0
    limit = 250
    
    while True:
        accounts = salesflare_get(f"/accounts?limit={limit}&offset={offset}")
        if not accounts:
            break
        
        for account in accounts:
            payload = {
                "name": account["name"],
                "email_domain": account.get("domain", ""),
                "custom_fields": [
                    {"custom_field_definition_id": SALESFLARE_ID_FIELD_ID,
                     "value": str(account["id"])}
                ]
            }
            result = copper_post("/companies", payload)
            if result:
                account_id_map[account["id"]] = result["id"]
                time.sleep(0.34)  # ~3 requests/sec ceiling
        
        offset += limit
        if len(accounts) < limit:
            break
 
# -----------------------------------------------
# STEP 3: Migrate Contacts → People
# -----------------------------------------------
 
contact_id_map = {}  # {salesflare_contact_id: copper_person_id}
 
def migrate_people():
    offset = 0
    limit = 250
    
    while True:
        contacts = salesflare_get(f"/contacts?limit={limit}&offset={offset}")
        if not contacts:
            break
        
        for contact in contacts:
            # Resolve company link via ID map
            company_ids = []
            for position in contact.get("positions", []):
                sf_account_id = position.get("account_id")
                if sf_account_id and sf_account_id in account_id_map:
                    company_ids.append({"id": account_id_map[sf_account_id]})
            
            payload = {
                "name": f"{contact.get('firstname','')'} {contact.get('lastname','')}".strip(),
                "emails": [{"email": e, "category": "work"} 
                           for e in contact.get("email", [])],
                "company_ids": company_ids[:1],  # Copper People link to one primary company
                "custom_fields": [
                    {"custom_field_definition_id": SALESFLARE_ID_FIELD_ID,
                     "value": str(contact["id"])}
                ]
            }
            result = copper_post("/people", payload)
            if result:
                contact_id_map[contact["id"]] = result["id"]
                time.sleep(0.34)
        
        offset += limit
        if len(contacts) < limit:
            break

Trade-offs:

  • Preserves relational links between all record types
  • Handles custom fields programmatically including type conversion
  • Requires 2–5 days of engineering time for a clean, validated implementation
  • Must respect Copper's 180 req/min limit — build in fixed throttling (0.34s sleep between requests) and retry logic
  • You own error handling, idempotency, and rollback strategy — use the Salesflare_ID field for all three

Method 3: Managed Migration Service

Best for: Teams that cannot afford migration errors, lack engineering bandwidth, or need activity and note history preserved alongside structured records.

Trade-offs:

  • Fastest time-to-completion (typically 3–7 business days for datasets under 50,000 records)
  • Handles edge cases — deduplication, encoding issues, date format normalization, multi-select field formatting, historical email deduplication
  • Costs money, but eliminates the risk of a broken migration corrupting live CRM data
  • Best option when the team needs to keep selling during the transition with zero downtime

When to use a managed service (objective thresholds):

  • More than 10,000 total records across all objects
  • More than 20 custom fields requiring type mapping
  • Activity history (email logs, call records, meeting notes) must transfer — not just contacts and deals
  • More than 5% of contacts lack email addresses (high dedup risk)
  • Sales team cannot pause operations during the migration window

What Is the Correct Order of Operations?

Relational data must be migrated in a strict hierarchy. For API-based migrations, importing a child record before its parent causes the API to either reject the payload or create an orphaned record.

1. Users, Custom Fields, Pipelines, and Stages

Provision all users in Copper first. You cannot assign an Opportunity to a sales rep if their user ID doesn't exist in the target system. Create all custom fields (including the Salesflare_ID String field on Company, Person, and Opportunity), Pipelines, and Stages. Stage names are case-sensitive and must exactly match what you'll reference during import.

2. Companies (Salesflare Accounts)

Extract Accounts from Salesflare. Clean and standardize website URLs and email domains — Copper uses domains for native enrichment and deduplication. Use the format example.com, not www.example.com. Push to POST /v1/companies, storing the Salesflare-to-Copper ID map.

3. People (Salesflare Contacts)

Extract Contacts. Map each contact's account_id (from Salesflare positions []) to the corresponding Copper company_id using the ID map from Step 2. Push to POST /v1/people.

4. Opportunities

Extract Opportunities. Map Pipeline ID and Stage ID to Copper's pipeline configuration. Associate each Opportunity with the primary Copper Company and relevant Copper People using the stored ID maps. Push to POST /v1/opportunities.

5. Tasks and Notes

Extract Tasks and Internal Notes. Map creator_id to the correct Copper User ID. Attach each activity to the correct parent record type and ID. Push Notes to POST /v1/activities with type: {id: 0} (Note type in Copper's activity type system).

6. Historical Email Activities

Filter email logs from the Salesflare export to exclude anything within Copper's Gmail sync window (use a 45-day buffer). Import remaining historical emails as Activity records with a custom "Legacy Email" type. Push to POST /v1/activities.

Info

For CSV imports, Copper uses name and email matching to resolve cross-object links — you can import People with a Company Name column and Copper will attempt to match. But for reliable relational integrity at scale, the API approach with explicit ID mapping is strongly preferred. CSV matching logic fails silently when company names differ by even one character.

Reading Copper's Import Error Log

After every CSV import, Copper generates a downloadable error report. Access it from Settings → Imports → [most recent import] → Download Error File.

The error CSV contains one row per failed record with an Error column. Common error values and fixes:

Error Message Cause Fix
Invalid email format Email address fails validation Strip whitespace, check for double @ symbols
Stage not found Stage name doesn't match Copper pipeline exactly Check case sensitivity; re-export with exact stage name
User not found Owner email doesn't match a Copper user Pre-provision all users; map departed reps to active user
Company not found Company Name column doesn't match existing record Import Companies first; check for trailing spaces
Custom field not found CSV column references field that doesn't exist Create the custom field in Copper before re-importing
Value exceeds maximum length Field value exceeds character limit Truncate to 255 chars for String fields; use Text type for longer content

Always process the error log before considering an import complete. Copper counts a batch as "successful" even if 30% of records failed — the success confirmation refers to the batch processing completing, not all records being created.

Rollback Strategy

If your migration fails validation — records created with wrong stage assignments, broken company links, or corrupted custom field values — the Salesflare_ID custom field enables a clean rollback:

def rollback_migration(entity_type, copper_endpoint):
    """
    Find all Copper records with a Salesflare_ID value and delete them.
    Run per entity type in reverse dependency order:
    1. Activities/Tasks first
    2. Opportunities second
    3. People third
    4. Companies last
    """
    page = 1
    deleted_count = 0
    
    while True:
        # Search for records with Salesflare_ID populated
        search_payload = {
            "custom_fields": [
                {"custom_field_definition_id": SALESFLARE_ID_FIELD_ID,
                 "value": ""}  # Non-empty search
            ],
            "page_size": 200,
            "page_number": page
        }
        records = copper_search(copper_endpoint, search_payload)
        
        if not records:
            break
            
        for record in records:
            requests.delete(
                f"{COPPER_BASE_URL}/{copper_endpoint}/{record['id']}",
                headers=COPPER_HEADERS
            )
            deleted_count += 1
            time.sleep(0.34)
        
        page += 1
    
    print(f"Rollback complete: {deleted_count} {entity_type} records deleted.")

Run rollback in reverse dependency order: Activities and Tasks → Opportunities → People → Companies. Do not delete Companies before People or you'll create orphaned Person records.

Edge Cases That Break Most Migrations

Duplicate email addresses

Copper deduplicates People by email and Companies by domain. If your Salesflare data has contacts with duplicate email addresses — common when the same person exists under multiple accounts — Copper will overwrite the first imported record with the second. Deduplicate in Salesflare before export, or merge intentionally.

Multi-select field formatting

Salesflare multi-select custom fields export as arrays or comma-separated strings. Copper's CSV importer expects semicolon-separated values with no spaces: option1;option2;option3. A single extra space creates a new option like option2 (note the leading space), permanently polluting your dropdown options. Fix in your transformation step, not after import.

Tag column structure

Salesflare exports tags as a comma-separated list in one column. Copper's CSV importer expects one column per tag, each headed "Tag". If a contact has three tags, you need three columns all named "Tag" in your CSV. This is counterintuitive and is one of the most common CSV import failures.

Date format inconsistency

Copper accepts DD/MM/YYYY or MM/DD/YYYY, but you must be consistent within a single CSV. Mixing formats — or using YYYY-MM-DD (Salesflare's API default) — causes silent misinterpretation. A close date of 2026-03-07 won't throw an error; it will import as March 7 or July 3 depending on Copper's locale setting. Normalize all dates in your transformation step.

Unmapped users

If a sales rep left the company before the migration, their user account won't exist in Copper. Attempting to migrate their historical Opportunities without mapping their Salesflare User ID to an active Copper user causes API validation errors and silently drops records. Maintain a user mapping table and route departed users to a designated "Inactive Rep" account or system admin.

Orphaned activities

Salesflare allows notes to exist loosely on timelines. Copper requires strict parent-child relationships. If your script doesn't explicitly link a note to a Person, Company, or Opportunity via the parent field in the Activity payload, the POST will either fail or the record will not appear in any record's timeline.

State conflicts during cutover

Running a migration while the sales team is actively working in Salesflare guarantees data loss. Establish a hard cutoff date, run a primary sync, then run a delta sync — fetching records modified since the primary sync started using Salesflare's date_modified [from] filter parameter — immediately before cutover.

Connect fields don't import via CSV

Copper's "Connect" fields — the ones that link records to other records — cannot be populated via CSV import. Record relationships must be established either through Copper's name/email matching logic during import or programmatically via the API. This is not documented prominently in Copper's import UI and surprises most teams doing their first migration.

Silent truncation in String fields

Copper's String custom field type has a 255-character limit. Salesflare free-text fields (descriptions, notes appended to contact records) often exceed this. Fields exceeding the limit are truncated without error during CSV import. Map long-form text content to Copper's Text custom field type (up to 32,000 characters) to avoid losing data.

How Long Does a Salesflare to Copper Migration Take?

Dataset Size Method Estimated Duration
< 5,000 total records CSV import 1–2 days
5,000–20,000 records API migration 2–5 days
20,000–50,000 records API migration 5–10 days
50,000+ records Managed service 7–14 days

The variables that most commonly extend timelines:

  • Custom field count: More than 20 custom fields across objects adds 1–2 days of mapping and validation work.
  • Data quality: Duplicate contacts, inconsistent formatting, and stale records must be cleaned before migration, not after. A data quality remediation pass adds 1–3 days for messy datasets.
  • Activity history requirements: Transferring email logs and call records — not just contacts and deals — adds 2–3 days for API-based activity migration.
  • API write throughput ceiling: Copper's 180 req/min limit is the hard constraint on write speed. There is no bulk-create endpoint for People, Companies, or Opportunities in Copper's v1 API.

Pre-Migration Checklist

  1. Audit Salesflare data. Count records per object. Identify and merge duplicates. Flag contacts with no email address — if more than 5% lack emails, deduplicate before export.
  2. Map fields. Create a spreadsheet mapping every Salesflare field to its Copper equivalent. Flag fields with no native Copper match — these become custom fields. Note character limits for every target field.
  3. Decide what becomes a Lead vs. a Person. Salesflare has no Lead object. Identify which contacts should enter Copper as Leads (pre-qualification) vs People (qualified contacts) before any transformation work begins.
  4. Create custom fields and pipelines in Copper. Create the Salesflare_ID String field on Company, Person, and Opportunity objects. Create all other custom fields matching Salesflare types. Set up Pipeline Stages with exact names (case-sensitive) you'll reference during import.
  5. Export from Salesflare. Use the API for full-fidelity extraction including custom fields. The UI CSV export may not include all custom field data — verify field coverage before relying on UI exports.
  6. Transform the data. Reformat dates to DD/MM/YYYY consistently, restructure tags to one-per-column for CSV, convert multi-selects to semicolon-separated format with no spaces, normalize domains to example.com format, and normalize phone number formatting.
  7. Import in dependency order. Users and configuration first → Companies → People → Opportunities → Tasks and Notes → Historical Email Activities.
  8. Process the import error log. Download and review Copper's error CSV after each import batch. Do not proceed to the next object type until the current batch achieves zero errors or all errors are intentionally accepted.
  9. Validate post-import. Spot-check 50–100 records across all object types. Verify custom field values, pipeline stage assignments, People-to-Company links, and Opportunity-to-People associations.
  10. Connect Gmail accounts. Once data is validated in Copper, connect team Gmail accounts so Copper starts building interaction history going forward. Use the 45-day buffer rule to avoid email duplication with migrated historical activities.

For a deeper dive into CRM migration planning, see The Ultimate CRM Data Migration Checklist. For more on Copper's architecture and how it compares to other CRMs, see Pipeliner vs Copper: Architecture, TCO & Migration Guide.

Frequently Asked Questions

How do I map Salesflare objects to Copper CRM?
Salesflare Contacts map to Copper People, Accounts map to Companies, and Opportunities map to Opportunities. Salesflare has no Lead object, so you must decide which contacts should become Copper Leads vs People. Tasks map directly. Copper also has Projects with no Salesflare equivalent.
What is Copper CRM's API rate limit for data migration?
Copper's official developer documentation states 180 requests per minute on a rolling window, with bulk API endpoints further limited to 3 requests per second. Copper does not return rate-limit headers, so you must track request counts client-side and implement fixed throttling with exponential backoff.
Can I migrate Salesflare email history to Copper without duplicates?
Yes, but you must filter carefully. When you connect Google Workspace to Copper, it automatically syncs recent email history from Gmail. Only migrate emails older than Copper's sync window as custom activity records (e.g., 'Legacy Email' type) to avoid double-logging every email.
What order should I import data into Copper?
For API migrations, import in dependency order: Users and config first, then Companies, People, Opportunities, and finally Tasks and Notes. Child records need parent IDs to establish relationships, so parents must exist first.
How long does a Salesflare to Copper migration take?
Typically 2–5 days for under 20,000 records and 5–10 days for larger or complex environments. The main bottleneck is Copper's 180 requests/minute API limit. Custom field complexity, data quality issues, and activity history requirements extend the timeline.

More from our Blog