# Business List Quality & Change Detection (`emredurukan/business-list-quality`) Actor

Clean supplied JSON or CSV business lists, flag duplicate candidates and compare snapshots before CRM imports. Includes branch-aware matching and review reasons.

- **URL**: https://apify.com/emredurukan/business-list-quality.md
- **Developed by:** [Emre Durukan](https://apify.com/emredurukan) (community)
- **Categories:** Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 input record processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Business List Quality and Snapshot Changes

Check a business list before importing it into your CRM. Find data quality issues, flag exact or possible duplicates, and compare an export with an earlier snapshot to see which supplied records are new or changed.

This Actor works with **business records you provide** as JSON or CSV. It returns normalized records, review reasons and row references. It does not crawl websites, collect leads, enrich records or update your CRM.

**Version 0.3.0 is a paid release:** $0.10 per successful analysis plus $0.001 per input record across both supplied snapshots. Review the Pricing tab and the counting rules below before starting a run.

### Pricing and run budget

Prices are in USD. Each completed analysis costs **$0.10**, plus **$0.001 for each input record in the current and previous snapshots combined**. Duplicate and invalid records count too. Records present only in the previous snapshot count toward processing charges, even though the dataset contains results for current rows only.

| Input | Analysis | Input-record charges | Total at these rates |
| --- | --- | --- | --- |
| 100 current + 100 previous records | $0.10 | 200 × $0.001 | **$0.30** |
| 5,000 current + 5,000 previous records | $0.10 | 10,000 × $0.001 | **$10.10** |
| Empty current + empty previous snapshot | $0.10 | $0.00 | **$0.10** |
| Empty current + 100 previous records | $0.10 | 100 × $0.001 | **$0.20** |

An empty current snapshot still completes an analysis and incurs the analysis fee. Previous input records are still counted, while the dataset has zero current result rows. CSV headers and skipped blank lines are not business records.

Set a sufficient maximum total charge for the complete run. Before analysis, the Actor checks the full quote against the available run budget. If it cannot cover the complete quote, the run fails before analysis or analysis/input-record event charges; it does not analyze a smaller paid subset. Billing is determined by the Actor's platform pricing, not an input switch. The Pricing tab on this Actor page shows the effective event prices.

**Charges and output storage are not one transaction.** A platform interruption or failure can leave partial charges, partial output or an incomplete billing state. A failed run does not imply that no events were charged. Check the run's charges and `SUMMARY` before deciding whether to start a new run. Review results only when the run succeeds and `SUMMARY.output.complete` is `true`.

### Common uses

- Prepare a client-supplied lead list for review before a CRM import.
- Compare weekly or monthly business exports using stable source IDs.
- Give an agency a review queue for conflicting identities, repeated records and malformed fields.
- Keep known branches separate when businesses share a website or phone number.

### Quick start

1. Open the Actor's Input tab and use the JSON input editor.
2. Paste the fictional example below.
3. Review the Pricing tab, allow a sufficient maximum total charge, and start the run.
4. When the run succeeds, check `SUMMARY` and confirm `output.complete` is `true`.
5. Review the result rows in the default dataset, then export the dataset in your preferred format.

```json
{
  "currentRecords": [
    {"id": "B001", "name": "Harbor Dental", "website": "https://harbor.example", "country": "US", "city": "Washington"},
    {"id": "B002", "name": "Cedar Interior Studio", "website": "https://cedar.example", "country": "US", "city": "Washington"},
    {"id": "B003", "name": "Maple Design", "website": "https://maple.example", "country": "US", "city": "Washington"},
    {"id": "B003", "name": "Maple Design", "website": "https://maple.example", "country": "US", "city": "Washington"}
  ],
  "previousRecords": [
    {"id": "B001", "name": "Harbor Dental", "website": "https://harbor.example", "country": "US", "city": "Washington"},
    {"id": "B002", "name": "Cedar Interiors", "website": "https://cedar.example", "country": "US", "city": "Washington"}
  ],
  "matchKey": "id",
  "includeContactDetails": false
}
```

This example produces four current result rows: one `unchanged`, one `changed`, one `new` and one `exact_duplicate`. The changed row identifies `name` in `changedFields`; the repeated Maple Design row points to current row 3 through `duplicateOfRow`.

For a first quality check without change detection, omit both previous-snapshot fields. This creates a **baseline**. An explicit `previousRecords: []` instead means that you are comparing with an empty list, so valid current records can be marked `new`. Duplicate and invalid findings can take precedence in either mode.

### JSON, CSV and field mapping

Supply exactly one current source: `currentRecords` or `currentCsv`. You may optionally supply exactly one previous source: `previousRecords` or `previousCsv`. JSON and CSV can be used in the same run, one format per snapshot.

CSV input is the **text contents** of a comma-delimited CSV, including its header row. It is not a URL or file path. UTF-8 text, a leading BOM and quoted cells are supported. Headers must be unique and nonempty, and every data row must have the same number of cells as the header.

Use `fieldMap` when your CRM or agency export uses different column names. The direction is **canonical field → source column**, and the same mapping applies to both snapshots:

```json
{
  "currentCsv": "CRM ID,Business Name,Company URL,Country\nB001,Harbor Dental,https://harbor.example,US\nB002,Cedar Interiors,https://cedar.example,US\n",
  "fieldMap": {
    "id": "CRM ID",
    "name": "Business Name",
    "website": "Company URL",
    "country": "Country"
  },
  "countryCode": "US"
}
```

Supported canonical fields are `id`, `name`, `website`, `phone`, `country`, `address`, `city`, `postalCode` and `branchId`. A row needs a usable `id` or `name`. Unknown columns, including email, are ignored by the analysis.

| Option | How to use it |
| --- | --- |
| `matchKey` | Use a stable, unique `id` where possible. Alternatives are `website` and `phone`, with additional identity checks. |
| `countryCode` | Optional supported two-letter phone-region hint, such as `US` or `TR`. Use the business's actual country. |
| `compareFields` | Nonempty list of unique canonical fields to compare after matching. Defaults to all supported fields except `id`. |
| `includeContactDetails` | Defaults to `false`. Set to `true` only when you need normalized phone, street address and postal code values in the results. |
| `maxRecords` | Optional lower per-snapshot limit, from 1 to 5,000. Oversized snapshots fail; rows are not silently cut off. |
| `deleteInputOnSuccess` | Defaults to `false`. Requests best-effort removal of the run's input record after successful output storage. See the data-handling notes below. |

### Matching and results

The default dataset contains one result item for each current row. The key-value store's `SUMMARY` record contains counts, quality issue totals, warnings, pricing metadata, completion information and input-cleanup status.

**Use results only when the run succeeds and `SUMMARY.output.complete` is `true`.** A failed or interrupted run can leave partial output; the presence of dataset items alone does not establish completion.

| Status | Meaning |
| --- | --- |
| `baseline` | Processed without a previous snapshot. |
| `new` | No reliable match was found in the supplied previous snapshot. |
| `changed` | A matched row differs in at least one selected field after normalization. |
| `unchanged` | A matched row has no detected difference in selected fields. |
| `exact_duplicate` | A repeated normalized record meets the strict duplicate conditions. Review before removing it. |
| `possible_duplicate` | A duplicate candidate or ambiguous identity needs human review. |
| `invalid` | A row cannot be processed reliably; inspect its reason codes. |

Each item includes `rowNumber`, `record`, `previousRecord`, `changedFields`, `reasonCodes`, `matchedPreviousRow`, `duplicateOfRow` and `candidateRows`. Row references start at 1. `matchedPreviousRow` refers to the previous snapshot; duplicate and candidate references refer to the current snapshot.

Matching is conservative. Exact duplicates require identical normalized canonical values, no quality issues, and a stable ID or corroborating name, address and country. A shared name, website or phone alone does not prove a duplicate. Conflicting records with the same branch-qualified ID require review. Supply `branchId` for known locations: different explicit branches are kept distinct, and missing or ambiguous branch information can prevent a reliable match. The Actor never merges records automatically.

An earlier row missing from the current upload is **not reported as deleted or closed**. “New” means new to the supplied comparison, and “changed” means the supplied values differ after normalization. Neither label verifies a real-world business event.

### Limits

- Up to **5,000 records per snapshot**, or 10,000 across current and previous snapshots.
- Complete JSON input up to 8 MiB; each CSV text up to 3 MiB.
- Up to 100 CSV columns and 2,000 characters per supported field value.
- Duplicate candidate scanning considers at most 64 candidates per current row and returns at most 20 candidate references. Truncation is reported in warnings. This is not an exhaustive all-pairs search.

Phone and website checks normalize supplied values and flag some malformed data. They do not test website availability, phone reachability, contact ownership, business existence, marketing consent or legal identity. Review warnings and source records before importing, merging or discarding data. Runs can fail or be temporarily unavailable; check the run status before using output.

### Data handling and use conditions

Submit only business information you are authorized to process, including any required permission from your client or organization. Business names, direct phone numbers and addresses can contain personal information. Do not submit credentials, payment information, sensitive personal data or unrelated information.

Supported record fields are sent **over HTTPS to a Cloudflare-hosted analysis service**. This includes supplied contact fields even when `includeContactDetails` is `false`: that option filters the output, not the processing input. Unknown columns such as email are excluded from the remote analysis request, but can remain in the original Actor input stored on Apify. Records are not sent to an LLM or an enrichment service, and websites listed in your records are not fetched.

By default, `phone`, `address` and `postalCode` are omitted from both current and previous result records. Names, IDs, websites, cities, countries and branch IDs remain. **This is not anonymization and does not remove contact data from the uploaded input.**

Apify inputs, datasets, logs and other storage remain subject to platform retention and account access settings. The Cloudflare application processes requests in memory, does not intentionally persist record bodies, and does not intentionally log record values. These application choices do not guarantee deletion of all infrastructure records or metadata.

With `deleteInputOnSuccess: true`, check `SUMMARY.privacy.inputDeletionStatus` for `deleted` or `failed`. Cleanup is best effort and concerns the run's input record only. It does not erase datasets, backups, logs, previous runs or copies you hold. Failed or interrupted runs may retain input. Set appropriate access and retention rules before submitting production data.

You remain responsible for decisions about corrections, imports, merges, retention and deletion. No guarantee is made that all errors, duplicates or changes will be found. Do not use the output as the sole basis for a consequential decision about a person or business.

### Support

Use the **Support tab on this Actor page** to report an issue. Include the run ID, error code and a small fictional example that reproduces the problem. Remove customer records, credentials and unnecessary personal information before sharing. No response-time commitment is offered.

# Actor input Schema

## `currentRecords` (type: `array`):

JSON array of business objects. Use this OR currentCsv, never both. At most maxRecords rows, with a hard ceiling of 5,000. All supplied records, including duplicates and invalid rows, count toward input-record charges. An empty array produces no dataset rows but still incurs the completed-analysis fee and any previous-record charges.

## `currentCsv` (type: `string`):

CSV text including a header row. Use this OR currentRecords. Match columns using fieldMap when their names differ from canonical fields. Parsed business records count toward input-record charges; headers and skipped blank lines do not.

## `previousRecords` (type: `array`):

Optional previous snapshot, supplied in the same format as currentRecords. Omit for a baseline check. \[] means compare against an explicitly empty snapshot. Do not combine with previousCsv. Every supplied previous record counts toward input-record charges, including records absent from the current snapshot. Dataset results describe current rows only; absent previous rows are not reported as deleted.

## `previousCsv` (type: `string`):

Optional CSV snapshot including a header row. Do not combine with previousRecords. Each snapshot is limited by maxRecords; the combined hard ceiling is 10,000 rows. Parsed previous records count toward input-record charges even when absent from the current snapshot. Dataset results describe current rows only.

## `fieldMap` (type: `object`):

Canonical field to input column mapping, for example {"id":"place_id","name":"title"}. Canonical fields: id, name, website, phone, country, address, city, postalCode, branchId. The same mapping applies to both snapshots. Unmapped fields use canonical column names.

## `countryCode` (type: `string`):

Optional supported two-letter phone-region code, for example TR or US, used for national phone-number parsing. This does not verify reachability or ownership.

## `matchKey` (type: `string`):

Canonical key used to match rows across snapshots. A stable unique id is recommended. Duplicate or missing keys are reported rather than silently matched.

## `compareFields` (type: `array`):

Canonical fields to compare after normalization. Contact values are still omitted from output unless includeContactDetails is true.

## `includeContactDetails` (type: `boolean`):

Default false omits phone, street address and postal code values from output records. Names, business IDs, websites, cities and other allowed fields may still identify a business. Supplied contact fields are still sent over HTTPS to Cloudflare for analysis. This option filters output; it does not anonymize input or remove uploaded Actor input from Apify storage.

## `maxRecords` (type: `integer`):

Hard validation limit for EACH snapshot, from 1 to 5,000. Oversized input fails before output writes; rows are never silently truncated.

## `deleteInputOnSuccess` (type: `boolean`):

Default false retains input under the platform's storage policy. True requests best-effort removal of the run's input record after a successful run. Check SUMMARY.privacy for the actual result. This does not erase datasets, platform copies, backups, logs or previous runs; failed or interrupted runs may retain input.

## Actor input object example

```json
{
  "currentRecords": [
    {
      "id": "sample-1",
      "name": "Sample Business",
      "website": "https://example.com",
      "country": "TR"
    }
  ],
  "fieldMap": {},
  "matchKey": "id",
  "compareFields": [
    "name",
    "website",
    "phone",
    "country",
    "address",
    "city",
    "postalCode",
    "branchId"
  ],
  "includeContactDetails": false,
  "maxRecords": 5000,
  "deleteInputOnSuccess": false
}
```

# Actor output Schema

## `results` (type: `string`):

One dataset item per analyzed result, with contact values omitted by default. Empty snapshots can produce an empty dataset.

## `summary` (type: `string`):

JSON summary, quoted price, actual acknowledged Actor event charges, warnings, completion marker and best-effort input-deletion status. Check the run status and SUMMARY.output.complete before using results; interrupted runs can have incomplete output or charges.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "currentRecords": [
        {
            "id": "sample-1",
            "name": "Sample Business",
            "website": "https://example.com",
            "country": "TR"
        }
    ],
    "fieldMap": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("emredurukan/business-list-quality").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "currentRecords": [{
            "id": "sample-1",
            "name": "Sample Business",
            "website": "https://example.com",
            "country": "TR",
        }],
    "fieldMap": {},
}

# Run the Actor and wait for it to finish
run = client.actor("emredurukan/business-list-quality").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "currentRecords": [
    {
      "id": "sample-1",
      "name": "Sample Business",
      "website": "https://example.com",
      "country": "TR"
    }
  ],
  "fieldMap": {}
}' |
apify call emredurukan/business-list-quality --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,emredurukan/business-list-quality"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/ESAyJdB9e0Vu3UCxg/builds/N8DjcUbhr5LAPC6SF/openapi.json
