Skip to content

Teamwork CRM to Close Migration: Data Mapping, API & Cutover Guide

A technical guide to migrating from Teamwork CRM to Close, covering data model mapping, field-level translation, API extraction, import sequencing, and common failure modes.

Roopi Roopi · · 23 min read
Teamwork CRM to Close Migration: Data Mapping, API & Cutover 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

Teamwork CRM to Close Migration: Data Mapping, API & Cutover Guide

Info

This guide is published by ClonePartner, an engineer-led data migration service with 1,500+ completed migrations. We have hands-on experience moving data out of Teamwork CRM and into Close. Where we reference our own services, it is clearly marked. Last verified: July 2025.

Migrating from Teamwork CRM to Close is a model change, not just a tool swap. Teamwork CRM is Company-centric with separate Lead and Opportunity deal spaces. Close is Lead-centric — every record (contacts, opportunities, tasks, activities) nests under a single parent Lead. That structural mismatch is the root cause of duplicate companies, orphaned records, and broken pipelines during this migration.

Critical naming conflict: In Teamwork CRM, "Lead" means a pre-qualification deal object. In Close, "Lead" means a company container — the top-level record that owns all other objects. These two definitions are mutually exclusive. Every mapping decision in this guide uses Teamwork CRM and Close meanings explicitly. If a competing migration guide does not resolve this conflict in the first paragraph, do not follow it.

Teamwork CRM is not on Close's list of supported CRM-to-CRM migration sources. Close currently supports automatic imports from Act!, ActiveCampaign, Agile CRM, Attio, Capsule CRM, Copper, Firmao, Fow CRM, GoHighLevel, Highrise, HubSpot CRM, Insightly, Keap, Less Annoying CRM, MS Dynamics, Nimble, Nutshell, Onepage CRM, Pipedrive, PipelineDeals, Redtail CRM, Salesforce, Streak, Sugar CRM, SuiteCRM, Wealthbox, Zendesk Sell, and Zoho CRM. There is no one-click import path. You must export from Teamwork CRM (via CSV or API), transform the data, and import it into Close via CSV or the Close REST API.

This guide covers the complete technical map: data model comparison, field-by-field mapping, extraction methods including pagination, rate limits, edge cases, import error behavior, and the exact import sequence that avoids orphan records.

Data Model: Teamwork CRM vs. Close

The architecture difference controls every decision in this migration. Import objects in the wrong order and Close will auto-create malformed parent records that are tedious to merge at scale.

Teamwork CRM: Company-Centric with Split Deal Stages

In Teamwork CRM, Contacts are the main points of contact within a company you are trying to sell to. A Company is the target you are looking to acquire business from. Each company can have multiple contacts, but a contact can only be associated with one company.

A "deal" is a collective term for both leads and opportunities. For some businesses, they would regard 2 leads and 3 opportunities as 5 deals. In Teamwork CRM, leads and opportunities are managed in separate sections of your account, which allows you to do deeper reporting for both.

This gives Teamwork CRM four core entity types in its sales model:

  • Companies — the account/organization (top-level container)
  • Contacts — individuals linked to exactly one company
  • Leads — pre-qualification deal objects tracked in their own pipelines
  • Opportunities — qualified deal objects tracked in separate pipelines

The Teamwork CRM API exposes endpoints for activity-types, companies, contacts, currencies, custom-fields, deals, exports, files, imports, lost-reasons, notes, pipelines, products, stages, users, webhooks, and won-reasons.

Close: Lead-Centric, Everything Nested

Leads are the most important object in Close. They represent a company or organization and can contain contacts, tasks, opportunities, and activities. In fact, these other objects must be children of a Lead.

It is important to note that a Contact and Opportunity cannot exist without being tied to a Lead in Close.

In Close, the entity hierarchy is a hard constraint — not a convention:

  • Lead = Company/Account (the mandatory top-level container)
  • Contact = Person (nested under a Lead; cannot exist independently)
  • Opportunity = Deal (nested under a Lead, assigned to a Pipeline; cannot exist independently)
  • Activity = Calls, emails, SMS, notes, meetings (nested under a Lead)

The Dependency Graph: Why Import Order Is a Hard Constraint

This is not a recommended sequence — it is imposed by the data model. Each object depends on its parent existing first:

