> Enrich.chat gives AI agents verified work emails and mobiles. To use it from an agent, read https://enrich.chat/agents.md . This is the Markdown version of https://enrich.chat/use-cases/crm-enrichment/

# Enrich your CRM contacts and companies from a CSV or the batch API

Export the records with gaps from HubSpot or Salesforce, add work emails and company details with a Bulk enrich job or POST /v1/batch, and import the result. There is no CRM app and no automatic sync, so every write is yours to approve.

By Enrich.chat Editorial. Updated October 19, 2026.

**Short answer:** To enrich CRM data with Enrich.chat, export the records you want to fill as a CSV, run the file through Bulk enrich as a background job (up to 5,000 rows each) or through the batch API, then import the result with your CRM's own importer. A work email found uses 10 credits, a company profile 5, and a row that finds nothing is free.

## Key takeaways

- Bulk enrich takes a CSV of up to 5,000 rows and gives back your columns plus new ones. Run it on the server and it keeps going after you close the tab, and a signed webhook can tell your app when it finishes. A row that finds nothing is free.
- A work email found uses 10 credits and a company profile 5. Filling 2,000 emails and 500 company profiles fits the Growth plan.
- Import the result with an update-only import matched on the record ID, and use HubSpot's Don't overwrite option on fields that already hold values.
- There is no native CRM app and no automatic sync, and nothing refreshes by itself. You start each run, and a webhook announces finished jobs only.

## Everything a CRM clean-up needs

- **A CSV in, a CSV out.** Bulk enrich takes up to 5,000 rows per run and returns your columns first, then the new ones, ready for your CRM's importer.
- **Your columns, not ours.** Match the name, company website, LinkedIn URL or work email columns you already have. No renaming first.
- **Four modes.** Find work emails, enrich people, enrich companies, or find a company's email format.
- **A status on every email.** Each work email is marked verified, catch_all or unknown, so your import can leave weak ones out.
- **Credits only on matches.** 10 credits per work email found, 5 per company profile. A row that finds nothing is free.
- **Jobs, webhooks and a batch API.** `POST /v1/jobs` runs up to 5,000 rows on the server, and a signed webhook says when it is done. `POST /v1/batch` takes 25 items per call.

## From a CRM export to updated records

1. **Export from your CRM.** Export the records with gaps. Keep the record ID, the name, the company website and the LinkedIn URL if you have it.
2. **Enrich the file.** Upload it to Bulk enrich, match your columns, pick a mode and run it on the server. It keeps going if you close the tab. Download when it is done.
3. **Import it back.** Keep the rows that were found, then import with an update-only import matched on the record ID.

## Can Enrich.chat enrich HubSpot and Salesforce records?

Yes, with a round trip you control. You export the records, enrich the file in the dashboard or through the batch API, and import the result with your CRM's own importer. Enrich.chat has no HubSpot or Salesforce app and no automatic sync, so nothing is written to your CRM unless you write it.

This fills a gap in the built-in tools. HubSpot's [data enrichment](https://knowledge.hubspot.com/records/enrich-your-contact-and-company-data) fills contact properties such as job title, LinkedIn URL and location, but its contact list has no email address or mobile number, and it skips personal addresses such as Gmail. Enrich.chat adds those fields, and each work email comes back marked `verified`, `catch_all` or `unknown`. Our guide to [CRM data enrichment](https://enrich.chat/blog/crm-data-enrichment/) covers which fields to enrich and how often. This page covers the Enrich.chat workflow.

Bulk enrich has four modes. Pick the one that matches the gap in your records.

| What you want to fill | Mode and what you send | Credits when found |
| --- | --- | --- |
| A missing work email | **Find work emails.** Name and company website. | 10 per email |
| Title, company, level or a mobile for a known person | **Enrich people.** LinkedIn URL, work email, or name and company website. | 10 per email, 250 per mobile (off unless you tick it) |
| Industry, size, revenue band, location, technologies | **Enrich companies.** Company website. | 5 per company |
| How a company writes its addresses | **Find email formats.** Company website. | 5 per company |

