Loxo to Loxo Migration: The CTO's Technical Guide
Technical guide to migrating data between Loxo instances. Covers API methods, CSV limits, field mapping, relationship preservation, and edge cases for same-platform ATS migrations.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Loxo to Loxo Migration: The CTO's Technical Guide
A Loxo-to-Loxo migration is a schema-alignment problem, not a platform translation. Because both source and target run the same Talent Intelligence Platform, the data model is identical — People, Companies, Jobs, Activities, Deals, Attachments — but every internal ID changes, every relationship must be re-linked, and every pipeline stage configuration in the target must be mapped manually. If your team assumes "same platform = drag-and-drop transfer," you will orphan activity histories, break candidate-to-job associations, and lose the pipeline context your recruiters depend on.
The typical failure mode: an engineering team exports Loxo data via CSV and pushes it into the new instance without maintaining a strict ID-mapping table (a persistent lookup that maps every source record ID to its corresponding target record ID). The result is flattened data, orphaned candidate activities, severed Company → Person → Job relationship chains, and duplicated records.
This guide covers the exact reasons teams need this migration, every viable method with its trade-offs, field-level mapping, API constraints, and the edge cases that silently corrupt recruiting data.
Why Companies Migrate Between Loxo Instances
A same-platform migration sounds unusual until you see the real-world triggers:
- M&A consolidation. Two recruiting firms merge and each runs its own Loxo account. Candidates, companies, jobs, and deal pipelines need to be unified into a single instance without duplicating records or losing attribution.
- Agency restructuring. A parent agency spins off a division into its own Loxo instance, or consolidates regional offices that were set up on separate accounts.
- Data hygiene reset. After years of accumulated stale data, broken automations, and duplicate records, some teams prefer migrating only active records into a clean instance rather than cleaning in-place.
- Subscription or contract changes. A team moves to a new contract under a different organizational entity, requiring a fresh instance.
- Compliance isolation. GDPR or regional data residency requirements force a team to separate EU candidate data into a distinct Loxo instance.
The common thread: the data model stays the same, but the internal record IDs, user assignments, pipeline configurations, and dynamic field definitions differ between instances.
Loxo Data Model: What You're Moving
Before evaluating migration methods, understand what Loxo stores and how it connects:
| Object | Description | Key Relationships |
|---|---|---|
| People | Candidate/contact profiles — name, emails, phones, social URLs, work history, education, tags, dynamic fields | Linked to Jobs (via Applications), Companies, Activities, Attachments |
| Companies | Client/employer records — company name, industry, location, contacts, dynamic fields | Linked to People, Jobs, Deals |
| Jobs | Open requisitions — title, description, status, type, pipeline stages, assigned recruiters | Linked to People (applications), Companies (client) |
| Activities/Events | Recruiter actions — calls, emails, interviews, submissions, notes, status changes | Linked to People, Jobs |
| Deals | Revenue tracking for agency placements — fee, status, client, candidate | Linked to People, Companies, Jobs |
| Placements | Hire records requiring person_id, job_id, and start_date |
Linked to People, Jobs |
| Attachments | Resumes, contracts, compliance docs | Linked to People, Companies |
| Lists | Saved candidate segments | References People by ID |
| Users | Recruiter accounts, ownership assignments | Referenced by all objects via owner fields |
Loxo's API supports Applications, Attachments, Candidates, Job Interview Stages, Jobs, Organizations, and Users as core resources.
A detail that matters for migration: Loxo distinguishes between contact-type and candidate-type People. Contact-type events appear on the company Activity Feed, while candidate-only events do not. If you collapse this distinction during migration, you lose visibility in the company view.
The critical insight: every relationship is ID-based. When you export from the source instance, those IDs mean nothing in the target. Your migration must maintain an ID-mapping table to re-link every record correctly.
Migration Approaches
1. Native CSV Export/Import
How it works: Export People and Companies from the source instance as CSV files via the List view export function. Clean and reformat CSVs to match the target's field configuration. Import into the target instance. Jobs must be handled separately — they cannot be imported via CSV.
When to use it: Small datasets (under 5,000 people) where you only need basic profile data and can accept loss of activity history, job associations, and attachments.
Constraints to know:
- The fields included in the exported CSV match what's visible on the People or Companies List view. Resumes, activity feed history, work experience history, and education profiles are not included in the export.
- Accounts with access to Loxo Source are limited to exporting 500 profiles sourced from Loxo Source per 30 rolling days. Source profiles exceeding this limit are excluded from the CSV.
- For large-scale exports of more than 15,000 contacts, Loxo may restrict exporting to one page at a time. Users requiring a complete export should contact Loxo's Support Team.
- CSV imports are limited to 10,000 or fewer records per file, with a character limit of 2,800 per individual cell.
- Exporting to CSV is a function reserved for agency admins.
Pros: No engineering effort. Immediate execution. Good for basic contact data.
Cons: Loses all activity history, notes, and event data. No job associations or pipeline positions transfer. No attachments (resumes, documents). Manual field remapping in the import wizard. Duplicate risk if not deduplicated beforehand.
Complexity: Low | Scalability: Small datasets only
2. Loxo's Managed Data Migration Service
How it works: Request a full data export from your source Loxo instance (coordinated through your CSM). Upload the export to the target instance's file-sharing site or provide access credentials. Schedule a pre-mapping call with a Loxo Data Migration Specialist. Complete the Import Mapping form — map activity types, job statuses, pipeline stages, phone/email types. Loxo's data team executes the migration and merges records.
Request a full data export from your source Loxo instance — this can take multiple weeks, so the quicker the export is requested the quicker you can get started.
When to use it: Any standard migration into Loxo where you want decent data fidelity and don't want to build custom tooling, provided timeline isn't critical.
Key details:
- A Free Data Migration is limited to person and company data, contact information, and attachments. Jobs, activities/events, and notes are not included in a Free Data Migration.
- A full (paid) migration includes jobs, pipeline history, activities, notes, and document attachments.
- Customize your pipeline in Loxo prior to completing Import Mapping. Admins do this under Settings > Customize Workflow. Then map old activity types to Loxo's event types.
Pros: Highest data fidelity among hands-off options. Loxo's team handles the work. Built-in deduplication and record merging.
Cons: Timeline depends on Loxo's migration queue (the export alone can take weeks). Limited control over transformation logic. Full migration may carry cost beyond the free tier. You are dependent on Loxo's schedule.
Complexity: Low (for you) | Scalability: Any size
3. API-Based Migration (Loxo API → Loxo API)
How it works: Generate API keys in both instances (Settings > API Keys). Pull reference data first — dynamic fields, job types, job statuses, workflow stages. Extract records from the source via REST API endpoints, map old UUIDs to new UUIDs in a staging database, and POST the payloads to the target instance following dependency order.
Access to Loxo's Open API is a paid feature. If you do not have an active subscription, you need to schedule a demo to learn more.
API tokens can be created and managed by navigating to Settings > API Keys. If the API Keys option isn't available, reach out to Loxo Support to have the feature enabled. Only account admins can access this page.
All Loxo API endpoints are formed using the Agency Domain and Agency Slug, and use bearer token authentication in the header.
When to use it: When you need full programmatic control, selective data migration, complex deduplication logic, or a repeatable delta sync. This is the only approach that gives you total control over what gets migrated and how it transforms.
Pros: Full control over data mapping. Can implement custom deduplication, field transformation, and validation. Supports selective migration (date ranges, tags, user ownership). Repeatable — can run test migrations before go-live.
Cons: Requires engineering resources and API subscriptions on both instances. Rate limits are not publicly documented — expect throttling on high-volume calls. Must manually manage dependency ordering. Attachment migration requires separate file download/upload handling.
Complexity: High | Scalability: Enterprise-grade with proper batching
4. Third-Party Migration Services
How it works: Engage a migration service provider that has experience with Loxo's data model and API. The provider handles extraction, transformation, loading, relationship rebuilding, and validation.
When to use it: When you need full data fidelity but lack internal engineering bandwidth, or when the migration involves complex merging logic — M&A consolidation, deduplication across two large databases, or tight cutover timelines.
Pros: Offloads all technical complexity. Providers with ATS migration experience understand relationship preservation. Can handle edge cases — duplicates, dynamic field mismatches, attachment linking. Typically faster than DIY API migration.
Cons: External cost. Requires sharing API credentials or data exports with a third party. Quality varies between providers — some use the same flawed CSV methods under the hood. Vet providers by asking whether they maintain an ID-mapping table and import in dependency order.
Complexity: Low (for you) | Scalability: Any size
5. Middleware/Integration Platforms (Zapier, Make)
How it works: Configure triggers and actions using Loxo's API through iPaaS platforms. Typically used for ongoing sync rather than bulk migration.
When to use it: Ongoing data sync between two Loxo instances (e.g., syncing newly created Companies from a sourcing instance to production). Not recommended for one-time bulk migration.
Pros: No-code/low-code setup. Good for continuous sync of new records.
Cons: Not designed for bulk historical data transfer. Record-by-record processing — extremely slow for large datasets. Cannot preserve complex relationships reliably. Limited transformation capabilities. Zapier/Make Loxo integrations may not cover all objects.
Complexity: Medium | Scalability: Small, ongoing sync only
Migration Approach Comparison
| Criteria | CSV Export/Import | Loxo Managed Migration | API-Based | Third-Party Service | Middleware |
|---|---|---|---|---|---|
| Data fidelity | Low — profiles only | High — full data | High — configurable | High — full data | Low — limited objects |
| Activity history | ❌ Lost | ✅ Preserved | ✅ Preserved | ✅ Preserved | ❌ Not practical |
| Attachments | ❌ Not included | ✅ Included | ✅ With extra work | ✅ Included | ❌ Not supported |
| Relationships | ❌ Broken | ✅ Rebuilt | ✅ With ID mapping | ✅ Rebuilt | ⚠️ Partial |
| Engineering effort | None | None | High | None | Low–Medium |
| Timeline | Hours | Weeks (Loxo queue) | Days–weeks (see estimates below) | Days | N/A for bulk |
| Cost | Free | Free tier (People, Companies, Attachments) / Paid tier (Jobs, Activities, Notes) | Dev time + API subscription fees on both instances | Service fee | Platform subscription |
| Best for | Quick contact transfer | Standard migrations | Custom/selective | M&A, complex merges | Ongoing sync |
Decision Tree: Choosing Your Migration Method
START: Do you need activity history, job associations, or attachments?
│
├─ NO → Is your dataset under 5,000 People?
│ ├─ YES → CSV Export/Import
│ └─ NO → Loxo Managed Migration (free tier)
│
└─ YES → Do you have engineering resources and API subscriptions?
├─ NO → Is your timeline flexible (4+ weeks)?
│ ├─ YES → Loxo Managed Migration (paid tier)
│ └─ NO → Third-Party Migration Service
│
└─ YES → Is this an M&A merge requiring cross-database deduplication?
├─ YES → Third-Party Service or Custom API + dedup engine
└─ NO → API-Based Migration (in-house)
Recommendations by Scenario
- Small agency, simple transfer (<5K records, no activity history needed): CSV export/import is sufficient.
- Standard migration, any size, flexible timeline: Loxo's managed migration service is the path of least resistance.
- M&A consolidation or multi-instance merge: Third-party migration service or custom API pipeline — deduplication and merge logic are too complex for native tools.
- Ongoing sync between instances: Middleware platform or custom API integration with webhooks.
- Dedicated dev team, selective migration: API-based pipeline gives maximum control.
Pre-Migration Planning
A migration is won or lost in the planning phase. Before touching the API, execute the following.
Data Audit Checklist
Inventory what's in your source instance:
| Object | Audit Action |
|---|---|
| People | Count total records. Identify duplicates (same email, same LinkedIn URL). Flag records with no activity in 24+ months. |
| Companies | Count records. Check for duplicates (same company name + location). Review dynamic field completeness. |
| Jobs | Count open vs. closed. Decide whether to migrate closed jobs or only active ones. |
| Activities | Count by type (calls, emails, notes, submissions). Decide cutoff date — do you need 5 years of history? |
| Deals | Count by status (open, won, lost). Verify fee amounts and client associations. |
| Placements | Count. Verify each has the required person_id, job_id, and start_date. |
| Attachments | Estimate total file size. Identify orphaned attachments not linked to any record. |
| Lists | Document all saved lists and their filter criteria. |
| Dynamic Fields | Export the full dynamic field configuration from both instances. Identify mismatches in names, types, and dropdown options. |
Define Migration Scope
Not everything should move. The most obvious solution is to copy over all historical data to the new ATS. However, Loxo advises against taking that approach. Be deliberate:
- Time-based filtering: Migrate only records active within the last 2–3 years.
- Status-based filtering: Skip candidates marked as "Do Not Contact" or "Unsubscribed" unless compliance requires retention.
- Owner-based filtering: In an M&A scenario, migrate only records owned by the team joining the target instance.
Migration Strategy
- Big bang: Export everything, import everything, cut over in one operation. Fastest, but highest risk. Works for small datasets.
- Phased: Migrate by object type — Companies first, then People, then Jobs, then Activities. Allows validation between phases.
- Incremental: Migrate historical data first, then run a delta migration for records created or modified during the migration window. Best for large datasets where migration takes multiple days. If you need delta sync, plan around Loxo's webhooks from day one.
Overlapping data in the target: If the destination Loxo instance already contains data, you must define a strict deduplication strategy before migrating. Decide whether the source or target record takes precedence when a candidate email matches.
Data Mapping: Loxo Source → Loxo Target
Since both instances run Loxo, the schema is structurally identical. But configuration-level differences are where migrations break.
Terminology Note: Dynamic Fields
Loxo's API refers to custom fields as dynamic fields. Throughout this guide, "dynamic fields" and "custom fields" refer to the same thing — user-defined fields on People and Company records, configured under Settings > Customize and accessed via the API's dynamic field endpoints.
Object Mapping
| Source Object | Target Object | Mapping Notes |
|---|---|---|
| People | People | Direct 1:1. Dynamic fields must be created in target before import. Preserve person type (contact vs. candidate). |
| Companies | Companies | Direct 1:1. Match on company name + location to deduplicate. |
| Jobs | Jobs | Pipeline stages may differ — map source stages to target stages. Requires job_type_id, raw_company_name, and title when creating via API. |
| Activities/Events | Activities/Events | Activity types must be mapped. Intake notes are not person_events — they live on the person description field. AI Notetaker transcripts are stored separately from person_events. |
| Deals | Deals | Fee structures and deal stages may differ between instances. |
| Placements | Placements | Requires person_id, job_id, and start_date. Resolve to new IDs. |
| Attachments | Attachments | Must be linked to correct People/Company records post-import. Files are separate API endpoints (person documents, resumes, job documents, person-event documents). |
Field-Level Mapping (People)
The following table documents the transformation logic for core People fields. Note: Loxo's API documentation should be consulted for the exact accepted field names on the POST /people endpoint, as field naming conventions may change across API versions.
| Source Field | Target Field | Transformation Required |
|---|---|---|
| First Name | First Name | Trim whitespace. |
| Last Name | Last Name | Trim whitespace. |
| Email (Work) | Email (Work) | Validate format. Map email types if they differ between instances. |
| Email (Personal) | Email (Personal) | Map type if custom types were used. |
| Phone (Mobile) | Phone (Mobile) | Normalize format (strip non-numeric characters except leading +). |
| Current Title | Current Title | None. |
| Current Company | Current Company | Must match a Company record in target or will be stored as plain text. |
| LinkedIn URL | LinkedIn URL | None. |
| Tags | Tags | Verify tag names exist in target instance. |
| Owner | Owner | Remap to target instance user IDs via user mapping table. |
| Dynamic Field (Source) | Dynamic Field (Target) | Create field in target first. Match field type (text, dropdown, date). Map dropdown option values. |
| Pipeline Stage | Pipeline Stage | Map to target's configured stages under Settings > Customize Workflow. |
Handling Dynamic Fields
Both Loxo instances support dynamic fields on People and Companies, but the field names, types, and dropdown options must match. Before migration:
- Extract the full dynamic field configuration from both instances (via API or admin UI).
- Create any missing dynamic fields in the target instance (Settings > Customize).
- For dropdown/picklist fields, ensure all option values exist in the target.
- For free-text fields that exceed the 2,800 character limit per cell (relevant for CSV imports), truncate or split content before import.
- If a dynamic field exists in the source but not the target, you must either create it in the target or map that data into a general "Notes" field — with a documented decision log.
Handling Relationships
This is the hardest part of any same-platform migration. The dependency chain:
Users (recruiters) → Companies → People → Jobs → Applications → Activities → Placements → Attachments
You must import in this order. Each record gets a new ID in the target instance. Maintain a mapping table:
source_id | target_id | object_type
----------|-----------|------------
12345 | 67890 | person
111 | 222 | company
333 | 444 | job
Every subsequent import step references this table to rebuild relationships.
Migration Architecture
The most resilient architecture for an API-based Loxo-to-Loxo migration is an Extract, Transform, Load (ETL) pipeline with a staging database (PostgreSQL or SQLite).
- Extract: Pull all records from the source Loxo API. Store the raw JSON payloads in the staging database. Use CSV for People and Companies where useful; API for Jobs, Deals, Placements, Person Events, and binary files (resumes) through the API.
- Transform: Clean the data, apply deduplication logic, normalize dynamic field names and types, remap owner IDs, and format payloads to match the target instance's configuration.
- Load: Push the data to the target Loxo API in dependency order. Capture the new ID returned in the
201 Createdresponse. - Cross-Reference: Store the mapping of
Source_IDtoTarget_IDin your staging database. This is mandatory for rebuilding relationships.
API Authentication
All Loxo API endpoints are formed using the Agency Domain and Agency Slug, and use bearer token authentication in the header.
curl -X GET "https://{agency-domain}.loxo.co/api/{agency-slug}/people" \
-H "Authorization: Bearer {SOURCE_API_KEY}" \
-H "Content-Type: application/json"Rate Limits and Pagination
Loxo does not publicly document specific API rate limits. Practical guidance:
- Start with conservative request rates (1–2 requests/second) and monitor for HTTP 429 responses. Increase gradually until you observe throttling.
- Implement exponential backoff with jitter for 429 responses. Start with a 1-second base delay, doubling up to a 60-second maximum.
- Use pagination parameters to iterate through large datasets. The
person_eventsendpoint supportsscroll_idpagination and Lucene queries. For/peopleand/jobsendpoints, usepageandper_pagequery parameters (consult Loxo's API documentation for current defaults and maximums). - Third-party integrations like Clay pull a maximum of 10,000 people per run. Note: this reflects Clay's integration design, not necessarily a Loxo API hard limit.
Loxo's API rate limits are not publicly documented. Always implement retry logic with exponential backoff. We found no documented bulk import/export API or published numeric rate limit, so production migrations should batch requests and keep a dead-letter queue for failures.
Estimated Migration Timelines by Volume
These estimates assume single-threaded API access at 1 request/second with 100ms processing overhead per record:
| Record Volume | Object Types | Estimated API Time | Total with Validation |
|---|---|---|---|
| 5,000 People + 10,000 Activities | People, Activities | ~4 hours | 1–2 days |
| 50,000 People + 200,000 Activities + 5,000 Jobs | Full migration | ~71 hours (~3 days continuous) | 1–2 weeks |
| 200,000 People + 1M Activities + 20,000 Jobs | Full migration | ~333 hours (~14 days continuous) | 3–5 weeks |
These estimates do not include attachment download/upload time, deduplication processing, or manual review cycles. Parallelizing across multiple threads may reduce elapsed time but will not circumvent per-account rate limits.
Handling Large Datasets
For instances with 50,000+ records:
- Batch API calls: Process records in batches of 100–500.
- Paginate reads: Use offset/cursor pagination on GET endpoints.
- Parallelize carefully: Multiple API keys may not circumvent per-account rate limits.
- Checkpoint progress: Log the last successfully imported record so you can resume after failures without re-importing.
- Plan for delta migration: Moving 500,000+ candidates via REST API will take weeks. You must plan a final delta sync (only records changed during the migration window) just before go-live.
Step-by-Step Migration Process
Step 1: Extract Data from Source Instance
import requests
import time
SOURCE_BASE_URL = "https://{domain}.loxo.co/api/{slug}"
SOURCE_API_KEY = "your-source-api-key"
HEADERS = {"Authorization": f"Bearer {SOURCE_API_KEY}",
"Content-Type": "application/json"}
def extract_people(page=1, per_page=100):
"""Extract all people with pagination."""
all_people = []
while True:
resp = requests.get(
f"{SOURCE_BASE_URL}/people",
headers=HEADERS,
params={"page": page, "per_page": per_page}
)
if resp.status_code == 429:
retry_after = int(resp.headers.get("Retry-After", 60))
time.sleep(retry_after)
continue
resp.raise_for_status()
data = resp.json()
if not data.get("results"):
break
all_people.extend(data["results"])
page += 1
time.sleep(0.5) # Conservative rate limiting
return all_peopleExtract Companies and People first. Then pull Jobs, Placements, Person Events, and binary files (resumes) through the API. Alternatively, request a full data export from your source Loxo instance — this can take multiple weeks, so the quicker the export is requested the quicker you can get started.
Step 2: Transform Data
import re
def transform_person(source_record, user_map, company_map):
"""Remap IDs and normalize fields for target instance."""
return {
"first_name": source_record.get("first_name", "").strip(),
"last_name": source_record.get("last_name", "").strip(),
"emails": source_record.get("emails", []),
"phones": normalize_phones(source_record.get("phones", [])),
"title": source_record.get("title", ""),
"company_id": company_map.get(source_record.get("company_id")),
"owner_id": user_map.get(source_record.get("owner_id")),
"tags": source_record.get("tags", []),
"dynamic_fields": remap_dynamic_fields(
source_record.get("dynamic_fields", {})
),
"_source_id": source_record["id"] # Track for mapping
}
def normalize_phones(phones):
"""Strip non-numeric chars (preserve leading +), standardize format."""
return [{"type": p["type"],
"number": re.sub(r'[^\d+]', '', p["number"])}
for p in phones if p.get("number")]Step 3: Load into Target Instance
Follow the dependency order: Users → Companies → People → Jobs → Applications → Activities → Placements → Attachments.
TARGET_BASE_URL = "https://{domain}.loxo.co/api/{slug}"
TARGET_API_KEY = "your-target-api-key"
TARGET_HEADERS = {"Authorization": f"Bearer {TARGET_API_KEY}",
"Content-Type": "application/json"}
id_mapping = {} # {"person": {source_id: target_id}, ...}
def load_person(transformed_record, max_retries=3):
"""Create person in target and store ID mapping."""
source_id = transformed_record.pop("_source_id")
for attempt in range(max_retries):
resp = requests.post(
f"{TARGET_BASE_URL}/people",
headers=TARGET_HEADERS,
json=transformed_record
)
if resp.status_code == 429:
wait = min(2 ** attempt * 1, 60)
time.sleep(wait)
continue
if resp.status_code >= 500:
time.sleep(2 ** attempt)
continue
if resp.status_code >= 400:
log_error(f"4xx for source_id={source_id}: {resp.text}")
return None # Do not retry client errors
resp.raise_for_status()
target_id = resp.json()["id"]
id_mapping.setdefault("person", {})[source_id] = target_id
return target_id
log_dead_letter(source_id, "person", transformed_record)
return NoneLog every record with source ID, target ID, object type, payload hash, response code, retry count, and last error. Retry 429 and 5xx with exponential backoff. Never retry 4xx blindly — those indicate payload problems. Keep failed records in a dead-letter queue for manual repair.
Step 4: Rebuild Relationships
After all core records are created, link candidates to jobs using the ID-mapping table:
def rebuild_job_associations(source_applications, id_mapping):
"""Re-create candidate-job associations in target."""
for app in source_applications:
target_person = id_mapping["person"].get(app["person_id"])
target_job = id_mapping["job"].get(app["job_id"])
if not target_person or not target_job:
log_error(f"Missing mapping: person={app['person_id']}, "
f"job={app['job_id']}")
continue
resp = requests.post(
f"{TARGET_BASE_URL}/jobs/{target_job}/applications",
headers=TARGET_HEADERS,
json={"person_id": target_person,
"stage": app.get("stage", "sourced")}
)
handle_response(resp, "application", app)Then replay Person Events, handling intake notes and transcripts separately (they are distinct from person_events in Loxo's data model — intake notes live on the person description field, and AI Notetaker transcripts are stored in their own location). Finally, download binary files (resumes) from the source via GET requests and upload them to the target via multipart/form-data POST requests, attaching them to the correct target People IDs.
Step 5: Validate Data
Run count comparisons and spot-check records after each phase. See the Validation section below for the full protocol.
Edge Cases and Challenges
Duplicate Records
When merging two populated instances, duplicates are the primary data quality risk. Loxo auto-merges people only when the name is at least a 70% match and there is either an identical email or social URL, with documented exceptions. That auto-merge is helpful for cleanup but unsafe as your primary migration deduplication strategy.
The auto-merge misses:
- Candidates with different email addresses across instances
- Records with no email (phone-only contacts)
- Companies with slightly different names ("Acme Corp" vs "Acme Corporation")
Mitigation: Build a multi-field matching algorithm — match on email OR (first name + last name + phone) OR LinkedIn URL. Flag conflicts for manual review rather than auto-merging. If a match is found, use a PUT/PATCH request to update the existing record rather than POSTing a new one.
Avoid merging any records during migration — merging is very hard to undo. Let the migration team merge records as part of the data migration process.
Activity History and Note Types
Activities must be imported chronologically to maintain accurate timelines. If you import a "Phone Screen" activity dated March 2024 after a "Placement" dated June 2024, the activity feed renders correctly — but some pipeline analytics may misinterpret the sequence.
Critical distinction — three separate data locations for "notes" in Loxo:
- Person Events (
person_events): Calls, emails, meetings, notes, submissions — the standard activity stream. - Intake Notes: Stored on the person description field, not as
person_events. If you import intake notes as activities, they appear in the wrong place. - AI Notetaker Transcripts: Stored separately from both
person_eventsand the description field.
If you collapse everything into one stream, you will lose meaning and context.
Pipeline Stage Mismatches
The Loxo Hiring Pipeline is the most important customization to your account. The events and activities there are reportable and filterable — they track progress in jobs, notes, and actions within the system.
If the source instance has 8 pipeline stages and the target has 5, you must decide how to consolidate. Create a stage mapping table before migration — this cannot be fixed retroactively without re-importing all applications. Example:
| Source Stage | Target Stage | Notes |
|---|---|---|
| Sourced | Sourced | Direct match |
| Phone Screen | Phone Screen | Direct match |
| Technical Interview | Interview | Merged stage |
| Culture Fit Interview | Interview | Merged stage |
| Offer Extended | Offer | Renamed |
| Offer Accepted | Offer | Merged — add note to distinguish |
Loxo Source Data Restrictions
Loxo Source data is proprietary and will be limited to protect their own data. The 500-profile limit applies universally and cannot be bypassed through API use.
Candidate profiles that originated from Loxo Source (Loxo's proprietary talent database) carry export restrictions. This is an application-level restriction enforced by Loxo — it applies to both CSV exports and API extraction. Data your team manually added to these profiles — dynamic field entries, notes, tags — is your data and will export. But the underlying Source profile data (the original contact information sourced from Loxo's database) may not transfer to a new instance.
Practical impact: If your source instance has 10,000 People and 3,000 originated from Loxo Source, you can only export 500 of those Source-originated profiles per 30-day rolling window. Plan accordingly — this alone could extend your migration timeline by months if not accounted for.
Attachment Bandwidth
Resumes and documents must be downloaded from the source and re-uploaded to the target. For instances with tens of thousands of attachments, this becomes a bandwidth problem. Plan for:
- Parallel download/upload streams (2–4 concurrent, respecting rate limits)
- Temporary cloud storage (S3, GCS) for files in transit
- Filename collision handling (two candidates with "resume.pdf" — prefix with source person ID)
- The API surface includes person documents, resumes, job documents, and person-event documents as distinct endpoints
User ID Remapping
Recruiter ownership assignments reference user IDs that differ between instances. Before migration, build a user mapping table. If a source user doesn't exist in the target (e.g., a recruiter who left the company), decide whether to assign their records to a default user or create a placeholder account. Document this decision — it affects reporting accuracy.
GDPR Deletion Requests During Migration
If a data subject submits a deletion request (GDPR Article 17) for a record that exists in both the source instance and has already been migrated to the target, you must delete from both instances. Your migration process should include:
- A hold queue for records with pending deletion requests
- A process to check both instances when processing deletion requests during the migration window
- Documentation of which records have been migrated, so deletion can be applied to both source and target
Multi-Level Relationships
A Person can be both a Candidate for one Job and a Hiring Manager (contact) for another. Contact-type People and candidate-type People behave differently on the company activity feed. Your ID-mapping table must be perfectly maintained to handle these overlapping associations.
Workflow and Progression Activities
Loxo's progression activities only move candidates forward, not backward, and custom progression activities cannot be triggered automatically by Loxo's system. Workflow changes take effect immediately across all jobs and users. Plan the target's workflow configuration carefully before loading any data.
Limitations and Constraints
| Constraint | Impact |
|---|---|
| CSV export excludes activities, attachments, work history, education | Cannot do a full migration via CSV alone |
| Jobs cannot be imported via CSV | Jobs must be created via API or manually |
| API is a paid feature | Both instances need active paid subscriptions with API access enabled |
| Rate limits undocumented | Must discover limits empirically; plan for throttling and implement backoff |
| Loxo Source export cap (500/30 days) | Sourced profiles may require months to fully extract |
| CSV import limit: 10,000 records/file, 2,800 chars/cell | Large imports must be split into batches |
| No bulk API endpoints documented | All operations appear to be single-record CRUD |
| Free migration excludes jobs, activities, notes | Full migration carries additional cost from Loxo |
| No custom objects | All data must fit within People, Companies, Jobs, Deals — no arbitrary object types |
| GDPR settings cannot be turned off without Support | Lock GDPR and validation settings early in target configuration |
Loxo's lack of custom objects means any data stored in dynamic fields on People or Companies must be mapped field-by-field. There is no concept of creating arbitrary object types — everything maps to the core object set.
Validation and Testing
Do not trust a 200 OK response blindly. Implement a rigorous validation protocol.
Record Count Validation
After each migration phase, compare counts:
def validate_counts(source_counts, target_counts):
"""Compare record counts between instances."""
discrepancies = []
for obj_type, source_count in source_counts.items():
target_count = target_counts.get(obj_type, 0)
if source_count != target_count:
discrepancies.append({
"object": obj_type,
"source": source_count,
"target": target_count,
"delta": source_count - target_count
})
print(f"MISMATCH: {obj_type} — "
f"Source: {source_count}, Target: {target_count}, "
f"Delta: {source_count - target_count}")
else:
print(f"OK: {obj_type} — {source_count} records")
return discrepanciesField-Level Spot Checks
Randomly sample 50–100 records per object type. For each:
- Compare all standard fields between source and target.
- Verify dynamic field values transferred correctly.
- Confirm owner assignment matches the user mapping.
- Check that activity count on the target record matches the source.
Relationship Integrity Check
For every Job in the target instance, verify:
- The associated Company exists and is correctly linked.
- All candidate Applications are present.
- Pipeline stage assignments are correct.
- Activity histories are associated with the right candidate-job combination.
UAT Process
- Have 2–3 recruiters log into the target instance.
- Ask each to verify 10–15 of their own records — candidates they know, jobs they're working.
- Confirm pipeline views, search results, and activity timelines look correct.
- Have recruiters attempt their daily workflows (not just list views) — searching, adding notes, moving candidates through stages. That's where broken relationships surface.
- Document any discrepancies before cutover.
Rollback Plan
Before cutover:
- Maintain the source instance in read-only mode (do not delete or modify).
- Keep the full export archive stored securely.
- If the target instance was pre-populated, take a snapshot/export before migration.
- Define a rollback trigger: if >5% of records fail validation, halt and investigate before proceeding.
Rollback execution script:
def rollback_migration(id_mapping, object_types_in_reverse_order):
"""Delete all migrated records from target in reverse dependency order.
Reverse order: Attachments → Placements → Activities → Applications
→ Jobs → People → Companies
"""
for obj_type in object_types_in_reverse_order:
mappings = id_mapping.get(obj_type, {})
for source_id, target_id in mappings.items():
resp = requests.delete(
f"{TARGET_BASE_URL}/{obj_type}/{target_id}",
headers=TARGET_HEADERS
)
if resp.status_code == 200:
print(f"Deleted {obj_type} {target_id}")
elif resp.status_code == 404:
print(f"Already gone: {obj_type} {target_id}")
else:
log_error(f"Failed to delete {obj_type} {target_id}: "
f"{resp.status_code} {resp.text}")
time.sleep(0.5) # Respect rate limitsDelete in reverse dependency order to avoid orphaned references: Attachments → Placements → Activities → Applications → Jobs → People → Companies.
For a complete testing framework, see the Ultimate ATS Data Migration Checklist.
Post-Migration Tasks
Once the data is verified, execute the following operational steps.
Rebuild Automations and Sequences
Loxo's outreach sequences, email templates, and workflow automations do not transfer between instances. After migration:
- Re-create email sequences in the target instance.
- Rebuild any outreach templates.
- Reconfigure pipeline automation rules (auto-stage advancement, notifications). Remember: progression activities only move candidates forward.
- Re-enable job-board publishing and integration toggles. Loxo's job feeds depend on Active + Published jobs.
- Set up reporting dashboards and saved views.
Keep automations and webhooks turned off during the migration itself to prevent accidental mass emails to candidates or overwhelming downstream systems.
User Training and Onboarding
The Loxo Help Center is a great place to find articles. Register for live trainings — the Agency Organization training or Customizations Workshop help teams understand platform configurations.
Even when migrating between identical platforms, recruiters need orientation:
- Dynamic fields may be named differently or organized differently in the target.
- Pipeline stages may have changed (review your stage mapping table with the team).
- Saved Lists and views don't transfer — show users how to recreate them.
- Train admins on API key storage and export rules. Loxo API keys are admin-only and shown in full once.
Monitor for Data Inconsistencies
For at least 2 weeks after cutover:
- Check daily for records that appear in the source but not the target.
- Monitor for duplicate creation (recruiters manually re-adding candidates they can't find).
- Run a delta migration if the source instance was still active during migration and new records were created.
- Watch the duplicates log and failed-import queue.
- Re-enable webhooks after confirming the migration is stable.
Best Practices
- Back up before everything. Export the target instance's existing data before importing anything. Loxo has no "undo" for bulk imports.
- Never migrate directly from prod to prod without a test run. Migrate 500–1,000 records and validate thoroughly before running the full migration.
- Freeze the source instance. Once export begins, instruct recruiters to stop creating new records in the source. Records created during migration require a delta sync.
- Validate incrementally. Don't wait until the end to validate. Check counts and spot-check records after each object type is imported.
- Log everything. If a record fails to migrate, your script must log the exact source ID, target endpoint, request payload hash, and the Loxo API error message so you can manually review it.
- Disable webhooks during bulk load. Turn off webhooks in the target to prevent overwhelming downstream systems.
- Keep an old→new ID mapping table for every object. This is the backbone of the entire migration. Persist it in a database, not just in memory.
- If the project is bigger than a People/Company CSV cleanup, treat it like an engineering program, not an admin task. Assign a technical lead, define acceptance criteria, and plan for a 2-week stabilization period post-cutover.
For compliance considerations when moving candidate data, see Ensuring GDPR & CCPA Compliance When Migrating Candidate Data. For common ATS migration pitfalls, see 5 "Gotchas" in ATS Migration.
Making the Call
A Loxo-to-Loxo migration is simpler than a cross-platform migration in one respect — the schema is identical — and harder in another — you inherit all of Loxo's export limitations and API constraints on both sides.
If your requirement is just moving People and Companies into a cleaner Loxo workspace, native CSV tooling or Loxo's managed migration service (free tier) is sufficient. If you need Jobs, Placements, Person Events, attachments, workflow parity, or an M&A consolidation with deduplication logic, the reliable path is an API-led migration with formal validation — or a migration partner who has done this before and can demonstrate that they maintain ID-mapping tables, import in dependency order, and validate relationship integrity.
The right approach depends on three factors: your data volume (which determines timeline), the complexity of your relationships (which determines method), and whether your engineering team has weeks to dedicate to disposable migration code.
Frequently Asked Questions
- Can I do a full Loxo-to-Loxo migration with CSV only?
- No. Loxo's CSV export only includes fields visible on the People or Companies List view. Resumes, activity history, work experience, education, and attachments are excluded. Jobs cannot be imported via CSV at all. Loxo's free migration scope also excludes jobs, activities/events, and notes. For full-fidelity migration, you need the API or Loxo's paid managed migration service.
- Does Loxo have a bulk migration API?
- Loxo's Open API supports reading and writing individual records (People, Companies, Jobs, Applications, Attachments), but there is no documented bulk API for mass import/export. You need to paginate reads and loop through single-record POST calls, respecting undocumented rate limits. The person_events endpoint supports scroll_id pagination.
- How do I preserve activity history in a Loxo-to-Loxo migration?
- CSV exports strip activity history entirely. Use the Loxo API to extract activities (person_events) from the source instance, then re-create them in the target via API in chronological order, linking them to the correct People and Job records using an ID-mapping table. Note that intake notes and AI Notetaker transcripts are separate from person_events and require different handling.
- What data typically breaks in a Loxo-to-Loxo migration?
- Job associations, placements, person events, intake notes, file attachments, and dynamic fields are the usual failure points because they depend on correct load order, ID remapping, and separate API endpoints. Pipeline stage mismatches between instances also cause silent data corruption if not mapped before migration.
- How long does a Loxo-to-Loxo migration take?
- It depends on the method. CSV transfer of basic contact data takes hours. Loxo's managed migration service can take 2-4 weeks including the export request queue. An API-based migration with a dedicated engineer typically completes in 3-7 days for datasets under 100,000 records, though large datasets with hundreds of thousands of records can take longer due to rate limits.
