---
title: "Teamwork CRM to Close Migration: Data Mapping, API & Cutover Guide"
slug: teamwork-crm-to-close-migration-data-mapping-api-cutover-guide
date: 2026-08-18
author: Roopi
categories: [CRM, Migration Guide]
excerpt: "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."
tldr: "Teamwork CRM is Company-centric with separate Lead/Opportunity deal stages; Close is Lead-centric where everything nests under one parent. Companies → Leads, Contacts → Contacts, Deals → Opportunities — get the mapping and import order right to prevent orphaned records."
canonical: https://clonepartner.com/blog/teamwork-crm-to-close-migration-data-mapping-api-cutover-guide/
---

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


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

> [!NOTE]
> 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. <cite index="21-4">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.</cite> 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

<cite index="47-16,47-17,47-18">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.</cite>

<cite index="47-5,47-6,47-7">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.</cite>

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

<cite index="60-1">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.</cite>

### Close: Lead-Centric, Everything Nested

<cite index="24-4,24-5,24-6">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.</cite>

<cite index="65-16">It is important to note that a Contact and Opportunity cannot exist without being tied to a Lead in Close.</cite>

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

<cite index="12-1">Within Teamwork CRM, you can export custom lists of contacts, companies and users from your site via CSV.</cite> <cite index="13-1">You can also export custom leads information or details of all active leads on your site via CSV.</cite> <cite index="10-1">The same applies to opportunities.</cite> <cite index="15-1">Activities can also be exported via CSV.</cite>

<cite index="13-3">The export function is available in list view only.</cite>

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

### Teamwork CRM API (Recommended)

<cite index="55-8">Almost all endpoints start with `https://example.teamwork.com/crm/api/v2/`.</cite> 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.

> [!NOTE]
> **Rate limit:** <cite index="28-1">Teamwork CRM's API has a rate limit of 150 requests per minute.</cite> <cite index="28-2">Implement logic to handle HTTP 429 errors gracefully, such as retrying after a delay.</cite> 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.** <cite index="72-14">Close doesn't support multiple currencies and their exchange rates just yet.</cite> 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)

<cite index="19-2">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.</cite>

Key constraints and column naming requirements:

- <cite index="68-2,68-3">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.</cite>
- 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`).
- <cite index="71-6,71-7,71-8">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.</cite>
- <cite index="68-7">The Opportunity Status should match any of the Opportunity statuses you have in your pipeline.</cite> 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.

### Close REST API (Recommended for Full Fidelity)

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.

<cite index="37-1,37-2,37-3">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.</cite>

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:

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

```python
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](https://clonepartner.com/blog/blog/how-to-export-data-from-close-methods-api-limits-formats/) 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`.

<cite index="27-4,27-5">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.</cite>

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

```python
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 `companyId` → `lead_id`, then POST to `/api/v1/opportunity/`:

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

- **Note** — `POST /api/v1/activity/note/`
- **Call** — `POST /api/v1/activity/call/`
- **Email** — synced from connected inbox, not imported via API
- **Meeting** — `POST /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:

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

<cite index="14-1">You can export custom product information or details of all products on your site via CSV.</cite> 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](https://clonepartner.com/blog/blog/the-ultimate-crm-data-migration-checklist-a-10-point-plan-for-a-zero-loss-transition/). If you are evaluating Close against other platforms, our [Close vs Accelo guide](https://clonepartner.com/blog/blog/close-vs-accelo-architecture-tco-migration-guide/) and [folk CRM vs Close comparison](https://clonepartner.com/blog/blog/folk-crm-vs-close-architecture-tco-migration-guide/) compare Lead-centric models to traditional CRM structures. For details on Close's export capabilities, read [How to Export Data from Close](https://clonepartner.com/blog/blog/how-to-export-data-from-close-methods-api-limits-formats/).

> Migrating from Teamwork CRM to Close and want to make sure nothing gets lost? ClonePartner has completed 1,500+ data migrations. We handle the extraction, mapping, and import — typically in under a week. Book a free 30-minute consultation.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

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