**Figure 1: The CRM enrichment round trip.** Four steps in a loop. Export a CSV of the records with gaps from your CRM. Enrich it with Bulk enrich or the batch API, which uses credits only on matches. Filter to the rows that were found. Import them back with an update-only import matched on the record ID.

*The round trip. Enrich.chat handles step 02 only. Steps 01, 03 and 04 happen in your CRM and your spreadsheet. Sources: [HubSpot's import options](https://knowledge.hubspot.com/import-and-export/import-objects) and [Bulk enrich](https://enrich.chat/features/bulk-enrichment/).*

## How do you enrich a CRM export with Bulk enrich?

Export the records with gaps, upload the CSV, match your columns, choose a mode and run it. One run takes up to 5,000 rows, so a larger export goes through in parts.

1. **Export from your CRM.** In HubSpot, open a Contacts or Companies view and [choose Export and pick CSV](https://knowledge.hubspot.com/import-and-export/export-records). Under Customize, "All properties on records" adds every column and puts Record ID first. In Salesforce, run a report and [export it with Details Only as a CSV](https://help.salesforce.com/s/articleView?id=sf.reports_export.htm&language=en_US&type=5), which needs the Export Reports permission. Keep the record ID column, because you need it to import.
2. **Upload it in the dashboard.** Open [Bulk enrich](https://enrich.chat/app/), drop the file or paste rows, and match your columns to name, company website, LinkedIn URL or work email.
3. **Choose a mode and run.** Leave "Run it on the server" ticked. The job keeps going after you close the tab or the laptop, and you can follow it under Jobs at `/app/#/jobs`. Untick it to run the file in your tab instead. The dashboard then sends rows 25 at a time through the batch endpoint, so a 5,000-row file is 200 calls and the tab must stay open until the run ends.
4. **Download the CSV.** You get your columns first, then new ones that start with `enrich_`, such as `enrich_work_email` and `enrich_email_status`. A job's results are kept 30 days.

A server job applies your team's Do not contact list and opt-outs. Those rows come back with blank contact columns and a flag in `enrich_do_not_contact`, so keep them out of your import. A job stops by itself, with the status "stopped", if your credits or free lookups run out. Upgrade your plan or wait for the renewal, then start a new job for the skipped rows.

A row that lacks the columns a mode needs is skipped, and a row that finds nothing is free. Test with a few rows first and read every result. The free tier gives 10 lookups a day, enough for a handful of rows. A bigger file needs a [plan](https://enrich.chat/pricing/).

**Figure 2: A CRM export before and after Bulk enrich.** The export has four columns: record_id, name, website and an empty work_email. After the Find work emails mode runs, the downloaded file keeps those four columns and adds enrich_found, enrich_work_email and enrich_email_status. Two rows are found, one verified and one catch_all. The third row finds nothing, so its new cells stay blank and it uses no credits.

*One export before and after Bulk enrich, with made-up people at example.com, example.org and example.net. New columns start with `enrich_`. A row that finds nothing stays blank and uses 0 credits. Source: [Enrich.chat Bulk enrich](https://enrich.chat/features/bulk-enrichment/).*

## How do you import the enriched file without overwriting your data?

Keep only the rows that were found, then run an update-only import matched on the record ID. That protects values your team already entered and avoids duplicate records.

- **Filter the file first.** Keep rows where `enrich_found` says yes. Hold back `catch_all` and `unknown` emails, or load them into a separate property. A catch-all domain accepts every address, so the mailbox cannot be confirmed. Never import an unconfirmed guess.
- **HubSpot.** Choose "Update existing records only" and map the Record ID column first. HubSpot's [import tool](https://knowledge.hubspot.com/import-and-export/import-objects) lets you tick "Don't overwrite" on each property, and it [ignores blank cells](https://knowledge.hubspot.com/import-and-export/set-up-your-import-file), so an empty value never clears a field. Match on Record ID, not email, because a contact with no email has nothing to match on.
- **Salesforce.** The [Data Import Wizard](https://help.salesforce.com/s/articleView?id=sf.data_import_wizard.htm&language=en_US&type=5) handles accounts, contacts and leads from a CSV, up to 50,000 records at a time, and Salesforce says to test a small file first. For bigger loads, Salesforce's [Trailhead module](https://trailhead.salesforce.com/content/learn/modules/lex_implementation_data_management/lex_implementation_data_import) points to Data Loader once you pass 50,000 records.

Check for duplicates before you import. HubSpot [matches imported contacts on email and companies on domain name](https://knowledge.hubspot.com/records/deduplication-of-records) unless you choose Record ID or a custom unique property, and companies created through its API are not deduplicated by domain.

## How do you backfill a large CRM with a background job and a webhook?

Start a job from the API, add a webhook, and collect the file when it arrives. A job runs on the server, so it carries on after your script goes away. See [background jobs](https://enrich.chat/features/background-jobs/) and the code panel above.

1. **Add a webhook once.** `POST /v1/webhooks` takes an https address on a public host name and the events `job.completed` and `job.stopped`. The reply shows a signing secret (`whsec_...`) one time, so store it.
2. **Start the job.** `POST /v1/jobs` takes an `op` such as `email`, `person` or `company`, up to 5,000 items and a label. Put your CRM record ID in each item's `id`. A job needs a key, an OAuth token or a dashboard login.
3. **Check the signature.** The `Enrich-Signature` header holds a timestamp and an HMAC-SHA256 of the timestamp, a dot and the raw body, made with your secret. Reject a timestamp older than five minutes.
4. **Fetch the results.** `GET /v1/jobs/<id>/results` returns JSON, 100 rows at a time, or add `?format=csv` for one file with the `enrich_*` columns, and your own columns too if you sent them as `headers` and `rows` with the job. Then run your update-only import.

Failed deliveries are retried five times, the last after 6 hours. A webhook that fails ten in a row is switched off, and `POST /v1/webhooks/<id>/enable` turns it back on. You can also poll `GET /v1/jobs/<id>`.

| Route | Rows | Best for |
| --- | --- | --- |
| Bulk enrich on the server | Up to 5,000 per job | A one-off file from a person at the dashboard |
| `POST /v1/jobs` with a webhook | Up to 5,000 per job | A scripted backfill that needs a signal when it ends |
| Bulk enrich in your tab | Up to 5,000, 25 per call | A short file you watch finish |
| `POST /v1/batch` | 25 per call | Your own loop or a no-code tool |

## How do you automate CRM enrichment with the batch API?

Call `POST /v1/batch` from any tool that can send an HTTP request. One call takes up to 25 items, and each item has an `op`: `person`, `email`, `mobile`, `company`, `company_tech` or `email_pattern`. Put your CRM record ID in `id`. It comes back unchanged, so you can write each result to the right record.

Each item is billed like its single call, and the response carries `credits_used`. Keep to about one call a second. On a `429`, wait `retry_after` seconds. A `402` means the free limit is reached, the plan's credits are used up, or billing needs attention. Any tool that sends HTTP requests can call the endpoint, such as n8n, Make, Zapier or [Google Sheets](https://enrich.chat/integrations/google-sheets/). For one record at a time, see how to enrich from [Clay](https://enrich.chat/integrations/clay/), [HubSpot](https://enrich.chat/integrations/hubspot/) and [Salesforce](https://enrich.chat/integrations/salesforce/). Enrich.chat does not refresh records on a schedule, so your automation tool sets the schedule.

```text
Use www.enrich.chat to read my attached CRM export, find the work email for every contact with no email using their name and company website, and add the email, its status and credits_used as new columns. Leave every existing column unchanged, mark rows with no match as not found, and give me the total credits_used.
```

An AI agent can run the same job. Add the MCP server from the [install page](https://enrich.chat/install/), and use the [API](https://enrich.chat/features/api/) for anything the agent cannot reach.

## How much does CRM enrichment cost on Enrich.chat?

You spend credits only on matches. Take a CRM with 2,000 contacts missing a work email and 500 accounts missing company details. If every row matches, the two jobs use 22,500 credits, which fits the Growth plan at $29 a month.

| Job and mode | Rows | Credits if every row matches |
| --- | --- | --- |
| Fill missing work emails (Find work emails) | 2,000 contacts | 20,000 |
| Fill company details (Enrich companies) | 500 accounts | 2,500 |
| **Both jobs** |  | **22,500** (Growth plan, $29 a month) |

Rows that find nothing use 0 credits, so the real number is at or below this. A work email found uses 10 credits, a mobile 250 and a company profile 5. Every plan includes every product and unlimited seats, and plans bill monthly. On a team, the owner's credits pay for every job a member runs, and a viewer cannot spend credits (see [team workspaces](https://enrich.chat/features/team-workspaces/)). Credits renew with each payment, and unused credits do not roll over. The [pricing page](https://enrich.chat/pricing/) lists the plans, and `GET /v1/usage` shows what you have left.

## What are the limits of this route?

This route is a manual round trip, and it has gaps you should know about before you commit.

- There is no native HubSpot or Salesforce app and no automatic sync. You import the file, or your automation writes the data.
- There is no scheduled refresh. You start every run yourself, whether it runs in the tab or as a server job. For records that update inside the CRM on their own, HubSpot's built-in enrichment [refreshes enriched records monthly](https://knowledge.hubspot.com/records/get-started-with-data-enrichment) for the properties it covers.
- A job holds at most 5,000 rows, up to three run at once, and results are kept for 30 days. A webhook announces finished jobs only, not new matches or changes to people.
- Enrich.chat does not merge or deduplicate your records. Do that in the CRM first.
- Company lookups take a website, not a company name. A work email is looked up on the email input, and personal addresses such as Gmail are not.
- Every contact is a strong lead, not a guarantee. Read the status before you send.

Enriching a record does not change your duties. The FTC says [CAN-SPAM makes no exception](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business) for business-to-business email, and the ICO says a [work address can still be personal data](https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/business-to-business-marketing/). Anyone can opt out at [enrich.chat/opt-out](https://enrich.chat/opt-out/), and opt-outs are applied to every result. Your team's shared [do-not-contact list](https://enrich.chat/features/do-not-contact/) leaves people out of list exports and background jobs. This is not legal advice.

## Frequently asked questions

### Can I enrich HubSpot contacts with Enrich.chat?

Yes. Export the contacts as a CSV, run the file through Bulk enrich in the Enrich.chat dashboard, then import the result with HubSpot's importer set to update existing records only, matched on Record ID. HubSpot's own data enrichment does not fill contact emails or mobiles, so this adds them. There is no HubSpot app, so you run the import yourself. The [HubSpot page](https://enrich.chat/integrations/hubspot/) covers workflow routes.

### How do I enrich Salesforce leads?

Run a report of your leads and export it as CSV with Details Only, upload it to Bulk enrich, match the name and company website columns, and run Find work emails or Enrich people. Then load the result with the Data Import Wizard, which takes up to 50,000 records at a time, or with Data Loader for more. Enrich.chat has no Salesforce app. The [Salesforce page](https://enrich.chat/integrations/salesforce/) covers Flow and Apex.

### Does Enrich.chat sync with my CRM automatically?

No. Enrich.chat has no native HubSpot or Salesforce app and no automatic sync. You move data by CSV, or you call the API from an automation tool, an AI agent or your own code, which writes the results back on a schedule you set. Enrich.chat does not refresh records on a schedule. A background job runs when you start it, keeps going after you close the tab, and can tell your app with a webhook when it finishes.

### How many rows can I enrich at once?

A background job takes up to 5,000 rows. In Bulk enrich, leave Run it on the server ticked and it carries on after you close the tab or the laptop. Results are kept 30 days, then deleted, so download them. You can instead run the file in your browser tab, 25 rows per call, and keep the tab open. For a bigger file, split it into jobs. The batch API takes up to 25 items per call.

### How much does CRM enrichment cost?

You spend credits only on matches: 10 per work email found, 250 per mobile and 5 per company profile. A row that finds nothing uses 0. Filling 2,000 emails and 500 company profiles takes at most 22,500 credits, which fits the Growth plan at $29 a month. Plans bill monthly and include unlimited seats. A background job is billed the same way, row by row.

### Will enrichment overwrite what is already in my CRM?

Enrich.chat never writes to your CRM. It gives you a file or JSON, and your import settings decide what changes. In HubSpot, choose Update existing records only and tick Don't overwrite on properties that already hold values. HubSpot also ignores blank cells in an import file, so a row with no match leaves the record as it was.

## Sources

1. [Enrich your contact and company data](https://knowledge.hubspot.com/records/enrich-your-contact-and-company-data)
2. [Get started with data enrichment](https://knowledge.hubspot.com/records/get-started-with-data-enrichment)
3. [Export your records](https://knowledge.hubspot.com/import-and-export/export-records)
4. [Import records and activities](https://knowledge.hubspot.com/import-and-export/import-objects)
5. [Set up your import file](https://knowledge.hubspot.com/import-and-export/set-up-your-import-file)
6. [Deduplication of records](https://knowledge.hubspot.com/records/deduplication-of-records)
7. [Data Import Wizard](https://help.salesforce.com/s/articleView?id=sf.data_import_wizard.htm&language=en_US&type=5)
8. [Export a Report](https://help.salesforce.com/s/articleView?id=sf.reports_export.htm&language=en_US&type=5)
9. [Import Data](https://trailhead.salesforce.com/content/learn/modules/lex_implementation_data_management/lex_implementation_data_import)
10. [CAN-SPAM Act: A Compliance Guide for Business](https://www.ftc.gov/business-guidance/resources/can-spam-act-compliance-guide-business)
11. [Business-to-business marketing](https://ico.org.uk/for-organisations/direct-marketing-and-privacy-and-electronic-communications/business-to-business-marketing/)

## Examples

**cURL**

```bash
# id is your CRM record ID. It comes back unchanged.
curl -X POST "https://enrich.chat/v1/batch" \
  -H "content-type: application/json" \
  -d '{"items": [
    {"id": "101", "op": "email",
     "name": "Ana Lee", "domain": "example.com"},
    {"id": "102", "op": "email",
     "name": "Raj Rao", "domain": "example.org"},
    {"id": "103", "op": "email",
     "name": "Mia Cho", "domain": "example.net"},
    {"id": "acct-9", "op": "company",
     "domain": "example.com"}
  ]}'

# The free tier needs no key. With a paid key from
# enrich.chat/account/, add this line to the call:
#   -H "Authorization: Bearer $ENRICH_KEY"
```

**Response**

```json
{
  "count": 4,
  "credits_used": 25,
  "free_lookups_remaining": 7,
  "results": [
    {"id": "101", "status": 200, "result": {
      "found": true,
      "work_email": {"value": "ana.lee@example.com",
                     "status": "verified"},
      "title": "Director of Operations",
      "credits_used": 10}},
    {"id": "102", "status": 200, "result": {
      "found": true,
      "work_email": {"value": "raj.rao@example.org",
                     "status": "catch_all"},
      "title": "Controller",
      "credits_used": 10}},
    {"id": "103", "status": 200, "result": {
      "found": false,
      "guess": {"email": "mia.cho@example.net",
                "basis": "most common format"},
      "credits_used": 0}},
    {"id": "acct-9", "status": 200, "result": {
      "found": true,
      "name": "Example Co",
      "industry": "Software Development",
      "size": "201-500",
      "credits_used": 5}}
  ]
}
```

**Python**

```python
import csv, os, time
import requests

# Optional: a paid key from enrich.chat/account/
# lifts the free limit
key = os.environ.get("ENRICH_KEY")
headers = {"Authorization": "Bearer " + key} if key else {}

rows = list(csv.DictReader(open("crm-export.csv", newline="")))
for start in range(0, len(rows), 25):
    chunk = rows[start:start + 25]
    items = [{"id": r["record_id"], "op": "email",
              "name": r["name"], "domain": r["website"]}
             for r in chunk]
    res = requests.post("https://enrich.chat/v1/batch",
                        json={"items": items},
                        headers=headers, timeout=60)
    body = res.json()
    if res.status_code != 200:
        print(body["error"], body["message"])
        break
    for item in body["results"]:
        if item["status"] != 200:
            print(item["id"], item["error"])
        elif item["result"]["found"]:
            print(item["id"], item["result"]["work_email"])
    time.sleep(1)  # about one call a second
```

**Background job**

```text
# A job belongs to an account, so it needs a key from enrich.chat/account/ (or a dashboard login).
# 1. Start the job on the server. id is your CRM record ID and comes back unchanged.
curl -X POST "https://enrich.chat/v1/jobs" \
  -H "Authorization: Bearer $ENRICH_KEY" \
  -H "content-type: application/json" \
  -d '{"op": "email", "label": "CRM backfill", "items": [
    {"id": "101", "name": "Ana Lee", "domain": "example.com"},
    {"id": "102", "name": "Raj Rao", "domain": "example.org"}]}'

# Response (202, illustrative)
{"id": "job_2xYb9QwErTy8UiOp", "status": "queued", "total": 2,
 "status_url": "https://enrich.chat/v1/jobs/job_2xYb9QwErTy8UiOp"}

# 2. Optional: add a webhook once. The signing secret is shown one time.
curl -X POST "https://enrich.chat/v1/webhooks" \
  -H "Authorization: Bearer $ENRICH_KEY" \
  -H "content-type: application/json" \
  -d '{"url": "https://example.com/hooks/enrich", "events": ["job.completed", "job.stopped"]}'

# 3. When the webhook fires (or the status_url says "done"), download the file.
curl -H "Authorization: Bearer $ENRICH_KEY" \
  "https://enrich.chat/v1/jobs/job_2xYb9QwErTy8UiOp/results?format=csv"
```

**Check a webhook**

```text
import hashlib, hmac, time

def is_valid(secret: str, signature: str, raw_body: bytes) -> bool:
    # The Enrich-Signature header looks like: t=1792400000,v1=<hex>
    parts = dict(p.split("=", 1) for p in signature.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False  # reject an old timestamp
    message = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

# In your web handler: read the raw body, call is_valid() with the
# whsec_... secret and the Enrich-Signature header, then fetch the results.
```

## Prices

| What you get | Credits |
| --- | --- |
| Work email found | 10 |
| Mobile number found | 250 |
| Company tech stack or profile found | 5 |

10 free lookups a day for every IP address, no signup. Nothing is charged when nothing is found. Monthly plans start at $29 for 100,000 credits.

## Try this prompt

Enrich a CRM export from your agent

```text
Use www.enrich.chat to read my attached CRM export, find the work email for every contact with no email using their name and company website, and add the email, its status and credits_used as new columns. Leave every existing column unchanged, mark rows with no match as not found, and give me the total credits_used.
```

## Related

- [Bulk enrichment from a CSV](https://enrich.chat/features/bulk-enrichment/)
- [CRM data enrichment: what, how often, how](https://enrich.chat/blog/crm-data-enrichment/)
- [HubSpot data enrichment: fill emails and mobiles](https://enrich.chat/integrations/hubspot/)
