How to enrich CRM records at scale with ApiOne
This guide covers the complete workflow for enriching Salesforce or HubSpot account and contact records using ApiOne. It covers exporting records, normalising domains, submitting async batch jobs, receiving results via webhook, writing enriched data back to your CRM, and estimating credit cost before you begin.
Overview
The recommended workflow for CRM enrichment at scale is:
- Export account/contact records from your CRM as a CSV
- Normalise domains and deduplicate
- Submit records as an async batch job to ApiOne
- Receive enriched results via webhook
- Write enriched data back to your CRM via the Salesforce or HubSpot API
Step 1 — Export and normalise your CRM records
const fs = require('fs');
const { parse } = require('csv-parse/sync');
// Load your exported CRM CSV
const raw = fs.readFileSync('accounts.csv', 'utf8');
const records = parse(raw, { columns: true });
function normaliseDomain(input) {
if (!input) return null;
let d = input.trim().toLowerCase()
.replace(/^https?:\/\//, '')
.replace(/^www\./, '')
.split('/')[0].split('?')[0];
return d.includes('.') ? d : null;
}
// Deduplicate and normalise
const domains = [...new Set(
records
.map(r => normaliseDomain(r.Website || r.Domain || r.Company_Domain))
.filter(Boolean)
)];
console.log(`${domains.length} unique domains to enrich`);Step 2 — Estimate credit cost
Company enrichment costs 5 credits per successful call and 1 credit for a not-found response. For a typical B2B CRM, expect a 10–20% not-found rate for smaller or newer companies.
const totalDomains = domains.length;
const estimatedNotFound = Math.round(totalDomains * 0.15); // 15% not-found estimate
const estimatedCredits =
(totalDomains - estimatedNotFound) * 5 + estimatedNotFound * 1;
console.log(`Estimated credit cost: ${estimatedCredits} credits`);
// 1,000 domains → ~4,350 credits (fits in Growth plan)Step 3 — Submit async batch jobs
ApiOne async jobs accept up to 1,000 records per job. For larger datasets, split into batches:
async function submitBatch(domains, webhookUrl) {
const response = await fetch('https://apione.store/api/v1/async/enrich', {
method: 'POST',
headers: {
'X-API-Key': process.env.APIONE_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'company',
records: domains.map(domain => ({ domain })),
webhook_url: webhookUrl,
}),
});
return await response.json();
}
// Split into batches of 1,000
const BATCH_SIZE = 1000;
const batches = [];
for (let i = 0; i < domains.length; i += BATCH_SIZE) {
batches.push(domains.slice(i, i + BATCH_SIZE));
}
const jobIds = [];
for (const batch of batches) {
const result = await submitBatch(batch, 'https://your-server.com/webhooks/apione');
jobIds.push(result.job_id);
console.log(`Submitted batch: ${result.job_id}`);
// Small delay between batch submissions
await new Promise(r => setTimeout(r, 500));
}
console.log('All batches submitted:', jobIds);Step 4 — Receive results via webhook
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/apione', async (req, res) => {
res.sendStatus(200); // Acknowledge immediately
const { event, results } = req.body;
if (event !== 'job.completed') return;
for (const record of results) {
if (record.status !== 'success') continue;
const { domain, company_name, industry, employee_count, tech_stack } = record.data;
// Write back to your CRM
await updateSalesforceAccount(domain, {
Industry: industry,
NumberOfEmployees: parseHeadcount(employee_count),
Tech_Stack__c: tech_stack?.join(', '),
});
}
});Step 5 — Write enriched data back to Salesforce
const jsforce = require('jsforce');
const conn = new jsforce.Connection({ loginUrl: 'https://login.salesforce.com' });
await conn.login(process.env.SF_USERNAME, process.env.SF_PASSWORD);
async function updateSalesforceAccount(domain, enrichedData) {
// Find account by domain
const result = await conn.query(
`SELECT Id FROM Account WHERE Website LIKE '%${domain}%' LIMIT 1`
);
if (!result.records.length) return;
const accountId = result.records[0].Id;
await conn.sobject('Account').update({ Id: accountId, ...enrichedData });
console.log(`Updated Salesforce account for ${domain}`);
}HubSpot users: Use the HubSpot Companies API to update records by domain. The pattern is identical — find the company by domain, then PATCH the enriched fields using the HubSpot REST API or their Node.js SDK.
Handling errors and missing data
for (const record of results) {
if (record.status === 'success') {
await updateCRM(record.input.domain, record.data);
} else if (record.status === 'not_found') {
// Log for manual review
await logMissingDomain(record.input.domain);
} else {
// Unexpected error — retry later
await queueForRetry(record.input.domain);
}
}Related
- Async Jobs reference
- Webhooks reference
- Company Enrichment API reference
- Upgrade your plan — Growth plan includes 25,000 credits/month