Companies (Teamwork) → Leads (Close)
    └── Contacts (Teamwork) → Contacts (Close) [requires lead_id]
    └── Leads/Opportunities (Teamwork) → Opportunities (Close) [requires lead_id + status_id + pipeline_id]
        └── Activities/Notes (Teamwork) → Activities (Close) [requires lead_id]

If you import Contacts before Leads exist, Close auto-creates a new Lead named after each contact. If you import Opportunities before Leads exist, the same happens. These auto-generated Leads are structurally broken and require manual merge or deletion. At 5,000+ records, this is not a minor cleanup — it is a project in itself.

How to Extract Data from Teamwork CRM

Two paths: native CSV export or the Teamwork CRM API. For migrations involving more than a few hundred records, the API is strongly recommended because CSV exports flatten relational data and lose foreign key references.

Native CSV Export

Within Teamwork CRM, you can export custom lists of contacts, companies and users from your site via CSV. You can also export custom leads information or details of all active leads on your site via CSV. The same applies to opportunities. Activities can also be exported via CSV.

The export function is available in list view only.

What CSV export gives you:

  • Contacts (with company association as a text name, not ID)
  • Companies (with custom fields)
  • Leads / Opportunities (with pipeline, stage, value)
  • Activities (with type, date, description)
  • Products

What CSV export loses:

  • Foreign key IDs (company-to-contact and deal-to-contact linkages become text matches, which break when company names are not unique)
  • File attachments
  • Full activity-to-deal threading
  • Notes on specific deals (may be flattened into generic rows)
Warning

CSV exports are filter-sensitive. Teamwork's exports are shaped by the list view columns and filters active when you run the export. Two admins running "the same" export with different saved views can produce structurally different files. Lock your filters, document the exact view configuration, and record the export timestamp and row count before proceeding. Reproducibility is as important as the data itself.

Almost all endpoints start with https://example.teamwork.com/crm/api/v2/. Authentication uses a Bearer token generated from your personal settings.

Key extraction endpoints:

GET /crm/api/v2/companies.json
GET /crm/api/v2/contacts.json
GET /crm/api/v2/deals.json          # Returns both leads and opportunities
GET /crm/api/v2/pipelines.json
GET /crm/api/v2/stages.json
GET /crm/api/v2/activities.json
GET /crm/api/v2/notes.json
GET /crm/api/v2/files.json
GET /crm/api/v2/custom-fields.json
GET /crm/api/v2/products.json
GET /crm/api/v2/lost-reasons.json
GET /crm/api/v2/won-reasons.json

Pagination

The Teamwork CRM API uses page-based pagination. Pass page and pageSize as query parameters. The response includes a meta.totalCount field that tells you the total number of records available:

GET /crm/api/v2/companies.json?page=1&pageSize=100

For a reliable full extraction, loop until the number of records returned is less than pageSize, or until (page * pageSize) >= meta.totalCount. Do not assume you have all records if you only make one request. Omitting pagination logic is the most common silent truncation error in DIY API extractions.

Info

Rate limit: Teamwork CRM's API has a rate limit of 150 requests per minute. Implement logic to handle HTTP 429 errors gracefully, such as retrying after a delay. At pageSize=100 and 150 requests/minute, a company database of 10,000 records requires a minimum of 100 requests — well within a single minute. However, fetching associated contacts, deals, and activities per company multiplies request count by 3–5x. Budget 30–90 minutes for a full extraction of a 5,000-company dataset with related objects.

The API preserves foreign key relationships (companyId on contacts, contactId and companyId on deals), which is why it is the preferred extraction method over CSV.

Warning

Email sync warning: If your Teamwork CRM users rely on synced mail, decide on email-history handling before you disconnect accounts. Teamwork CRM allows one connected email account per user, and disconnecting it removes previously synced emails from the Mail area and from deal timelines. Close's standard import routes cover leads, contacts, opportunities, notes, tasks, and calls — not historical email threads from a prior CRM.

Field-by-Field Data Mapping

The table below covers every standard object. Custom fields require their own mapping pass (covered after the table).

