Pipedrive to Attio Migration: The CTO's Guide to Data Mapping & APIs
A technical guide to migrating from Pipedrive to Attio: object mapping, API rate limits, Import2 limitations, and the engineering decisions that determine data integrity.
Planning a migration?
Get a free 30-min consultation. Our engineers review your setup and map out a custom migration plan — no obligation.
Schedule Free Consultation- 1,200+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
Migrating from Pipedrive to Attio is a data-model translation problem, not a CSV exercise. Pipedrive organizes everything around a linear deal pipeline — Organizations, Persons, Deals, Activities — with a flat custom-field layer and no custom objects. Attio is a flexible, object-relational CRM where you define your own schema: standard objects, custom objects, relationship attributes, and lists. A CSV export from Pipedrive will move rows, but it won't preserve the multi-object associations, activity links, or pipeline stage history that give your CRM data its meaning.
This guide covers the structural mismatches between the two systems, the object-mapping decisions you need to make, the API constraints on both sides, the viable migration methods and their trade-offs, and the edge cases that catch teams off guard — so you can choose the right approach before writing a single line of migration code.
For teams evaluating HubSpot as an alternative target, the Pipedrive extraction architecture is nearly identical — see Pipedrive to HubSpot Migration: Data Mapping, APIs & Rate Limits. For a deeper look at Attio's data model and workspace configuration, see The Ultimate Guide to Attio CRM (2026).
Why Teams Migrate from Pipedrive to Attio
The drivers we see most often:
- Data model flexibility. Pipedrive's model is deal-pipeline-centric: a Person has a single Organization association, and there are no custom objects. When your business needs to model multi-company relationships, product usage data, or investor relationships, Pipedrive forces you into workarounds with custom fields. Attio lets you create custom objects, define relationship attributes between any objects, and build lists that aggregate records across workflows.
- Relationship intelligence. Attio automatically ingests email and calendar data when users connect their inboxes, enriching People and Company records with interaction history — no manual logging.
- API-first architecture. Attio's API is well-documented and consistent, which matters for teams building internal tooling and integrations around their CRM.
- Cost consolidation at scale. Pipedrive's per-seat pricing with add-ons (LeadBooster, Smart Docs, Projects) adds up. Attio's plan structure bundles more functionality per tier.
The Architectural Shift: Pipedrive vs. Attio Data Models
Pipedrive is pipeline-centric. Core entities are Organizations, Persons, Deals, Leads, Activities, and Products. Deals sit at the center — activities, notes, and files fan out from deals or their linked contacts. Leads live in a separate inbox. There are no custom objects; only custom fields on standard entities. Custom fields are identified by random 40-character hex hashes in the API, and these hashes differ across every Pipedrive account. (developers.pipedrive.com)
Attio is object-relational. It ships with five standard objects: People, Companies, Deals, Users, and Workspaces. You can create custom objects on Pro (up to 12 objects) and Enterprise (unlimited) plans. Objects connect through relationship attributes — one-to-one, one-to-many, or many-to-many. Lists aggregate records from a single object type and can carry their own attributes, functioning like filtered views with additional context. (docs.attio.com)
The key structural mismatches:
| Pipedrive | Attio | Migration implication |
|---|---|---|
| Organizations | Companies | Near 1:1. Pipedrive's org_id can serve as a legacy identifier. Companies are matched by domain in Attio. |
| Persons | People | Near 1:1. Matched by email_addresses in Attio. A Person's single org link becomes a relationship attribute. |
| Deals | Deals | Conceptually similar, but Attio Deals have no default unique attribute — you must add one (e.g., pipedrive_deal_id) before using the Assert API. |
| Leads | People + List entries | Pipedrive Leads are a separate entity. In Attio, model them as People records added to a "Leads" list, or as Deal records in an early pipeline stage. |
| Activities | Notes / Tasks | Pipedrive activities link to one deal, lead, or project at a time. Attio has separate Notes and Tasks APIs with different linking semantics. |
| Products | Custom object | No direct equivalent. Create a custom Products object or use list attributes on Deals. |
| Custom fields | Attributes | Pipedrive custom fields map to Attio attribute types. Enum/set fields need option-value mapping. |
Pipedrive Leads are not Contacts. In Pipedrive, Leads are unconverted prospects in a separate inbox with limited field support. Decide during planning whether they become People records, Deal records in a qualification stage, or list entries in Attio.
Attio custom object constraints. Attio supports custom objects, but the real limitation is key design: custom required attributes are only available on custom objects, and custom unique attributes are not available on standard Companies or People. This means you cannot add a pipedrive_org_id as a unique key on Companies — you must rely on domains for matching. (attio.com)
Data Model & Object Mapping in Detail
Organizations → Companies
Pipedrive Organizations map to Attio Companies. Attio matches Companies by domain — this is the natural unique key. Your migration script should extract the website/domain field from each Pipedrive Organization and use it as the matching attribute in Attio's Assert Company endpoint. Attio validates domains and stores root domains, not full URLs — strip protocols and paths before loading. (attio.com)
Watch for: Pipedrive Organizations without a domain. These will create orphan Company records in Attio that can't be deduplicated later by domain matching. Clean these before migration or assign a synthetic identifier in an external crosswalk table.
Persons → People
Pipedrive Persons map to Attio People, matched by email address. A Person in Pipedrive has a single org_id linking to one Organization. In Attio, People can have multi-value relationship attributes to Companies — so you can preserve the primary association and add secondary affiliations.
Records without a valid email address can still be imported but won't benefit from Attio's native deduplication or enrichment. Normalize and lowercase emails before loading. (docs.attio.com)
Deals → Deals
Pipedrive Deals map to Attio Deals. The critical detail: Attio Deals do not have a default unique attribute. You must create a custom attribute (e.g., pipedrive_deal_id) with a unique constraint before running the Assert endpoint. Without this, every API write creates a new record instead of upserting.
Pipedrive's pipeline stages map to Attio's status attribute on the Deals object. Pre-create matching stage options in Attio and build a stage-name-to-option mapping in your transform layer. Attio Deals ship with default stages like Lead, In Progress, Won 🎉, and Lost — replace or extend these with your Pipedrive stage names. (docs.attio.com)
Leads → People + List Entries or Deals
Pipedrive Leads live in a separate inbox and inherit the deal custom-field structure. Map them based on business meaning, not source object name. Pre-qualified pipeline intake often belongs in a list in Attio. Sales-ready leads usually become Attio Deals with linked People and Companies. (developers.pipedrive.com)
Activities → Notes + Tasks
This is where the models diverge most. Pipedrive activities are a single entity type covering calls, meetings, emails, and tasks — each linked to at most one deal, lead, or project. After Pipedrive's March 2024 API change, an activity can link to only one of deal, lead, or project at a time.
Attio separates these into:
- Notes — free-text records attached to any record via the Notes API
- Tasks — actionable items with due dates and assignees
Your transform layer needs to classify each Pipedrive activity by type and route it to the appropriate Attio endpoint, preserving the link to the parent record. Open work belongs in Tasks. Completed calls, meetings, and historical interaction summaries belong in Notes.
Custom Fields → Attributes
Pipedrive custom fields use 40-character hex hashes as API keys. These hashes differ across accounts, so your extraction script must first call the GET /v1/{entity}Fields endpoint to build a hash-to-label mapping, then use that mapping during extraction and transformation.
Most Pipedrive field types have a direct Attio equivalent. Notable exceptions:
- Monetary fields: Pipedrive includes a currency code per value. Attio's currency attribute requires the currency to be set at the attribute level. If your Pipedrive deals use mixed currencies, you'll need separate Attio attributes per currency or a transformation layer.
- Phone fields: Pipedrive supports labels like "work" and "mobile." Attio stores the number and country code but uses a different structure.
- Set fields: Pipedrive multi-option select maps to Attio multi-select, but option IDs won't match — use label-based mapping.
- Rich notes: Pipedrive notes are HTML-formatted and can be up to ~100KB each. Plan an HTML-to-Markdown or HTML-to-plaintext transform rather than assuming they'll drop in untouched. (developers.pipedrive.com)
Evaluating Migration Methods
Teams generally evaluate five approaches. Each has distinct trade-offs regarding engineering effort, data fidelity, and whether it works in a live Attio workspace.
Import2 (Attio's Native Migration Partner)
Attio has a built-in "Migrate" button that redirects to Import2. You connect both your source CRM and Attio, review auto-mapped fields, run a sample migration (up to 100 accounts), then execute the full migration.
When to use it: Small-to-medium datasets where you don't need to merge with existing Attio data.
The Import2 Limitation: Import2 creates new records but will not update attribute values on records that already exist in Attio. If your team has already connected email inboxes (which auto-creates People and Company records from email history), you must delete all existing records in Attio — including those created by mailbox sync — before running Import2. This means disconnecting all inboxes, purging the workspace, running the migration, then reconnecting inboxes. (attio.com)
Import2 also cannot import into Attio Lists — only into objects. And Attio's CSV importer does not support importing notes, though you can use Import2 for notes via its API-based path by contacting their support.
Native CSV Export/Import
Export each entity from Pipedrive as CSV/XLSX (via Tools > Export Data), then import each file into the corresponding Attio object. (support.pipedrive.com)
When to use it: Quick, low-complexity migrations where relationships don't matter or you can rebuild them manually.
Limitations: CSV flattens relationships. A Pipedrive Deal CSV row might include an organization name column, but importing it into Attio Deals won't automatically create or link to a Company record. Notes cannot be imported via CSV. Activity history is lost. Attio CSV imports top out at 100,000 rows per file, and multi-select values are additive during CSV updates, which can produce unexpected merged values. For why CSV-based migrations break multi-object relationships, see Using CSVs for SaaS Data Migrations.
Direct API Script (Python / Node.js)
Write extraction scripts against Pipedrive's REST API, transform the data, and load into Attio via the Assert endpoint.
When to use it: Datasets over 10K records, complex custom field mappings, multi-object relationships that must be preserved, or when existing Attio records (from inbox sync) cannot be deleted.
Advantages: Full control over transformation logic. You can implement upsert logic using Pipedrive legacy IDs as matching attributes, preserving existing Attio records. You handle activity classification, field-type transformation, and relationship linking in code.
Risks: You own rate limiting, retries, logging, QA, and rollback logic. Building a loader that creates records is easy. Building one that is idempotent, resumable, and auditable is harder.
Custom ETL Pipeline
Extract raw Pipedrive data into a staging store. Persist crosswalk tables from source IDs to Attio record IDs. Run typed transforms, validation rules, and relationship rebuild jobs. Queue Attio writes with retry and dead-letter handling.
When to use it: Enterprise or regulated migrations, large archives, multiple cutover rehearsals, or when you need ongoing sync during a phased rollout.
Trade-off: Highest engineering cost and most moving parts, but best auditability and control over partial reruns and rollback. Overkill for a tiny workspace. Exactly right for a messy one. (pipedrive.readme.io)
iPaaS / Middleware (Zapier, Make)
Build multi-step automations that trigger on Pipedrive records and create/update corresponding Attio records.
When to use it: Ongoing sync between Pipedrive and Attio during a coexistence window. Not suitable for historical bulk migration.
Limitations: These tools are designed for event-driven, record-at-a-time processing. Migrating 20,000 historical records through Zapier means 20,000+ task executions — slow, expensive, and fragile. No built-in pagination, no batch processing, no error aggregation. Zapier's Create or Update Record action still depends on a unique matching attribute, and Zapier cannot currently link tasks to a record in Attio. (attio.com)
Comparison Matrix
| Method | Complexity | Relationships preserved | Handles existing records | Notes/Activities | Best for |
|---|---|---|---|---|---|
| Import2 | Low | Partial | ❌ (requires delete) | Notes only | Small, greenfield migrations |
| CSV | Low | ❌ | N/A | ❌ | Quick record dump |
| Direct API script | High | ✅ | ✅ (via Assert upsert) | ✅ | Full-fidelity migration |
| Custom ETL | High | ✅ | ✅ | ✅ | Enterprise / phased cutovers |
| iPaaS | Medium | Partial | ✅ | Limited | Ongoing sync only |
| Managed service | Low (for you) | ✅ | ✅ | ✅ | Complex or time-constrained |
Practical recommendation:
- Empty Attio, simple data, low engineering bandwidth: Test Import2 first, then validate hard. (attio.com)
- Existing Attio workspace, connected inboxes, or nontrivial history: Skip Import2 and use API or managed ETL.
- One-time migration with complex data: Direct API script is usually enough if the team can build QA and retry logic.
- Ongoing sync after cutover: Middleware or ETL plus webhooks.
Pre-Migration Planning & Data Audit
Before writing any code or clicking any import buttons:
Data audit checklist
- Record counts by entity: Organizations, Persons, Deals (by pipeline), Leads, Activities, Notes, Products
- Custom fields per entity: List all custom fields, their types, and whether they're populated on >10% of records. Unpopulated fields are dead weight — don't migrate them.
- Duplicate analysis: Run Pipedrive's merge-duplicates feature on Organizations and Persons before migration. Duplicates in Pipedrive become duplicates in Attio, and they're harder to clean up after import.
- Domain and email quality: Organizations without a usable domain and People without a usable email address are the hardest records to handle. Attio can import them, but won't use them for enrichment or duplicate checking. (attio.com)
- File attachments: Count files linked to deals and contacts. Files require separate extraction — Pipedrive's global export does not include Google Drive stored files. (support.pipedrive.com)
- Archived records: Pipedrive has separate archived paths for deals and leads. Teams often forget these until validation fails.
- Inactive records: Deals closed >2 years ago, contacts with no activity — define whether these migrate or stay behind.
Define your Attio schema first
Don't start importing until your Attio workspace schema is finalized:
- Activate the standard objects you need (Deals, Workspaces, Users — they're off by default).
- Create custom objects if Pipedrive data doesn't fit standard objects (e.g., Products, Subscriptions).
- Create custom attributes on each object for Pipedrive fields that don't have a native Attio equivalent.
- Create a
pipedrive_idattribute (text, unique constraint) on Deals and each custom object you're migrating — this is your upsert key. Remember: you cannot add custom unique attributes to standard Companies or People. (attio.com) - Pre-create select/status options for pipeline stages and enum fields.
Migration strategy
- Big bang: Freeze Pipedrive on a weekend, run the full migration, validate, cut over Monday. Best for teams with <50K total records and no ongoing deal activity that can't wait 48 hours.
- Phased: Migrate Companies and People first (static data). Then Deals. Then Activities and Notes. Validate each phase before starting the next.
- Incremental with coexistence: Keep both systems live. Use an iPaaS for real-time sync of new records. Run a historical backfill in parallel via API scripts. Cut over once validation passes. Pipedrive v2 list endpoints support
updated_sincefilters for delta loads. Highest engineering effort, lowest business disruption.
Handling Pipedrive API Rate Limits & Extraction
If you choose an API-based approach, your primary hurdle on the source side is Pipedrive's rate limiting.
Token-based rate limiting
Pipedrive uses a token-based rate limiting (TBRL) system. The daily token budget formula is:
30,000 base tokens × subscription plan multiplier × number of seats (plus any purchased top-ups)
Each API endpoint consumes a different number of tokens based on complexity. Lightweight GET endpoints cost fewer tokens; data-intensive filtered queries cost more. On top of the daily budget, burst rate limits apply on a rolling 2-second window per user/token — for OAuth apps, burst limits range from 80 requests per 2 seconds on Lite to 480 on Ultimate. Exceeding burst limits returns a 429 Too Many Requests that can escalate to a 403 CDN block if you keep retrying aggressively. (pipedrive.readme.io)
The Search API has its own stricter burst limit: 10 requests per 2 seconds per token, regardless of plan.
Extraction architecture
For a migration, you're doing a one-time bulk read — not ongoing sync. Here's how to stay within limits:
- Use v2 endpoints where available. Pipedrive's v2 API offers cursor-based pagination, stricter validation, RFC 3339 timestamps, and lower token costs. Fall back to v1 only for entities not yet on v2 (Notes, Files, some mail endpoints).
- Paginate with
cursor(v2) orstart+limit(v1). Fetch in batches of 100–500 records. (pipedrive.readme.io) - Respect rate-limit headers. Pipedrive returns
X-RateLimit-Remaining,X-RateLimit-Reset, and token budget headers. Implement exponential backoff with jitter on 429s. - Separate extraction passes. Export Organizations, Persons, Deals, Activities, Notes, and Files in separate sequential passes. Activities, notes, and files linked to deals and contacts must be exported separately — Pipedrive's global export does not bundle them.
- Cache custom field definitions first. Call the fields endpoint for each entity type and store the hash-to-label mapping before extracting any records.
- Extract archived records separately. Pipedrive has separate archived paths for deals and leads — don't assume active list endpoints are enough.
import requests
import time
PD_API_TOKEN = "your_pipedrive_api_token"
BASE_URL = "https://yourcompany.pipedrive.com/api/v2"
def extract_all(entity: str, updated_since: str = None) -> list:
"""Extract all records for a Pipedrive entity with cursor-based pagination."""
records = []
cursor = None
while True:
params = {"api_token": PD_API_TOKEN, "limit": 500}
if updated_since:
params["updated_since"] = updated_since
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE_URL}/{entity}", params=params)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 2))
time.sleep(retry_after)
continue
resp.raise_for_status()
data = resp.json()
records.extend(data.get("data", []))
cursor = data.get("additional_data", {}).get("next_cursor")
if not cursor:
break
return records
# Extract in dependency order
orgs = extract_all("organizations")
persons = extract_all("persons")
deals = extract_all("deals")Monitor your daily budget. Check the API Usage Dashboard in Pipedrive's Company Settings before starting. A 10-seat Professional plan gives ~300,000 tokens/day. A migration extracting 50,000 records across all entity types can consume 30–60% of that budget in a single day.
Loading Data into Attio: The Assert Endpoint
Attio's Assert endpoint (PUT /v2/objects/{object}/records) is the correct tool for migration writes. It functions as an upsert: provide a matching_attribute, and Attio will update an existing record if found, or create a new one if not.
For People, match on email_addresses. For Companies, match on domains. For Deals and custom objects, use the custom pipedrive_id attribute you created with a unique constraint.
import requests
import time
ATTIO_API_KEY = "your_attio_api_key"
ATTIO_BASE = "https://api.attio.com/v2"
def assert_company(org: dict) -> dict:
"""Upsert a Company record in Attio using domain as the match key."""
root_domain = normalize_root_domain(org.get("web_domain"))
values = {
"name": [org.get("name", "")],
"pipedrive_org_id": [str(org["id"])]
}
if root_domain:
values["domains"] = [root_domain]
resp = requests.put(
f"{ATTIO_BASE}/objects/companies/records",
headers={"Authorization": f"Bearer {ATTIO_API_KEY}",
"Content-Type": "application/json"},
params={"matching_attribute": "domains"},
json={"data": {"values": values}}
)
else:
# Fallback: create without match key — cannot deduplicate later
resp = requests.post(
f"{ATTIO_BASE}/objects/companies/records",
headers={"Authorization": f"Bearer {ATTIO_API_KEY}",
"Content-Type": "application/json"},
json={"data": {"values": values}}
)
if resp.status_code == 429:
time.sleep(float(resp.headers.get("Retry-After", 1)))
return assert_company(org)
resp.raise_for_status()
return resp.json()That fallback branch is the hard part. Because standard Companies and People do not support custom unique attributes, records without a domain or email need either an external lookup strategy or a different target model. (attio.com)
Attio API constraints to know
- Write rate limit: 25 requests/second. Read rate limit: 100 requests/second.
- No batch write endpoint. Every record assert/create is a single API call. For 20,000 records, a single-threaded import at 20 req/sec takes roughly 17 minutes per object type — manageable for most migrations.
- Score-based rate limits: The List Records and List Entries endpoints use a complexity score based on sorts, filters, and total record count. Complex queries can trigger per-query limits even below the requests/second threshold.
- Attributes cannot be deleted via API — only archived. Plan your schema carefully before migration.
logo_urlon Companies cannot be set via API. Logos come from enrichment only.- Object limits by plan: Free: 3, Plus: 5, Pro: 12, Enterprise: unlimited. Companies and People count toward these limits.
Load order matters. Attio doesn't automatically create referenced records — the record you're referencing must already exist. Import Companies first, then People (with company relationship references), then Deals (with people and company references). If you reverse the order, reference writes will fail silently.
Assert endpoint gotcha for Deals: Attio's Deal object has no default unique attribute. If you call the Assert endpoint without specifying a matching attribute that has a unique constraint, every call creates a new record. Create and populate a pipedrive_deal_id attribute first.
Edge Cases That Break Most Migrations
The former-employee email trap
Attio automatically pulls in email history when users connect their inboxes. For active team members, this means historical correspondence resurfaces in Attio without any migration effort.
The gap: correspondence from employees who have since left the company and disconnected their email will not come back. Those emails were in Pipedrive because the former employee's mailbox was connected to Pipedrive at the time. Once they left and their inbox was disconnected, that interaction history became orphaned. (attio.com)
If preserving full communication history matters (for deal context or compliance), you need to extract those activities and notes from Pipedrive's API and inject them into Attio as Notes linked to the appropriate People and Company records. This is one of the highest-value aspects of an API-based migration — and one that Import2 and CSV approaches can't address.
Duplicate records from inbox sync
If your team has already connected inboxes to Attio before migration, People and Company records already exist from email auto-detection. Running Import2 or a CSV import on top of these creates duplicates. The Assert API endpoint solves this — match on email or domain and upsert — but Import2 cannot do this. You either delete existing records first (risky) or use the API approach.
Custom field type mismatches
Pipedrive's "monetary" field type includes a currency code per value. Attio's currency attribute requires the currency to be set at the attribute level, not per-value. If your Pipedrive deals use mixed currencies, you'll need separate Attio attributes per currency or a transformation layer.
Pipedrive "set" fields (multi-option select) map to Attio "multi-select" attributes, but option IDs won't match — you need a label-based mapping.
Attachments and file migration
Pipedrive stores file attachments that can be downloaded via the API (GET /v1/files/{id}/download). Attio's API doesn't have a direct file-upload endpoint for record attachments in the same way. Files typically need to be hosted externally (S3, Google Drive) and linked as URLs on Attio records. Pipedrive's global export does not include Google Drive stored files. Treat attachment migration as its own proof-of-concept workstream, not a footnote. (support.pipedrive.com)
Records without domain or email
Organizations without a usable domain and People without a usable email address are the hardest records in a Pipedrive to Attio migration. Attio can import them, but it will not use them for enrichment or duplicate checking, and you cannot add custom unique attributes to standard Companies or People. These records need either an external lookup strategy, a different target model, or upfront data cleanup.
Archived pipeline data
Do not assume active list endpoints are enough. Pipedrive has separate archived paths for deals and leads, and teams often forget them until validation fails. Extract archived records in a separate pass during the extraction phase.
Step-by-Step Migration Process
A successful API-based migration follows a strict order of operations:
- Extract and map users. Extract Pipedrive users and map their IDs to Attio workspace members. This ensures historical record ownership is preserved.
- Cache field definitions. Call Pipedrive's fields endpoint for each entity type to build the custom field hash-to-label mapping.
- Migrate Companies. Push Organizations to Attio using domain matching. Capture the newly generated Attio record IDs and map them against the legacy Pipedrive org IDs in a crosswalk table.
- Migrate People. Push Persons to Attio using email matching, with company relationship references from the crosswalk table.
- Migrate Custom Objects. Push any normalized custom object data, using the unique identifier fields established during planning.
- Migrate Deals. Push Pipedrive Deals into Attio using your
pipedrive_deal_idmatching attribute. Associate them with the correct Companies and People via the crosswalk table. - Migrate Activities and Notes. Classify Pipedrive activities by type. Route notes and completed interactions to Attio's Notes API. Route open work to the Tasks API. Include historical emails from former employees. Link each to the correct parent record.
- Validate. Run the full validation suite before declaring the migration complete.
Migration script outline
A production-grade migration usually needs more structure than a flat script:
class PipedriveExtractor:
"""Handles Pipedrive API extraction with rate-limit management."""
def __init__(self, api_token: str, base_url: str):
self.api_token = api_token
self.base_url = base_url
self.field_maps = {} # entity -> {hash: label}
def load_field_definitions(self, entity: str):
"""Cache custom field hash-to-label mappings."""
pass
def extract_all(self, entity: str) -> list:
"""Paginated extraction with retry on 429."""
pass
class AttioLoader:
"""Handles Attio API writes with upsert logic."""
def __init__(self, api_key: str):
self.api_key = api_key
self.id_maps = {} # pipedrive_id -> attio_record_id
def assert_record(self, object_slug: str, matching_attr: str,
values: dict) -> str:
"""Upsert a record, return Attio record_id."""
pass
def create_note(self, parent_object: str, parent_record_id: str,
title: str, body: str):
"""Create a Note linked to a record."""
pass
class Transformer:
"""Maps Pipedrive fields to Attio attributes."""
def transform_org(self, org: dict) -> dict: pass
def transform_person(self, person: dict, company_id_map: dict) -> dict: pass
def transform_deal(self, deal: dict, company_id_map: dict,
people_id_map: dict) -> dict: pass
def run_migration():
extractor = PipedriveExtractor(PD_TOKEN, PD_URL)
loader = AttioLoader(ATTIO_KEY)
transformer = Transformer(field_maps, stage_map, user_map)
# Phase 1: Companies
orgs = extractor.extract_all("organizations")
for org in orgs:
values = transformer.transform_org(org)
record_id = loader.assert_record("companies", "domains", values)
loader.id_maps[f"org_{org['id']}"] = record_id
# Phase 2: People
persons = extractor.extract_all("persons")
for person in persons:
values = transformer.transform_person(person, loader.id_maps)
record_id = loader.assert_record("people", "email_addresses", values)
loader.id_maps[f"person_{person['id']}"] = record_id
# Phase 3: Deals
deals = extractor.extract_all("deals")
for deal in deals:
values = transformer.transform_deal(deal, loader.id_maps, loader.id_maps)
loader.assert_record("deals", "pipedrive_deal_id", values)
# Phase 4: Notes & Activities
notes = extractor.extract_all("notes")
for note in notes:
parent_key = (f"deal_{note['deal_id']}" if note.get('deal_id')
else f"person_{note['person_id']}")
parent_id = loader.id_maps.get(parent_key)
if parent_id:
loader.create_note(
"deals" if note.get('deal_id') else "people",
parent_id, "", note["content"]
)Store these artifacts between runs: source cursor/offset state, source-to-target ID crosswalks, validation errors by object and field, API retry/dead-letter logs, and the mapping version used for the run. That's the difference between a migration script and a migration system.
Validation & Testing
Validation is not optional. Every migration should include:
- Record count comparison. Total records per entity in Pipedrive vs. Attio, including archived subsets. Any discrepancy means dropped records.
- Field-level spot checks. Sample 50 records per entity and compare every field value against the Pipedrive source. Pay special attention to dates (timezone shifts), currency values, and multi-select fields.
- Relationship integrity. For a sample of Deals, verify that the linked Company and People records exist and are correct in Attio.
- Notes and activity count. For 20 randomly selected contacts, compare the number of notes/activities in Pipedrive vs. Attio.
- Pipeline stage distribution. Compare the count of Deals per stage in Pipedrive vs. Attio. Mismatches indicate a stage-mapping error.
- Sample ugly records. Don't just validate happy-path rows. Find records with missing domains, multiple emails, mixed currencies, or orphaned activities and verify they handled correctly.
Do not sign off on record counts alone. The failures that matter are stage mappings, owner mappings, duplicates, and broken associations.
Rollback plan
Before the final migration run:
- Export a full backup of Attio via the workspace export feature
- Document the exact state of the Attio workspace pre-migration
- If the migration fails validation, delete the migrated records using the Pipedrive legacy ID attribute as a filter, and re-run
- Keep Pipedrive read-only (don't delete it) for at least 30 days post-migration
Sample Field Mapping Table
| Pipedrive Field | Pipedrive Type | Attio Attribute | Attio Type | Notes |
|---|---|---|---|---|
name (Org) |
varchar | name |
Text | Direct map |
address (Org) |
varchar | primary_location |
Location | Parse into structured fields |
owner_id (Org) |
int | owner |
Workspace member | Map Pipedrive user IDs to Attio member IDs |
name (Person) |
varchar | name |
Personal name | Split into first_name / last_name |
email (Person) |
array | email_addresses |
Multi-value; lowercase and dedupe before loading | |
phone (Person) |
array | phone_numbers |
Phone | Restructure from label-based to country-code-based |
org_id (Person) |
int | company |
Record reference | Map to Company record_id via crosswalk |
title (Deal) |
varchar | name |
Text | Direct map; required in Attio |
value (Deal) |
monetary | deal_value (custom) |
Currency | Set currency at attribute level |
stage_id (Deal) |
int | stage |
Status | Map stage IDs to Attio status options |
person_id (Deal) |
int | associated_people |
Record reference | Map to People record_id; supports multiple |
org_id (Deal) |
int | associated_company |
Record reference | Map to Company record_id |
add_time (Deal) |
datetime | created_date (custom) |
Date | Preserve original timestamp |
note (Note) |
text/html | Note body | Note (via API) | HTML-to-Markdown transform; link to parent record |
Post-Migration Tasks
After the data lands:
- Reconnect inboxes. If you disconnected inboxes for Import2, re-sync all team members' Google or Microsoft accounts. Attio will backfill email history for active mailboxes.
- Rebuild automations. Pipedrive workflow automations don't transfer. Rebuild them in Attio's workflow builder or via the API.
- Configure pipeline views. Set up Kanban and list views for each pipeline stage in Attio. Attio Deals ship with default stages — map these to your actual pipeline before the team starts working.
- Team training. Attio's UI and mental model differ from Pipedrive. Run a focused walkthrough covering: record navigation, relationship attributes, list views, and the notes/tasks workflow.
- Monitor for 2 weeks. Watch for missing data, broken relationships, and user-reported inconsistencies. Keep the Pipedrive account read-only for at least 30 days post-migration.
Why Choose a Managed Migration Service
Building a custom ETL pipeline for a one-off CRM migration is a real engineering investment. A typical Pipedrive-to-Attio migration with 30+ custom fields, 20K+ records, activities, and notes takes 40–80 engineering hours to build, test, and validate — plus the opportunity cost of pulling an engineer off product work for 1–2 weeks.
The hidden cost of DIY is rarely the first successful import. It's the second run under real load — when you discover edge cases in currency handling, orphaned activities from archived deals, and records without domains that silently created duplicates.
Where a managed service earns its keep:
- Safe upserts into live Attio workspaces — merging historical Pipedrive data with existing inbox-synced records without deleting anything.
- Former-employee email extraction — recovering historical correspondence from churned reps that Attio's native inbox sync can't reach.
- Rate limit management across both platforms with proper throttling, retry logic, and dead-letter handling.
- Import ordering and ID mapping that must be exact — one bug in the Company → People → Deal chain breaks all relationships.
- Systematic validation — record counts, field-level spot checks, relationship integrity — not ad hoc checks.
At ClonePartner, we've built reusable infrastructure for exactly this class of problem. Our Inuka case study is a concrete example: a two-day, error-free migration to Attio where native importers couldn't handle the data complexity.
What to Do Next
- Audit your Pipedrive data. Run record counts, identify custom fields worth migrating, and flag duplicates and records missing domains or emails.
- Design your Attio schema. Don't import into a default workspace. Define objects, attributes, unique keys, and relationships first.
- Pick your method. If your dataset is under 5K records with no custom objects and you haven't connected inboxes yet, Import2 may work. Anything more complex warrants an API-based approach or a managed service.
- Run a test migration. Whatever method you choose, migrate a 100-record sample first and validate thoroughly before the full run.
- Plan for coexistence. Keep Pipedrive read-only for at least 30 days after cutover.
Frequently Asked Questions
- Can I use Import2 to migrate from Pipedrive to Attio without losing existing data?
- No. Import2 creates new records but cannot update attributes on records that already exist in Attio. If your team has connected email inboxes (which auto-creates People and Company records), you must delete all existing records before running Import2. For teams that can't lose inbox-synced data, an API-based migration using Attio's Assert (upsert) endpoint is the correct approach.
- What are the API rate limits for Pipedrive and Attio during migration?
- Pipedrive uses a token-based system: 30,000 base tokens × plan multiplier × seats per day, with burst limits on a rolling 2-second window (80 requests/2s on Lite up to 480 on Ultimate). The Search API is stricter at 10 requests per 2 seconds. Attio allows 100 read requests/second and 25 write requests/second, with score-based limits on complex list queries.
- How do Pipedrive Deals map to Attio Deals?
- Pipedrive Deals map to Attio's Deals object, but Attio Deals have no default unique attribute. You must create a custom attribute (e.g., pipedrive_deal_id) with a unique constraint before using the Assert API. Pipeline stages map to Attio's status attribute — pre-create matching stage options and build a stage-name mapping in your transformation layer.
- What happens to email history from former employees during migration?
- Attio pulls in email history when active users connect their inboxes, so current employees' correspondence backfills automatically. Emails from employees who have left the company and disconnected their inbox won't resurface through sync. That interaction history must be explicitly extracted from Pipedrive's API and imported into Attio as Notes.
- How long does a Pipedrive to Attio migration take?
- It depends on the method and dataset size. A small Import2 migration (<5K records) can finish in a day. An API-based migration of 20K+ records with custom fields, activities, and relationship mapping typically takes 40-80 engineering hours for a custom build, or 2-5 days with a managed service.