A
ApiOne
/Docs

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:

  1. Export account/contact records from your CRM as a CSV
  2. Normalise domains and deduplicate
  3. Submit records as an async batch job to ApiOne
  4. Receive enriched results via webhook
  5. 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