Teamwork CRM Object Teamwork CRM Field Close Object Close Field Notes
Company Company Name Lead Lead Name (display_name) Direct 1:1 map
Company Address, Phone, URL Lead Address, Phone, URL Standard fields
Company Owner Lead Assigned User Match by user email; requires user mapping table
Company Custom Fields Lead Lead Custom Fields Type compatibility varies — see below
Contact First Name, Last Name Contact Name Concatenate or map directly
Contact Email(s) Contact Email(s) Close supports multiple emails per contact
Contact Phone(s) Contact Phone(s) Close supports multiple phones per contact
Contact Title Contact Title Direct map
Contact Company (FK) Contact lead_id Must resolve Company → Lead ID first; missing mapping auto-creates a broken Lead
Lead (deal) Title Opportunity note field No dedicated "name" field on Opportunities in Close; use note or a custom text field
Lead (deal) Value Opportunity Value Close stores values in cents (integer). $5,000 → 500000. Off-by-100x errors silently corrupt pipeline reporting.
Lead (deal) Pipeline + Stage Opportunity Pipeline + Status Must pre-create pipeline and statuses in Close before any Opportunity import
Lead (deal) Owner Opportunity User Match by email; requires user mapping table
Lead (deal) Expected Close Date Opportunity Custom field No native "expected close date" field in Close
Lead (deal) Lost Reason Opportunity Custom field / note No dedicated lost-reason field in Close
Lead (deal) Probability Opportunity Confidence Map stage probability to confidence for forecast accuracy
Opportunity (deal) Same fields as Lead (deal) Opportunity Same mapping Qualified deals go into a separate Close pipeline or a distinct status set
Activity Type, Description, Date Activity (Note) Note body, date Most activity types collapse to Notes; see activity mapping below
Note Content, Date Activity (Note) Note body, date Direct map; preserve date_created and user_id explicitly
File Attachment File File (on Lead) API-only; not available via CSV import; requires binary download and re-upload
Product Name, Price Custom Field Close has no native product catalog object

Custom Field Type Compatibility

Teamwork CRM Field Type Close Custom Field Type Notes
Text Text Direct match
Number Number Direct match
Dropdown Dropdown (single) Pre-create all options before import; unrecognized options are rejected
Date Date Direct match; use ISO 8601 format
Checkbox Dropdown with Yes/No No native checkbox type in Close
Currency Number Loses currency symbol; store original currency code in a separate text field
URL URL Close has a URL field type
Warning

Close does not support multiple currencies. Close doesn't support multiple currencies and their exchange rates just yet. If your Teamwork CRM deals use different currencies, normalize all values to a single currency before import, or store the original currency code and amount in custom text fields for reference.

Warning

Close stores Opportunity values in cents (integer), not dollars. A $5,000 deal must be sent as 500000. A $1,250.50 deal must be sent as 125050. This is a silent corruption risk — Close will accept whatever integer you provide without validating the magnitude, so a $50,000 deal imported as 50000 appears as $500.00 in your pipeline with no error thrown.

Tip

Create visible text custom fields in Close for source_company_id, source_contact_id, source_lead_id, and source_opportunity_id. These let you trace every record back to Teamwork CRM for validation and rollback. Do not use Close's Hidden field type for these — Hidden fields are not visible in exports and cannot be used in lead search, which defeats their purpose for validation.

Import Error Behavior

Understanding how Close handles malformed imports prevents silent data loss:

Error Condition Close API Behavior
Missing required field (e.g., no lead_id on Contact) Auto-creates a new Lead named after the contact; returns 200 with a new lead_id
Invalid status_id (Opportunity) Returns 400 Bad Request; record not created
Invalid pipeline_id (Opportunity) Returns 400 Bad Request; record not created
Duplicate email on Contact import Merges with existing contact if deduplication is enabled; otherwise creates duplicate
Value field as float instead of integer Truncates to integer silently; $5,000.99 sent as 500099.0 may be stored as 500099
Unrecognized custom field ID Field is silently ignored; no error thrown

The most dangerous error mode is the silent ones: missing lead_id creates bad records, unrecognized custom field IDs drop data, and wrong value magnitudes corrupt reporting — all without returning an error status.

How to Import Data into Close

Close offers two import paths: the native Lead Importer (CSV/XLSX) and the REST API.

Close Lead Importer (CSV)

Using the Lead Importer, you can import (and update) Leads from a CSV or XLSX/XLS (Excel format) file or perform a one-time data migration from an old CRM product/service.

