# CSV Diff & Reconciliation – Compare Two CSV Exports by Key (`kestrelwick/csv-export-parity-auditor`) Actor

Compare two CSV exports by key and get one row per difference: mismatched fields, rows missing on either side, duplicate and blank keys. Explicit column mapping, composite keys, every row accounted for. Matches are free. No API keys.

- **URL**: https://apify.com/kestrelwick/csv-export-parity-auditor.md
- **Developed by:** [Kestrelwick Studios](https://apify.com/kestrelwick) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.80 / 1,000 parity exception founds

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

## CSV Diff & Reconciliation – Compare Two CSV Exports by Key

Paste two CSV exports of the same records (say, your shop's orders and your accounting system's orders) and get **one row per problem**: values that don't match, records missing on either side, duplicate keys and blank keys. You tell us which columns hold the key and which columns should agree, and we account for **every single row** on both sides, so nothing slips through silently.

Values are compared as exact strings. We don't guess at numbers or dates, round anything or "fix" your data: `10.00` vs `10` is reported, and raw values are always kept in the output. Everything runs inside the run, with no API keys, proxies or outside services.

Built and maintained by **Kestrelwick Studios**.

### Who it's for

- **Finance and ops teams** reconciling orders, invoices or payouts between a store, a payment provider and the books.
- **Data and CRM teams** checking a migration or sync: did every record arrive, and did the fields survive?
- **E-commerce sellers** comparing a supplier feed with their catalog (SKU, price, stock).
- **Developers and analysts** who need a repeatable parity check they can call from the API, Make, Zapier or n8n.

### What we return

One dataset row per finding:

| `type` | Meaning |
|---|---|
| `MISMATCH` | The key exists once on each side, but at least one compared column differs. `changedFields` lists each column pair with both raw values. |
| `LEFT_ONLY` / `RIGHT_ONLY` | The key exists on one side only. |
| `DUPLICATE_KEY` | The key appears more than once on either side. We quarantine every row with that key (on both sides) instead of picking a winner; `keyGroupCounts` shows how many on each side. |
| `MISSING_KEY` | A row has a blank key value. |
| `MATCH` | An identical pair. Only written if you turn on **Include matching pairs**, and always free. |

Every row also carries the normalized `key`, a `left` and `right` reference (record number, physical line, the raw key and compared values) and a stable `id` you can use to dedupe downstream.

A run summary is saved as `SUMMARY` in the run's key-value store: overall `status` (`EQUAL`, `EXCEPTIONS` or `EMPTY`), counts per finding type, and per-side row accounting where `parsedRows` always equals missing + duplicate + one-sided + matched + mismatched rows. `complete: true` means every finding was saved.

### Input

- **Left CSV** and **Right CSV**: paste each export, header row included (up to 2 MiB and 20,000 rows per side, 64 columns).
- **Key columns**: pairs of header names, for example `[{"left": "order_id", "right": "Order ID"}]`. Add up to 8 pairs for a composite key.
- **Columns to compare**: pairs of header names whose values should agree, for example `[{"left": "total", "right": "Amount"}]`. Columns you don't list are ignored.
- **Delimiter**: comma, semicolon or tab (the same for both files).
- **Key matching** and **Value comparison**: `exact` (default), `trim` (ignore surrounding spaces) or `trim_ascii_lower` (also ignore A–Z case).
- **Include matching pairs**: also write a free `MATCH` row for every identical pair.

```json
{
  "leftCsv": "order_id,total,status\nA-1001,49.00,paid\nA-1003,120.00,refunded\n",
  "rightCsv": "Order ID,Amount,Payment Status\nA-1001,49.00,paid\nA-1003,120.00,paid\n",
  "keyPairs": [{ "left": "order_id", "right": "Order ID" }],
  "comparePairs": [{ "left": "total", "right": "Amount" }, { "left": "status", "right": "Payment Status" }]
}
```

### Output example

```json
{
  "type": "MISMATCH",
  "key": ["A-1005"],
  "left":  { "record": 5, "lineEnd": 6, "keyValues": { "order_id": "A-1005" },
             "compareValues": { "email": "eli@example.com", "total": "64.20", "status": "paid" } },
  "right": { "record": 6, "lineEnd": 7, "keyValues": { "Order ID": "A-1005" },
             "compareValues": { "Customer Email": "eli@example.com", "Amount": "64.02", "Payment Status": "paid" } },
  "changedFields": [{ "leftHeader": "total", "rightHeader": "Amount", "leftValue": "64.20", "rightValue": "64.02" }],
  "id": "42ad32f2e1cd085226076a7b755115e517206b11abfb80c8be656890ad2f8fc6"
}
```

Export the findings as CSV, Excel, JSON or XML, or read them from the Apify API.

### Pricing

Pay per result: **$1.80 per 1,000 exception rows** (mismatches, one-sided rows, duplicate-key and missing-key rows). **Matching rows are free, there's no start fee, and a clean audit with no exceptions costs nothing in event fees.** Platform usage is included. Set a maximum charge per run to cap spend: if we reach it, we stop, mark the summary `complete: false`, and you're never charged past your limit.

### Strict by design

- Header names are matched exactly and are case-sensitive.
- All cells stay strings: no number, date or null interpretation. `001` ≠ `1`, `NULL` is just text.
- We accept quoted fields, embedded newlines, CRLF/LF line endings and a leading BOM. We reject malformed CSV (stray quotes, ragged rows, duplicate or missing headers) with a clear error instead of guessing, and we never truncate oversized inputs.
- Findings come out in a deterministic order with deterministic IDs, so the same input always gives the same result.

### Tips

- Compare a few columns that matter (amount, status, quantity) rather than every column, so the findings stay readable.
- If one system pads or upper-cases IDs, set **Key matching** to `trim` or `trim_ascii_lower`. Raw values are still shown.
- Start a fresh run for each audit; don't append to an old dataset.
- If you open the results in a spreadsheet, keep its formula protection on: we return your values unchanged, including any that look like formulas.

### FAQ

**Can it read files or URLs?** Not in this version: paste the CSV text into the input (or send it through the API). That keeps runs fast and your data inside the run.
**Why is `10.00` vs `10` a mismatch?** We compare exact text on purpose, so formatting drift between systems is caught rather than hidden.
**What happens with duplicate keys?** Every row sharing that key is reported as `DUPLICATE_KEY` on both sides; we don't pair them up, because any pairing would be a guess.
**How fast is it?** Two 20,000-row exports are audited in a couple of seconds.
**Support:** open an issue on the actor's Issues tab and we'll respond.

# Actor input Schema

## `leftCsv` (type: `string`):

Paste the first CSV export (for example your shop or CRM export), header row included. Up to 2 MiB UTF-8 and 20,000 rows. Cells are compared as exact strings.

## `rightCsv` (type: `string`):

Paste the second CSV export (for example your accounting or warehouse export), header row included. Same limits as the left CSV.

## `keyPairs` (type: `array`):

1 to 8 objects mapping the key column names on each side, e.g. \[{"left":"order_id","right":"Order ID"}]. Several pairs make a composite key. Header names are exact and case-sensitive.

## `comparePairs` (type: `array`):

1 to 64 objects mapping the columns whose values must match, e.g. \[{"left":"total","right":"Amount"}]. Columns you don't list are ignored.

## `delimiter` (type: `string`):

Field separator used by both CSVs.

## `keyNormalization` (type: `string`):

How keys are normalized before matching. Raw values are always kept in the output.

## `valueNormalization` (type: `string`):

How compared values are normalized. No number or date parsing: 10.00 and 10 are different.

## `includeMatches` (type: `boolean`):

Also write one MATCH row per identical pair. MATCH rows are free.

## Actor input object example

```json
{
  "leftCsv": "order_id,email,total,status\nA-1001,ana@example.com,49.00,paid\nA-1002,ben@example.com,15.50,paid\nA-1003,cy@example.com,120.00,refunded\nA-1004,dee@example.com,8.99,paid\nA-1005,eli@example.com,64.20,paid\nA-1006,fay@example.com,30.00,paid\n,gus@example.com,12.00,paid\n",
  "rightCsv": "Order ID,Customer Email,Amount,Payment Status\nA-1001,ana@example.com,49.00,paid\nA-1002,ben@example.com,15.50,paid\nA-1003,cy@example.com,120.00,paid\nA-1004,dee@example.com,8.99,paid\nA-1004,dee@example.com,8.99,paid\nA-1005,eli@example.com,64.02,paid\nA-1007,hal@example.com,22.00,paid\n",
  "keyPairs": [
    {
      "left": "order_id",
      "right": "Order ID"
    }
  ],
  "comparePairs": [
    {
      "left": "email",
      "right": "Customer Email"
    },
    {
      "left": "total",
      "right": "Amount"
    },
    {
      "left": "status",
      "right": "Payment Status"
    }
  ],
  "delimiter": ",",
  "keyNormalization": "exact",
  "valueNormalization": "exact",
  "includeMatches": false
}
```

# Actor output Schema

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

No description

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

No description

# 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 = {
    "leftCsv": `order_id,email,total,status
A-1001,ana@example.com,49.00,paid
A-1002,ben@example.com,15.50,paid
A-1003,cy@example.com,120.00,refunded
A-1004,dee@example.com,8.99,paid
A-1005,eli@example.com,64.20,paid
A-1006,fay@example.com,30.00,paid
,gus@example.com,12.00,paid`,
    "rightCsv": `Order ID,Customer Email,Amount,Payment Status
A-1001,ana@example.com,49.00,paid
A-1002,ben@example.com,15.50,paid
A-1003,cy@example.com,120.00,paid
A-1004,dee@example.com,8.99,paid
A-1004,dee@example.com,8.99,paid
A-1005,eli@example.com,64.02,paid
A-1007,hal@example.com,22.00,paid`,
    "keyPairs": [
        {
            "left": "order_id",
            "right": "Order ID"
        }
    ],
    "comparePairs": [
        {
            "left": "email",
            "right": "Customer Email"
        },
        {
            "left": "total",
            "right": "Amount"
        },
        {
            "left": "status",
            "right": "Payment Status"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kestrelwick/csv-export-parity-auditor").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 = {
    "leftCsv": """order_id,email,total,status
A-1001,ana@example.com,49.00,paid
A-1002,ben@example.com,15.50,paid
A-1003,cy@example.com,120.00,refunded
A-1004,dee@example.com,8.99,paid
A-1005,eli@example.com,64.20,paid
A-1006,fay@example.com,30.00,paid
,gus@example.com,12.00,paid
""",
    "rightCsv": """Order ID,Customer Email,Amount,Payment Status
A-1001,ana@example.com,49.00,paid
A-1002,ben@example.com,15.50,paid
A-1003,cy@example.com,120.00,paid
A-1004,dee@example.com,8.99,paid
A-1004,dee@example.com,8.99,paid
A-1005,eli@example.com,64.02,paid
A-1007,hal@example.com,22.00,paid
""",
    "keyPairs": [{
            "left": "order_id",
            "right": "Order ID",
        }],
    "comparePairs": [
        {
            "left": "email",
            "right": "Customer Email",
        },
        {
            "left": "total",
            "right": "Amount",
        },
        {
            "left": "status",
            "right": "Payment Status",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("kestrelwick/csv-export-parity-auditor").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 '{
  "leftCsv": "order_id,email,total,status\\nA-1001,ana@example.com,49.00,paid\\nA-1002,ben@example.com,15.50,paid\\nA-1003,cy@example.com,120.00,refunded\\nA-1004,dee@example.com,8.99,paid\\nA-1005,eli@example.com,64.20,paid\\nA-1006,fay@example.com,30.00,paid\\n,gus@example.com,12.00,paid\\n",
  "rightCsv": "Order ID,Customer Email,Amount,Payment Status\\nA-1001,ana@example.com,49.00,paid\\nA-1002,ben@example.com,15.50,paid\\nA-1003,cy@example.com,120.00,paid\\nA-1004,dee@example.com,8.99,paid\\nA-1004,dee@example.com,8.99,paid\\nA-1005,eli@example.com,64.02,paid\\nA-1007,hal@example.com,22.00,paid\\n",
  "keyPairs": [
    {
      "left": "order_id",
      "right": "Order ID"
    }
  ],
  "comparePairs": [
    {
      "left": "email",
      "right": "Customer Email"
    },
    {
      "left": "total",
      "right": "Amount"
    },
    {
      "left": "status",
      "right": "Payment Status"
    }
  ]
}' |
apify call kestrelwick/csv-export-parity-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kestrelwick/csv-export-parity-auditor"
        }
    }
}
```

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/tUOpEkwEIQwSExrAU/builds/evSz0uFJ2cK2BDffh/openapi.json
