Apollo migration deep dive
'Comprehensive Apollo.io migration strategies.From its SKILL.md
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill apollo-migration-deep-diveAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.3 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
Apollo Migration Deep Dive
Current State
!npm list 2>/dev/null | head -10
Overview
Migrate contact and company data into Apollo.io from other CRMs (Salesforce, HubSpot) or CSV sources. Uses Apollo's Contacts API for creating/updating contacts and Bulk Create Contacts endpoint for high-throughput imports (up to 100 contacts per call). Covers field mapping, assessment, batch processing, reconciliation, and rollback.
Prerequisites
- Apollo master API key (Contacts API requires master key)
- Node.js 18+
- Source CRM export in CSV or JSON format
Instructions
Step 1: Define Field Mappings
// src/migration/field-map.ts
interface FieldMapping {
source: string;
target: string; // Apollo Contacts API field
transform?: (v: any) => any;
required: boolean;
}
// Salesforce -> Apollo
const salesforceMap: FieldMapping[] = [
{ source: 'FirstName', target: 'first_name', required: true },
{ source: 'LastName', target: 'last_name', required: true },
{ source: 'Email', target: 'email', required: true },
{ source: 'Title', target: 'title', required: false },
{ source: 'Phone', target: 'phone_number', required: false },
{ source: 'Company', target: 'organization_name', required: false },
{ source: 'Website', target: 'website_url', required: false,
transform: (url: string) => url?.startsWith('http') ? url : `https://${url}` },
{ source: 'LinkedIn', target: 'linkedin_url', required: false },
];
// HubSpot -> Apollo
const hubspotMap: FieldMapping[] = [
{ source: 'firstname', target: 'first_name', required: true },
{ source: 'lastname', target: 'last_name', required: true },
{ source: 'email', target: 'email', required: true },
{ source: 'jobtitle', target: 'title', required: false },
{ source: 'phone', target: 'phone_number', required: false },
{ source: 'company', target: 'organization_name', required: false },
{ source: 'website', target: 'website_url', required: false },
];
function mapRecord(record: Record<string, any>, mappings: FieldMapping[]): Record<string, any> {
const mapped: Record<string, any> = {};
for (const m of mappings) {
let value = record[m.source];
if (m.required && !value) throw new Error(`Missing: ${m.source}`);
if (value && m.transform) value = m.transform(value);
if (value) mapped[m.target] = value;
}
return mapped;
}
Step 2: Pre-Migration Assessment
// src/migration/assessment.ts
import fs from 'fs';
import { parse } from 'csv-parse/sync';
async function assess(csvPath: string, mappings: FieldMapping[]) {
const records = parse(fs.readFileSync(csvPath, 'utf-8'), { columns: true, skip_empty_lines: true });
const stats = { total: records.length, valid: 0, invalid: 0,
missing: {} as Record<string, number>, duplicateEmails: 0, errors: [] as string[] };
const emails = new Set<string>();
for (const record of records) {
try {
mapRecord(record, mappings);
const email = record.Email ?? record.email;
if (emails.has(email)) stats.duplicateEmails++;
else emails.add(email);
stats.valid++;
} catch (err: any) {
stats.invalid++;
const field = err.message.replace('Missing: ', '');
stats.missing[field] = (stats.missing[field] ?? 0) + 1;
if (stats.errors.length < 5) stats.errors.push(err.message);
}
}
console.log(`Total: ${stats.total}, Valid: ${stats.valid}, Invalid: ${stats.invalid}, Dupes: ${stats.duplicateEmails}`);
if (Object.keys(stats.missing).length) console.log('Missing fields:', stats.missing);
return stats;
}
Step 3: Batch Migration Using Bulk Create
Apollo's Bulk Create Contacts endpoint creates up to 100 contacts per call with intelligent deduplication.
// src/migration/batch-worker.ts
import axios from 'axios';
const client = axios.create({
baseURL: 'https://api.apollo.io/api/v1',
headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.APOLLO_API_KEY! },
});
interface MigrationResult {
total: number; created: number; existing: number; failed: number;
createdIds: string[];
errors: Array<{ record: any; error: string }>;
}
async function migrateBatch(records: Record<string, any>[], batchSize: number = 100): Promise<MigrationResult> {
const result: MigrationResult = { total: records.length, created: 0, existing: 0, failed: 0,
createdIds: [], errors: [] };
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
try {
// Bulk create endpoint handles deduplication
const { data } = await client.post('/contacts/bulk_create', {
contacts: batch,
});
const newContacts = data.contacts ?? [];
const existingContacts = data.existing_contacts ?? [];
result.created += newContacts.length;
result.existing += existingContacts.length;
result.createdIds.push(...newContacts.map((c: any) => c.id));
} catch (err: any) {
// Fall back to individual creates
for (const record of batch) {
try {
const { data } = await client.post('/contacts', record);
result.created++;
result.createdIds.push(data.contact.id);
} catch (e: any) {
result.failed++;
result.errors.push({ record, error: e.response?.data?.message ?? e.message });
}
}
}
// Rate limit: 100 requests/min for contacts
if (i + batchSize < records.length) {
await new Promise((r) => setTimeout(r, 1000));
}
console.log(`Progress: ${Math.min(i + batchSize, records.length)}/${records.length}`);
}
return result;
}
Step 4: Post-Migration Reconciliation
async function reconcile(sourceRecords: Record<string, any>[]) {
let matched = 0, missing = 0, mismatched = 0;
for (const source of sourceRecords.slice(0, 100)) { // Sample reconciliation
const { data } = await client.post('/contacts/search', {
q_keywords: source.email, per_page: 1,
});
const contact = data.contacts?.[0];
if (!contact) { missing++; continue; }
const nameMatch = contact.first_name === source.first_name && contact.last_name === source.last_name;
if (nameMatch) matched++;
else { mismatched++; console.warn(`Mismatch: ${source.email}`); }
}
console.log(`Reconciliation: ${matched} matched, ${missing} missing, ${mismatched} mismatched`);
return { matched, missing, mismatched };
}
Step 5: Rollback
async function rollback(contactIds: string[]) {
console.log(`Rolling back ${contactIds.length} contacts...`);
let deleted = 0;
for (let i = 0; i < contactIds.length; i += 50) {
const batch = contactIds.slice(i, i + 50);
for (const id of batch) {
try { await client.delete(`/contacts/${id}`); deleted++; }
catch (err: any) { console.error(`Failed: ${id}: ${err.message}`); }
}
await new Promise((r) => setTimeout(r, 500));
console.log(`Rollback: ${Math.min(i + 50, contactIds.length)}/${contactIds.length}`);
}
console.log(`Rolled back ${deleted}/${contactIds.length} contacts`);
}
Output
- Field mappings for Salesforce and HubSpot to Apollo Contacts API
- Pre-migration assessment with validation, duplicates, and missing fields
- Batch migration via
POST /contacts/bulk_create(100 per call) - Post-migration reconciliation sampling
- Rollback procedure deleting created contacts
Error Handling
| Issue | Resolution |
|---|---|
| 403 on create | Contacts API requires master key |
| Bulk create fails | Falls back to individual POST /contacts calls |
| Duplicate contacts | Apollo's bulk_create handles dedup — returns existing_contacts |
| Field mapping error | Review source field names, check for case sensitivity |
| Rate limited | Increase delay between batches |
Examples
Full Migration Pipeline
const assessment = await assess('./salesforce-export.csv', salesforceMap);
if (assessment.invalid > assessment.total * 0.1) {
console.error('Too many invalid records (>10%). Clean data first.');
process.exit(1);
}
const records = parseCsv('./salesforce-export.csv').map((r) => mapRecord(r, salesforceMap));
const result = await migrateBatch(records, 100);
console.log(`Created: ${result.created}, Existing: ${result.existing}, Failed: ${result.failed}`);
// Save contact IDs for potential rollback
fs.writeFileSync('migration-ids.json', JSON.stringify(result.createdIds));
await reconcile(records);
Resources
Next Steps
After migration, verify data with apollo-prod-checklist.
What ships with it: 1 file
14.2 KB alongside SKILL.md
references/
- implementation-guide.md14.2 KB