Key constraints and column naming requirements:

  • The file has to contain one column that identifies the Lead, for example Lead Name, Email or Phone. This is how Close will identify the Lead to which the Opportunity belongs.
  • The Lead Importer expects specific column headers. Standard recognized headers include: Company, Lead Name, URL, Status, Email, Phone, Contact Name, Title, Opportunity Value, Opportunity Status, Opportunity Pipeline, Opportunity Note. Custom field columns must be prefixed with Custom. followed by the exact field label (e.g., Custom.Industry).
  • You can also create a Custom Field upon import. When importing a list of leads, Close will anticipate which Custom Fields you might want to create and mark those as New.
  • The Opportunity Status should match any of the Opportunity statuses you have in your pipeline. Pre-create pipelines and statuses before importing.
  • Close enforces a 1,000 opportunities per lead limit. If a Teamwork company has unusually deep transaction history, decide whether to split that account into multiple Leads before loading data.

Minimum viable CSV structure for Lead + Contact + Opportunity import:

Lead Name,URL,Status,Contact Name,Email,Phone,Opportunity Value,Opportunity Status,Opportunity Pipeline,Custom.Source CRM ID
Acme Corp,https://acmecorp.com,Potential,Jane Doe,jane@acmecorp.com,+15551234567,500000,Active,Enterprise Pipeline,company_12345

Note that Opportunity Value in the CSV is also expected in cents when importing via the API, but the CSV importer accepts dollar values. Verify the current behavior in Close's importer UI before bulk-loading, as this has historically been a source of confusion.

The Close API at api.close.com/api/v1/ gives programmatic control over Leads, Contacts, Opportunities, Activities, and custom field values with full relationship management.

API requests are limited at a per-Organization level across all users' API keys. Close also enforces a lower rate limit per API key. The per-Organization limit is currently 3 times higher than individual API key rate limits, meaning that if the API key rate limit maximum requests per second (RPS) is 20 RPS, the organization-wide limit would be 60 RPS for that same endpoint group.

When you hit the limit, Close returns a 429 Too Many Requests response with a Retry-After header specifying exactly how many seconds to wait. Parse this header dynamically rather than hardcoding a fixed delay — it allows your script to run at maximum speed without dropped payloads or unnecessary pauses.

Required Close API permissions: The API key used for import must belong to a user with Administrator role. Standard user keys cannot create custom fields, pipelines, or import data on behalf of other users.

One of the most useful features of the Close API is nested Lead creation: you can create a Lead, its Contacts, and its Opportunities in a single POST /api/v1/lead/ request. This eliminates the need to store intermediate IDs between sequential requests:

{
  "name": "Acme Corp",
  "url": "http://acmecorp.com",
  "custom.cf_industry": "Manufacturing",
  "custom.cf_source_company_id": "company_12345",
  "contacts": [
    {
      "name": "Jane Doe",
      "title": "VP of Sales",
      "emails": [{"type": "work", "email": "jane@acmecorp.com"}],
      "phones": [{"type": "office", "phone": "+15551234567"}]
    }
  ],
  "opportunities": [
    {
      "note": "Q3 Enterprise License",
      "confidence": 85,
      "value": 1500000,
      "value_period": "annual",
      "status_id": "stat_1234567890abcdef",
      "pipeline_id": "pipe_0987654321fedcba"
    }
  ]
}

Sending this to POST /api/v1/lead/ generates the Lead, Contact, and Opportunity simultaneously with relational links resolved automatically. For a dataset of 5,000 Leads with associated contacts and opportunities, expect 1–3 hours for API import depending on the number of related records per Lead and your rate limit headroom.

Step-by-Step Migration Sequence

Order is a hard constraint imposed by the Close data model, not a recommendation. Each step documents the dependency that makes skipping it destructive.

Step 1: Audit and Export from Teamwork CRM

Before touching any import tool:

  1. Export custom field definitions via GET /crm/api/v2/custom-fields.json — document every field name, type, and entity (company, contact, deal)
  2. Export pipelines and stages — map each Teamwork pipeline to a target Close pipeline
  3. Export companies (with pagination) — these become your Close Lead records
  4. Export contacts (with pagination) — preserve the companyId foreign key on every record
  5. Export deals (leads + opportunities, with pagination) — preserve companyId, contactId, pipelineId, stageId
  6. Export activities and notes — preserve deal and contact associations
  7. Export files if needed — requires downloading binary content via the API, not available via CSV

