Posted by
Tejas Mondeeri
on
Sep 5, 2025
Affinity to Attio Migration: An In-Depth Definitive Guide
From schema setup to entities, lists, deals, interactions, notes, and files, the ultimate Affinity to Attio migration playbook
Moving from Affinity to Attio is more than a simple export and import. Affinity organizes everything around lists and list entries, with key nuances like opportunities belonging to a single list and rich timelines of interactions and notes that you will want to preserve.
Attio models data as objects and records that you add to lists as entries, with first-class APIs for records, list entries, notes, and threads, which makes it a strong destination for a clean, API-driven migration.
This guide walks through how to map each concept one-to-one, handle attachments and ownership, and execute a zero-downtime cutover with reconciliation and deltas.
Complexity score: 3.5/5
Step 1: Determine the migration scope
Start by defining exactly what will move, what will not, and why. A precise scope reduces surprises, shortens timelines, and keeps QA objective.
Scope levers to decide:
Time window: the created or updated date range to migrate.
Business unit or department: include only the records relevant to specific teams.
Compliance or residency: include only data that meets legal or contractual constraints.
Office or region: limit by geography when local rules or relevance apply.
Entities covered: People, Organizations, Opportunities, Lists, List Entries, Interactions, Notes, Files. Decide which are in scope now versus later.
Field coverage: include only the custom fields that matter to operations and reporting.
Tags or labels: move records that match specific tags, skip the rest.
Ownership filters: include records owned by specific users or teams only.
Attachments: decide which file types and size limits are in scope.
Interaction history depth: full history, last N months, or none.
Archived or deleted records: include or exclude, and how to handle reinstates.
Step 2: Finalize the approach
Once the scope is clear, the next step is deciding how the migration will run. For Affinity to Attio, the recommended approach is to go API-first. Both platforms expose capable APIs that allow you to move not just the core records, but also the relationships, notes, interactions, and files that make up the complete history. This API first approach ensures data integrity and fidelity that a simple CSV export can’t match.
CSV or JSON exports can serve as fallback options if specific datasets cannot be accessed through the APIs, but they should be limited to those edge cases. By relying primarily on APIs, you get repeatability, safer retries, and the ability to preserve connections between people, companies, opportunities, and lists.
The goal at this stage is to commit to one primary method, document any exceptions, and make sure everyone is aligned before actual migration work begins. This keeps execution predictable and avoids unnecessary rework later.
Step 3: NDA and platform access
Before starting the migration, make sure an NDA or equivalent agreement is in place (if opting to go with a third-party organization for migration) so that the handling of sensitive data is clearly defined. The NDA should cover what data can be accessed, how long it can be retained, and how it must be protected.
Next, set up secure access to both platforms. Generate API credentials or tokens specifically for the migration, restrict them to the minimum required permissions, and store them securely. Always test with sandbox or trial environments first, and only switch to production once the setup is verified.
Step 4: Analyze the data and finalize the mapping
This step is about confirming what exists in Affinity and locking the one-to-one mapping you will implement in Step 5.
Focused mapping and migration plan:
Source (Affinity) | Target (Attio) | Mapping Notes |
---|---|---|
Person (internal and external) | User for internal, Person for external | Build an email-based crosswalk. Match internal Affinity people to Attio Users by email. External contacts become Attio People. |
Organization | Company | Key on the normalized domain where possible |
Opportunity | Deal | Preserve stage, value, currency, and dates. Respect Affinity’s single-list origin when recreating list membership. |
List | List | Recreate target lists. Document inclusion criteria if lists are filtered. |
List Entry | List Entry | Attach each record to the correct list. Keep created and updated timestamps if available. |
Field | Attribute | Attio attributes can be applied to objects and lists. Map Affinity global fields to object attributes. Map list-specific fields to list attributes. Preserve types and required flags. |
Person ↔ Organization link | People ↔ Company link | Rebuild after both sides exist. Preserve roles if present. |
Opportunity ↔ Person link | Deal → People link | Link all participating people to a deal. Attio does not have a people → deal link |
Opportunity ↔ Organization link | Deal → Company link | Link all associated companies to a deal. Attio does not have a company → deal link |
Interactions (email, meeting, call, chat) | Email and meetings via Attio’s native sync. Calls and chats as Notes | Allow Attio’s email and calendar connections to backfill and keep syncing. Convert calls and chats into Notes with tags like |
Notes (often attached to an interaction) | Notes | For each Affinity note tied to an interaction, create one Attio Note and attach it to all People on that interaction. Optionally, also attach to the related Company or Deal for a fuller context. Preserve author, body, and created time. |
Entity Files | External storage plus Attio links | Upload files to Google Drive or other cloud storage in neat, per-entity folders. In Attio, link these folders to relevant records. |
Thinking about making the move yourself?
If you’d rather have a migration team handle the schema prep, mapping, and cutover with zero downtime, we’ve built a dedicated Affinity → Attio migration service. Learn more here.
Step 5: Building the migration script
Prepare the target schema in Attio
Before migrating any records, it is critical to make sure the destination objects in Attio are ready to accept data from Affinity. This step ensures that every field you plan to move has a proper place, reducing surprises later when records are created or updated.
Enable the Users object
By default, the Users object in Attio is disabled. Only an admin can enable it. Since Affinity’s persons include both internal people (team members) and external people (contacts), enabling Users is essential so that internal staff can be represented appropriately. Without this step, internal Affinity persons would have nowhere to land in Attio.
Sync the Company schema
Affinity organizations will map to Attio Companies. To prepare:
Read Affinity organization fields (default + global)
Fetch the organization fields from Affinity v1
GET https://api.affinity.co/organizations/fields
Add missing attributes in Attio
For each field in the combined schema that does not exist on the Attio Company object, create a new object attribute.
POST /v2/objects/companies/attributes
At this point, Attio Companies will have all the attributes needed to store both standard and custom global fields from Affinity organizations.
Sync the People and Users schema
Affinity persons will map to both Attio People (external contacts) and Users (internal staff). To prepare:
Read Affinity person fields (default + global)
Fetch the person fields from Affinity v1.
GET https://api.affinity.co/persons/fields
Add missing attributes in Attio
For external contacts: add missing fields to the People object in Attio.
POST /v2/objects/people/attributes
For internal staff: add the relevant subset of fields to the Users object in Attio.
POST /v2/objects/users/attributes
After this step, both Attio People and Users are prepared to receive data. External contacts from Affinity will map into People, and internal team members will map into Users, with all global fields preserved.
Sync the Deals schema
Affinity opportunities will map to Attio Deals. To prepare:
Read Affinity opportunity fields
Fetch the opportunity entity schema from Affinity v1.
GET https://api.affinity.co/opportunities/fields
Add missing attributes in Attio
Create new object attributes on the Attio Deal object for every field that does not already exist.
POST /v2/objects/deals/attributes
With this step, Attio Deals are schema-aligned to hold all necessary data from Affinity opportunities.
Note: Only default + global fields are mapped at this stage. List-specific attributes will be added later when lists are rebuilt.
Migrate organizations to companies
Once the Company schema in Attio has been prepared with all the required attributes, the next step is to populate it with organization records from Affinity. Since Affinity separates entity data from field values, you need to combine these before writing to Attio.
Read organizations from Affinity.
Start by fetching the organization data:
List all organizations:
GET https://api.affinity.co/organizations?page_token={next_page_token}
Use the
next_page_token
for pagination until all records are retrieved.
Fetch field values per organization
Global field values must be pulled separately for each organization:
Fetch field values for an organization:
GET https://api.affinity.co/field-values?organization_id={ORG_ID}
This call returns all values tied to that organization, including both global and list-specific fields. For this step, filter down to the global fields you intend to map to Attio Companies.
Merge base data and global field values.
Combine the entity data from /organizations with the field values from /field-values. The result should be a complete representation of each organization, with both its standard attributes (name, domain, etc.) and its global attributes (industry, founded date, etc.).
Assert into Attio Companies
Finally, write each combined record into Attio:
Assert company record:
PUT https://api.attio.com/v2/objects/companies/records
Use an idempotent Assert so you can safely rerun the migration without creating duplicates. A unique attribute, such as domain or a dedicated external_id should be chosen as the matching key.
After completing this step, all Affinity Organizations are mirrored in Attio Companies with both their core fields and global fields intact. This lays the foundation for migrating People, Users, Deals, and the rest of the data model in subsequent steps.
Migrate persons to people and users
With the People and Users schema in Attio prepared, the next step is to migrate persons from Affinity. Affinity models both external contacts and internal staff under persons, so you must split them into two groups: external persons to Attio People, and internal persons to Attio Users.
Read persons from Affinity.
Start with the base person data:
List all persons:
GET https://api.affinity.co/persons?page_token={next_page_token}
Paginate using
next_page_token
until all records are retrieved.
Each person record includes id, type, primary_email, emails[], and other core attributes.
type
= 0 → external contacttype
= 1 → internal team memberFetch field values per person
Field values must be retrieved separately for each person:
Fetch field values for a person:
GET https://api.affinity.co/field-values?person_id={PERSON_ID}
This provides the actual values for the global and list-specific fields. For this step, filter down to global fields to populate object attributes.
Merge base data and field values.
Combine the entity data from /persons with the field values from /field-values. The result is a complete profile for each person, including both standard attributes (name, email, etc.) and global attributes (like job title, seniority, etc.).
Assert into Attio People and Users
External persons (type 0) → Attio People
Use
PUT /v2/objects/people/records
to assert each external contact into the People object. Match on email_addresses or a custom external_id to ensure idempotency.Internal persons (type 1) → Attio Users
Use
PUT /v2/objects/users/records?matching_attribute=primary_email_address
to assert staff into the Users object. Match by email address and include any relevant attributes created earlier.In parallel, query Attio workspace members (GET /v2/workspace_members) to build a crosswalk by email. This ensures that migrated internal persons are matched with real Attio workspace accounts for authorship and ownership.
Note: If the Affinity person record included an organization_id, set the company attribute when asserting the Person. This automatically places the person into the correct Company team in Attio, without any extra API calls.
After this step, Affinity persons are cleanly split: external contacts become Attio People, and internal staff become Attio Users. With both groups asserted into Attio, you are ready to migrate deals, lists, and relationships that link them together.
Migrate opportunities to deals
Read opportunities from Affinity.
Start with the base opportunity data:
List all opportunities:
GET https://api.affinity.co/opportunities?page_token={next_page_token}
Paginate using
next_page_token
until all records are retrieved.
This gives you the core attributes you’ll need to map to Deals.
Fetch field values per opportunity
To enrich each opportunity with its field values:
Fetch field values for an opportunity:
GET https://api.affinity.co/field-values?opportunity_id={OPPORTUNITY_ID}
This will return all the values tied to the opportunity. Filter them to the attributes you intend to map to Attio Deals (object attributes). List-specific values will be handled later when rebuilding list entries.
Merge base data and field values.
Combine the entity data from /opportunities with the field values from /field-values. The result should be a complete record: core attributes like stage and value, plus any custom attributes defined in your Affinity schema.
Assert into Attio Deals
Finally, assert each opportunity as a Deal in Attio:
Assert deal record:
PUT https://api.attio.com/v2/objects/deals/records
Use an idempotent Assert so the migration can be replayed safely. Choose a stable matching attribute, such as a dedicated external_id (for example, affinity:opportunity:{id}), to ensure uniqueness across runs.
Also set the relationships while asserting each Deal:
Associated Company:
If the Affinity opportunity had an organization_id, set the associated_company attribute on the Deal.
Owner (User) and Associated People:
Affinity opportunities do not have an owner field. For each Affinity opportunity, inspect its linked person_ids:
Resolve each linked person to Attio. If the person maps to an Attio User (internal), select one as the owner for the Deal and set the owner attribute.
For all linked persons that map to Attio People (external), include them in the Deal’s associated_people attribute.
If multiple Users are linked, pick a deterministic rule. For example, prefer the User whose email matches your primary sales domain, else the earliest created, else the first in sorted order. Document the rule in your runbook.
If no linked User exists, leave owner unset and only set associated_people.
Migrate Lists and list-entries
Read lists and their list-specific fields (Affinity)
Fetch the lists you’ll recreate, then enumerate each list’s own fields.
Lists:
GET /lists and GET /lists/{list_id}
.List-specific fields:
GET /fields?list_id={LIST_ID}
.
Create lists and list attributes (Attio)
Mirror each source list to the correct Attio object and add any missing list attributes.
Create list:
POST /v2/lists
.Create list attributes:
POST /v2/lists/{list}/attributes
.
Read list entries, then fetch their list-field values (Affinity)
Entries identify the parent record, but do not include the cells.
Entries:
GET /lists/{list_id}/list-entries?page_token={next_page_token}
Per-entry list-field values:
GET /field-values?list_entry_id={ENTRY_ID}
. Exactly one identifier is allowed per call.
Assert entries with list-attribute values (Attio).
For each entry, assert it to the Attio list and include the list-attribute values you just fetched.
Assert entry:
PUT /v2/lists/{list}/entries
.
Notes:
Keep global fields on objects. Only list-specific columns become list attributes here
Opportunities in Affinity have one list entry. Preserve that 1:1 when asserting Deal entries.
This step ensures that every Affinity list is recreated in Attio with its structure intact. Lists carry over their list-specific fields as attributes, and each entry is asserted with the correct values and parent record links. By doing this at the list level, users in Attio can continue working with the same groupings and columns they were familiar with in Affinity.
Migrate Interactions, Notes, and Entity Files
Interactions
What to migrate
Affinity interaction types:
Meeting 0
,Call 1
,Chat message 2
,Email 3
.Plan: let Emails and Meetings surface automatically in Attio via workspace email/calendar sync. Convert Calls and Chats into Attio Notes tagged for context, linked to the correct records.
Read from Affinity v1
List interactions (filter by entity, type, and time window):
GET /interactions?{person_id|organization_id|opportunity_id}=&type=&start_time=&end_time=
. Max one year per request, use pagination as needed.Get one interaction:
GET /interactions/{id}?type={0|1|2|3}
. Returns participants, direction for chat/email, and attached note IDs.
Write to Attio
Emails and Meetings: do not write. Ensure Attio’s email/calendar sync is enabled so these surface automatically on People/Companies.
For Call and Chat: create an Attio Note on the appropriate record(s). Include tags like
interaction:call
orinteraction:chat
in the note body or attributes. In Attio, a note links to a single parent (via parent_object_id and parent_record_id), so create one note per record you want the history to be visible onCreate note:
POST /v2/notes
. Provide parent_object and parent_record_id to link to a Person, Company, or Deal.
Notes
What to migrate
Affinity Notes can be associated with people, organizations, opportunities, and optionally attached to an interaction. Fields include
person_ids
,organization_ids
,opportunity_ids
,interaction_id
, andinteraction_person_ids
.
Read from Affinity v1
List notes (filter by entity):
GET /notes?person_id=&organization_id=&opportunity_id=&page_token=
.Get one note (to inspect interaction linkage and participants):
GET /notes/{note_id}
.
Write to Attio
Create one Attio Note per target record that should show the note. If an Affinity note is tied to an interaction with multiple people, fan out one note per person (and optionally also on the Deal or Company).
Create note:
POST /v2/notes
with the appropriate parent_object and parent_record_id.
Entity Files
Affinity entity files do not have a direct one-to-one equivalent in Attio. Instead of trying to upload them into Attio, the cleaner approach is to externalize storage:
Export all files into neatly organized folders in a cloud storage service such as Google Drive or Dropbox. Create one folder per entity type (People, Companies, Deals), and within each, subfolders per record if needed.
Connect these folders to Attio by linking them back to the appropriate entities. For example, create a Note on a Person/Company/Deal that contains the URL to its cloud folder, or set a dedicated “file links” attribute that points to the folder.
This approach keeps attachments manageable, avoids hitting size limits, and leverages external storage for retention and auditing, while still keeping every file discoverable from within Attio.
Step 6: Run a sample migration
With the script ready, it’s time to test it on a small slice of data. Choose just enough records to cover all objects, one or two Companies, a handful of People, a Deal or two, and their associated lists and notes.
The point here isn’t volume, it’s validation. Ensure the data is imported into Attio with the correct fields, links, and history. Check that list-specific attributes appear where expected, notes show up under the proper records, and file links resolve correctly.
If anything looks off, adjust the mapping or script logic before moving on.
Step 7: Run complete migration
Once the sample passes inspection, move on to the entire dataset. This is the heavy run where everything you’ve prepared comes together: schema, mappings, relationships, lists, notes, and files.
The script will page through every record in Affinity and assert it into Attio. Keep an eye on progress, monitor for rate limits, and confirm that batch counts match expectations.
At the end of this step, your Attio workspace will contain the whole history of your Affinity workspace, ready for the final round of cleanup and QA.
Step 8: Post migration cleanup and QA
With the full migration complete, the last step is to tidy up and validate. This isn’t about moving more data; it’s about making sure everything landed where it should.
Start with the basics: compare counts between Affinity and Attio for Companies, People, Deals, and Lists. Next, perform spot checks by opening a few records in Attio and verifying that the links, notes, and file references are accurate.
Next, clear out duplicates that slipped through, review ownership on Deals, and confirm that Users have access to the records they need. Finally, let a pilot group of users test the workspace in real workflows. Their feedback is the ultimate sign-off that the migration was a success.
Our team has run dozens of Affinity and Attio migrations, and can help you map, test, and reconcile with zero disruption.
