> Reverse Contact API documentation. This is the Markdown copy of https://docs.reversecontact.com/guides/enrich-a-csv-file
> Every page is listed at https://docs.reversecontact.com/llms.txt, every endpoint with its credit cost and status at https://docs.reversecontact.com/catalog.json

# Enrich a CSV file

> Upload a CSV of contacts in the app and download it enriched, or loop over its rows with the API.

- Last verified: 2026-10-16

You have a file of contacts and want it back with profiles, work emails or phone numbers. There are two ways to do it:

- **In the app, no code:** [Bulk Import](https://app.reversecontact.com/bulk) takes your CSV and gives you an enriched CSV back.
- **With the API:** your code reads the file and calls an endpoint for each row. The API takes JSON, one person per request: there is no endpoint that accepts a CSV file.

## Bulk Import in the app

1. **Upload your file.** Open [Bulk Import](https://app.reversecontact.com/bulk) and drop a CSV of up to 25,000 rows and 25 MB. You can also paste a list of emails, LinkedIn profile URLs or names instead of uploading a file.
2. **Map your columns.** Tell the import which columns hold the LinkedIn profile URL, the email, the first and last name, the company name and the company domain. Common headers such as `linkedin_url`, `email` or `profile_url` are detected for you, and you can change any mapping.
3. **Choose what to look up.** Each row can get its full profile, a work email and a phone number. Select one or several.
4. **Review and start.** The review step counts the rows that will be enriched, the duplicates and the rows with no usable input, and shows the credits the import reserves before it starts.
5. **Download the result.** The job page follows the import row by row. When it completes, you receive an in-app notification and an email, and the enriched CSV is ready to download.

A row needs at least one usable input: a LinkedIn profile URL, an email, or a name with its company. A row that repeats an earlier one is not enriched or charged twice.

### What the enriched file contains

The result keeps every column of your file and adds these columns to each row:

| Column | Content |
| ------ | ------- |
| `rc_status` | `enriched`, `partial`, `not_found`, `invalid_input`, `skipped`, `insufficient_credits` or `error` |
| `rc_message` | Why a row was not enriched, in plain English |
| `rc_*` profile columns | The fields of the [Enrich profile](https://docs.reversecontact.com/endpoints/enrich-person) response, one column per field (`rc_first_name`, `rc_current_position_title`, `rc_location_city`...). List fields such as `rc_experience` hold JSON. |
| `rc_email`, `rc_email_status` | The work email, when you asked for one |
| `rc_phone`, `rc_phone_type`, `rc_phone_status` | The phone number, when you asked for one |

`partial` means some of the requested lookups found a result and others did not. When you ask for contact details without the profile, a single `rc_linkedin_url` column replaces the profile columns. The file is UTF-8 and opens directly in Excel.

### What it costs

You pay only for rows that return a result. A row that is not found, invalid, a duplicate or skipped costs nothing.

| Lookup | Credits per row |
| ------ | :-------------: |
| Profile from a LinkedIn profile URL | 1 when found |
| Profile from a LinkedIn profile URL, with Live enrichment | 2 when found |
| Profile from an email or a name | 2 when found |
| Work email | 3 when found |
| Phone number | 20 when found |

If your balance does not cover every row, the import takes the rows it can afford in file order and marks the others `insufficient_credits`. Top up and upload those rows again to enrich them.

**Live enrichment** applies to rows with a LinkedIn profile URL: it scrapes each profile fresh instead of reading the cached profile, and a large file can take several minutes. Bulk Import works during the free trial with your trial credits, except Live enrichment.

## Enrich a CSV with the API

Read the file in your code and call, for each row, the endpoint that matches what the row holds:

| The row holds | Endpoint | Credits |
| ------------- | -------- | :-----: |
| A LinkedIn profile URL | [`POST /v2/fetch/persons`](https://docs.reversecontact.com/endpoints/fetch-profile) | 1 when found |
| An email, or a name with its company | [`POST /v2/enrich/persons`](https://docs.reversecontact.com/endpoints/enrich-person) | 2 when found |
| You need a work email | [`POST /v2/contact/email`](https://docs.reversecontact.com/endpoints/contact-email) | 3 when found |
| You need a phone number | [`POST /v2/contact/phone`](https://docs.reversecontact.com/endpoints/contact-phone) | 20 when found |

The example below reads `contacts.csv`, enriches each row from its `email` column with `POST /v2/enrich/persons`, and writes `contacts_enriched.csv` with five profile columns added. A `404` means no match and costs nothing. A `429` means the per-minute rate limit is reached: the example waits the number of seconds in `error.details.retryAfter`, then retries the same row.

**Enrich a CSV**

```javascript [JavaScript]
// npm install csv-parse csv-stringify
import { readFileSync, writeFileSync } from "node:fs";
import { parse } from "csv-parse/sync";
import { stringify } from "csv-stringify/sync";

const KEY = "YOUR_API_KEY";
const FIELDS = ["firstName", "lastName", "currentPositionTitle", "currentCompanyName", "linkedinUrl"];

async function enrich(email) {
  while (true) {
    const response = await fetch("https://api.reversecontact.com/v2/enrich/persons", {
      method: "POST",
      headers: { "Authorization": `Bearer ${KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({ email })
    });
    if (response.status === 429) {
      const { error } = await response.json();
      await new Promise((resolve) => setTimeout(resolve, error.details.retryAfter * 1000));
      continue;
    }
    if (response.status === 404) return null;
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return (await response.json()).data.person;
  }
}

const rows = parse(readFileSync("contacts.csv"), { columns: true, bom: true });
for (const row of rows) {
  const person = row.email ? await enrich(row.email) : null;
  for (const field of FIELDS) row[field] = person?.[field] ?? "";
}
writeFileSync("contacts_enriched.csv", stringify(rows, { header: true }));
```
```python [Python]
# pip install requests
import csv
import time

import requests

KEY = "YOUR_API_KEY"
FIELDS = ["firstName", "lastName", "currentPositionTitle", "currentCompanyName", "linkedinUrl"]

def enrich(email):
    while True:
        response = requests.post(
            "https://api.reversecontact.com/v2/enrich/persons",
            headers={"Authorization": f"Bearer {KEY}"},
            json={"email": email},
        )
        if response.status_code == 429:
            time.sleep(response.json()["error"]["details"]["retryAfter"])
            continue
        if response.status_code == 404:
            return None
        response.raise_for_status()
        return response.json()["data"]["person"]

with open("contacts.csv", newline="", encoding="utf-8-sig") as source, \
        open("contacts_enriched.csv", "w", newline="", encoding="utf-8") as target:
    reader = csv.DictReader(source)
    writer = csv.DictWriter(target, fieldnames=reader.fieldnames + FIELDS)
    writer.writeheader()
    for row in reader:
        person = enrich(row["email"]) if row["email"] else None
        for field in FIELDS:
            row[field] = (person or {}).get(field) or ""
        writer.writerow(row)
```

For a row with a LinkedIn profile URL, call `POST /v2/fetch/persons` with `{"url": ...}` instead: it costs 1 credit instead of 2. Work emails and phone numbers are asynchronous: send every row first, keep the `webhookId` of each request, then collect the results by [polling](https://docs.reversecontact.com/guides/polling) or through [webhooks](https://docs.reversecontact.com/guides/webhooks). See [Rate limits & credits](https://docs.reversecontact.com/guides/rate-limits-credits) to size a large file against your per-minute limit.

## FAQ

### Can I upload a CSV file to the Reverse Contact API?

No. The Reverse Contact API takes JSON, one person per request, and has no endpoint that accepts a CSV file. To enrich a CSV without code, use Bulk Import in the app: upload the file, map its columns and download the enriched CSV. With the API, read the file in your code and call `POST /v2/enrich/persons` or `POST /v2/fetch/persons` for each row.

### How many rows can Bulk Import enrich at once?

Bulk Import accepts a CSV of up to 25,000 rows and 25 MB per import. Split a larger file into several imports. Each row needs a LinkedIn profile URL, an email, or a name with its company.

### How much does it cost to enrich a CSV file?

Bulk Import charges only rows that return a result: 1 credit for a profile found from a LinkedIn profile URL, 2 credits for a profile found from an email or a name, 3 credits for a work email found and 20 credits for a phone number found. Rows that are not found, invalid or duplicated cost nothing.

### What does the enriched CSV contain?

The enriched CSV from Bulk Import keeps every column of the original file and adds `rc_status`, `rc_message` and one `rc_*` column per profile field, plus `rc_email` and `rc_phone` columns when a work email or a phone number was requested. `rc_status` tells whether each row was enriched, not found, invalid or skipped.

### What happens when my credits run out during a Bulk Import?

Bulk Import reserves credits before the job starts. If the balance does not cover every row, Bulk Import enriches the rows it can afford in file order and marks the other rows `insufficient_credits` in the enriched CSV, without charging them. Top up and upload those rows again to enrich them.