Record the exact filters, view definitions, export timestamps, and row counts for each entity. These become your validation baselines in Step 8.

Step 2: Build a Staging Dataset

Do not map raw Teamwork exports directly into Close. Create canonical tables keyed by source IDs:

  • Company table: teamwork_company_id → normalized company record
  • Contact table: teamwork_contact_id → normalized contact record, with teamwork_company_id FK
  • Deal table: teamwork_deal_id → normalized opportunity record, with teamwork_company_id and teamwork_contact_id FKs
  • User mapping table: teamwork_user_id → Close user email
  • Pipeline mapping table: teamwork_pipeline_id + teamwork_stage_id → Close pipeline_id + status_id
  • Activity mapping table: Teamwork activity type → Close activity type

This staging layer is where you resolve: duplicates, stale owners, blank contact emails, orphaned contacts, multi-currency normalization, cents conversion, and missing required fields.

Handling orphaned contacts: Teamwork CRM allows Contacts to exist without a Company. Close rejects any Contact without a parent Lead. Two options:

  1. Clean the data: Identify orphaned contacts during staging and assign them to the correct Company.
  2. Create a placeholder Lead: Programmatically create a Lead in Close (e.g., "Unassigned Contacts — July 2025") and nest all orphaned contacts under it for later reassignment.

Step 3: Pre-Create Close Infrastructure

Before importing any records:

  1. Create pipelines matching your Teamwork CRM pipelines. Teamwork CRM has separate Lead pipelines and Opportunity pipelines — decide whether to merge them or keep them separate in Close before touching any data.
  2. Create Opportunity Statuses in each pipeline matching the Teamwork stages. Map "Won" and "Lost" stages explicitly. Unrecognized status_id values cause 400 errors on Opportunity import.
  3. Create Custom Fields for Lead, Contact, and Opportunity. Match types using the compatibility table above. Pre-create all dropdown options — options not pre-created are rejected during import.
  4. Create Lead Statuses if needed. Default Close statuses are Potential, Bad Fit, and Qualified. Add custom statuses to match Teamwork's lead pipeline stages.
  5. Create or invite users and build a mapping table of Teamwork User IDs to Close User IDs (matched by email address).

Record the Close pipeline_id, status_id, and custom field IDs returned by the API — you need these for every subsequent import request.

Step 4: Import Companies as Leads

This is the foundation. Every other record depends on the Lead existing first.

Via API: POST to /api/v1/lead/ for each company. Store the returned lead_id alongside the original Teamwork companyId in your mapping table — you need it for every subsequent import:

import requests
import time
 
API_KEY = "your_close_api_key"
BASE_URL = "https://api.close.com/api/v1"
 
def create_lead(company, lead_id_map):
    payload = {
        "name": company["name"],
        "url": company.get("url", ""),
        "custom.cf_source_company_id": str(company["id"]),
        "addresses": [{
            "address_1": company.get("address", ""),
            "city": company.get("city", ""),
            "state": company.get("state", ""),
            "zipcode": company.get("zip", ""),
            "country": company.get("country", "")
        }] if company.get("address") else [],
    }
    while True:
        resp = requests.post(
            f"{BASE_URL}/lead/",
            json=payload,
            auth=(API_KEY, "")
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 5))
            time.sleep(retry_after)
            continue
        resp.raise_for_status()
        close_lead_id = resp.json()["id"]
        lead_id_map[str(company["id"])] = close_lead_id
        return close_lead_id

Via CSV: Create a CSV with columns for Lead Name, URL, Status, and all mapped custom fields. Import via the Lead Importer. After import, export the resulting Leads from Close to recover the lead_id values, then build your mapping table from the exported data by matching on Custom.Source Company ID.

Step 5: Import Contacts Under Their Leads

Use the mapping table from Step 4 to resolve each Contact's companyId to a Close lead_id.

In Close, Contacts belong to exactly one Lead (specified by lead_id). If you do not provide a lead_id then a new lead will be created, named after the contact.

Validate before importing: Confirm that every companyId in your contacts export has a corresponding entry in your lead_id_map. Any gap will cause Close to auto-create a malformed Lead named after the contact. Run this check programmatically before starting any POST requests:

missing = [c for c in contacts if str(c["companyId"]) not in lead_id_map]
if missing:
    raise ValueError(f"{len(missing)} contacts have no matching Lead. Resolve before importing.")

Deduplication behavior: Close's Contact importer can merge contacts when records share the same email or phone number. Shared inboxes (info@, sales@), switchboard numbers, and generic addresses are common causes of unintended merges. Normalize and deduplicate contact data in your staging dataset before import — particularly for contacts that share a company main number or team inbox.

Step 6: Import Deals as Opportunities

Teamwork CRM's Leads (pre-qualification deal objects) and Opportunities (qualified deal objects) both map to Close Opportunities. Decide how to represent the distinction before importing:

Option A — Separate Pipelines: Create a "Lead Pipeline" and an "Opportunity Pipeline" in Close, mirroring the Teamwork structure. Maintains full reporting separation but increases pipeline management overhead.

Option B — Single Pipeline with Stages: Merge both into one pipeline where early stages represent pre-qualification and later stages represent qualified opportunities. Reduces pipeline count but loses the explicit Teamwork reporting distinction.

Option C — Lead Status + Single Pipeline: Use Close's Lead Status field (set on the parent Lead record) to flag qualification level, and use a single pipeline for all deal stages. Most appropriate when the qualification distinction maps to company state rather than deal state.

For each deal, resolve companyIdlead_id, then POST to /api/v1/opportunity/:

def create_opportunity(deal, lead_id_map, pipeline_id, status_map):
    lead_id = lead_id_map.get(str(deal["companyId"]))
    if not lead_id:
        raise ValueError(f"No lead_id for company {deal['companyId']} — aborting.")
 
    raw_value = deal.get("value", 0)
    value_cents = int(round(float(raw_value) * 100))  # Convert dollars to cents; round first to avoid float truncation
 
    payload = {
        "lead_id": lead_id,
        "pipeline_id": pipeline_id,
        "status_id": status_map.get(str(deal["stageId"]), status_map["default"]),
        "value": value_cents,
        "note": deal.get("title", ""),
        "confidence": deal.get("probability", 50),
        "custom.cf_source_deal_id": str(deal["id"]),
    }
    while True:
        resp = requests.post(
            f"{BASE_URL}/opportunity/",
            json=payload,
            auth=(API_KEY, "")
        )
        if resp.status_code == 429:
            retry_after = int(resp.headers.get("Retry-After", 5))
            time.sleep(retry_after)
            continue
        if resp.status_code == 400:
            raise ValueError(f"Bad request for deal {deal['id']}: {resp.json()}")
        resp.raise_for_status()
        return resp.json()["id"]

Step 7: Import Activities and Notes

Teamwork CRM activities (calls, meetings, custom types) and notes need to be converted to Close activity types.

Close native activity endpoints:

  • NotePOST /api/v1/activity/note/
  • CallPOST /api/v1/activity/call/
  • Email — synced from connected inbox, not imported via API
  • MeetingPOST /api/v1/activity/meeting/
  • SMS — synced from Close's SMS feature, not imported via API

Recommended activity type mapping:

Teamwork CRM Activity Type Close Activity Type Notes
Phone call Call Use /api/v1/activity/call/; set duration_seconds if available
Meeting / Event Meeting Use /api/v1/activity/meeting/
To-do / Task Task Use /api/v1/task/
Email Note Historical emails cannot be imported as Email activities; import as Note with type flagged in body
Custom activity types Note Preserve original type in note body: [TYPE: Site Visit]\n\nDescription...
Freeform note Note Direct map

Do not collapse meetings and calls into generic notes if you care about historical reporting by activity type. Once imported as Notes, they cannot be retroactively reclassified without re-importing.

Preserving timestamps and authors: Set date_created explicitly on every imported activity. If omitted, Close assigns the current timestamp to all imported records, destroying your activity timeline. Also set user_id to preserve the original author:

{
  "lead_id": "lead_abcdef123456",
  "contact_id": "cont_fedcba654321",
  "user_id": "user_9876543210",
  "note": "[TYPE: Phone Call]\n\nDiscussed Q3 pricing. Client requested a 10% discount.",
  "date_created": "2024-08-12T09:15:00.000Z"
}

Close expects timestamps in ISO 8601 format with UTC timezone (Z suffix). Timestamps without timezone specification may be interpreted inconsistently.

Step 8: Validate

After import, run these checks against the baselines recorded in Step 1:

Validation Check Method
Lead count matches Teamwork Company count Export Leads from Close; compare row count
Contact count matches; every contact nested under correct Lead Export Contacts; spot-check lead_id assignments
Opportunity count = Teamwork Leads + Opportunities Export Opportunities; compare row count
Pipeline stage distribution matches source Compare stage-bucketed counts in both systems
Opportunity value sum matches source (watch for cents error) Sum both; divide Close total by 100 before comparing
Owner assignment parity across Leads, Contacts, Opportunities Export each object type; compare assigned user distribution
No orphaned Leads (auto-generated from missing parent) Search Close for Leads with no opportunities and auto-generated names
Phone and email searchability Spot-search 10–20 records in Close UI
Custom field accuracy Spot-check 20 records across all entity types
Activity timeline completeness Compare activity counts per record on 10–20 sample accounts

If the opportunity value sum is off by exactly 100x, you have a cents conversion error affecting all records. If it is off by a non-round factor, you have a mix of correctly and incorrectly converted records.

Step 9: Run a Delta Pass and Cut Over

If Teamwork CRM stays live during the test migration, your sales team continues working — creating new records and modifying existing ones. Before cutover, run a delta extraction:

GET /crm/api/v2/companies.json?updatedAfter=2025-07-15T00:00:00Z
GET /crm/api/v2/contacts.json?updatedAfter=2025-07-15T00:00:00Z
GET /crm/api/v2/deals.json?updatedAfter=2025-07-15T00:00:00Z

Check the Teamwork CRM API documentation for the exact filter parameter name — updatedAfter, updated_after, or modifiedSince depending on the endpoint version. If a timestamp filter is not available on a given endpoint, fall back to comparing exported IDs against your lead_id_map to identify net-new records.

For net-new records (IDs not in your mapping table), run a full create. For modified records (IDs already in your mapping table), run an update using the corresponding Close ID. The smaller the window between initial extraction and delta pass, the lower the risk of conflicts. Schedule the delta pass during off-hours and flip DNS or user access to Close immediately after.

Edge Cases and Failure Modes

Products Have No Close Equivalent

You can export custom product information or details of all products on your site via CSV. Close has no product object. Options in order of fidelity:

  1. Store product names and SKUs in an Opportunity custom dropdown field (best for structured reporting)
  2. Append product details to the Opportunity note (preserves detail, not queryable)
  3. Build a linked Custom Object in Close (available on Business and above tiers — confirm current pricing)

Lost and Won Reasons

Teamwork CRM tracks lost reasons and won reasons as structured data on deals. Close tracks win/loss as Opportunity statuses but has no dedicated reason field. Store reasons in a custom text or dropdown field on the Opportunity, or append to the Opportunity note. If you append to the note, use a consistent prefix (e.g., [LOST REASON: Price]) to make bulk search possible.

Multi-Contact Deals

In Teamwork CRM, a deal can be associated with a specific contact. In Close, an Opportunity is linked to a Lead (company), not a specific Contact. To preserve the deal-to-contact relationship, store the contact name and email in an Opportunity custom field (contact_name, contact_email).

File Attachments

Teamwork CRM files must be downloaded via the API (GET /crm/api/v2/files.json + binary download per file) and re-uploaded to Close via POST /api/v1/attachment/. This is not possible through CSV import. For large file libraries (500+ files), binary transfer time can dominate the overall migration timeline — budget accordingly and consider whether the file history is actively needed in Close vs. archivable elsewhere.

Contact Merges on Import

Close's importer can merge contacts when different records share the same email or phone number. Shared inboxes (info@, sales@), switchboard numbers, and generic addresses are common causes of accidental merges that silently reduce contact count. Normalize and deduplicate contact data before import. If you intentionally want to suppress deduplication, create contacts via the API with unique identifiers rather than through the CSV importer.

Teamwork CRM Data Retention After Migration

Teamwork CRM accounts remain readable after you stop adding data, but confirm with Teamwork's support team whether your subscription tier provides read-only access post-cancellation and for how long. Do not cancel the Teamwork CRM subscription until you have completed validation (Step 8) and the delta pass (Step 9).

Choosing Your Migration Method

Criteria CSV Import API-to-API ClonePartner Managed
Best for < 500 records, simple data Any size, full fidelity Complex datasets, zero-downtime
Preserves relationships Partial (text matching) Full (ID-based) Full
Activity history Not supported Full Full
File attachments Not supported Supported Supported
Custom field types Basic Full Full
Delta sync support Not supported Manual scripting required Included
Typical timeline 1–2 days 3–7 days (engineering time) 2–5 days
Risk of orphan records High Low (with validation) Near zero
Requires Close Admin role Yes Yes Handled

For teams with fewer than 500 companies and no complex activity history, a well-prepared CSV import via Close's Lead Importer can work. For anything larger — or where activity history, file attachments, delta sync, or multi-currency normalization matter — the API-to-API approach or a managed migration is the correct choice.

What About Ongoing Sync?

If you need to run both systems in parallel during a transition period, neither Teamwork CRM nor Close offers native bidirectional sync with the other. Options:

  • Custom sync layer: Webhook listeners on both APIs, with a mapping table to route changes. Feasible but requires engineering maintenance.
  • Zapier or Make: Trigger-based syncing (e.g., new deal in Teamwork → create Opportunity in Close). Works for low-volume, simple field mappings. Does not handle backfill or relationship resolution.
  • Close's native webhooks: Close fires webhooks on lead.created, opportunity.created, contact.created, and activity events. These can be used to keep a downstream system updated if Close is the system of record during transition.

For teams that need real-time data sync between CRM platforms during or after migration, that is a problem we solve regularly at ClonePartner with our continuous data sync service.

The Bottom Line

The Teamwork CRM to Close migration reduces to three hard mappings: Teamwork Companies → Close Leads, Teamwork Contacts → Close Contacts, Teamwork Leads + Opportunities → Close Opportunities. The complexity comes from:

  1. Preserving relational integrity — every Contact and Opportunity must reference a valid lead_id or Close creates malformed auto-Leads
  2. Handling Teamwork's split deal model — pre-qualification Leads and Opportunities are structurally identical in Close but need a pipeline strategy decision before import
  3. Objects with no Close equivalent — Products, Lost Reasons, Won Reasons, and multi-currency all require workarounds
  4. Silent data corruption risks — cents vs. dollars conversion, unrecognized custom field IDs, missing date_created on activities, and filter-sensitive CSV exports can all corrupt data without throwing errors

Get the import order right (Leads first, Contacts second, Opportunities third, Activities last), validate your ID mapping table at every step, and explicitly handle the 429 rate limit with Retry-After parsing. Those four things prevent the majority of migration failures on this route.

For a broader pre-cutover framework, see The Ultimate CRM Data Migration Checklist. If you are evaluating Close against other platforms, our Close vs Accelo guide and folk CRM vs Close comparison compare Lead-centric models to traditional CRM structures. For details on Close's export capabilities, read How to Export Data from Close.

Frequently Asked Questions

Can I migrate directly from Teamwork CRM to Close using Close's built-in CRM importer?
No. Teamwork CRM is not on Close's list of supported CRM-to-CRM migration sources. You need to export from Teamwork CRM via CSV or API, transform the data to match Close's Lead-centric structure, and then import using Close's Lead Importer or REST API.
How do Teamwork CRM Leads and Opportunities map to Close?
Both Teamwork CRM Leads (pre-qualification deals) and Opportunities (qualified deals) map to Close Opportunities. In Close, the parent 'Lead' is the Company/Account. You can keep the distinction by creating separate pipelines in Close, merging into one pipeline with stages representing qualification level, or using Close's Lead Status field.
What data is lost when migrating from Teamwork CRM to Close?
Close has no native Product catalog, no structured Lost/Won Reason fields, and does not support multiple currencies. Products must be stored in custom fields, deal reasons should go into custom fields or notes, and all currency values must be normalized to a single currency before import.
What are the API rate limits for Teamwork CRM and Close?
Teamwork CRM allows 150 requests per minute. Close allows approximately 20 requests per second per API key, with an organization-wide limit of about 60 RPS. Both return HTTP 429 when limits are exceeded, with retry-after headers.
What order should I import data when migrating to Close?
Always import in this order: 1) Companies as Leads, 2) Contacts under their parent Leads, 3) Deals as Opportunities under their parent Leads, 4) Activities and Notes. Importing out of order creates orphan records because Close auto-generates a Lead if a Contact or Opportunity has no parent.

More from our Blog