Reconciliation API
Compare two datasets and identify exact and probable matches, unmatched and duplicate records, field conflicts, and evidence-backed financial exposure. Accepts raw JSON — no CRM or ERP integration required.
Endpoint
POST /api/v1/reconcile
Metering is based on records processed — the sum of records in source_a and source_b. A request with 500 records in each source counts as 1,000 records against your monthly allowance. The synchronous maximum is 10,000 records per request.
Request fields
| Field | Type | Description |
|---|---|---|
| source_a* | object | First dataset: { name, records[] }. Records are arbitrary JSON objects. |
| source_b* | object | Second dataset in the same shape as source_a. |
| matching.primary_keys* | string[] | Fields whose normalized composite forms the exact-match key. |
| matching.secondary_keys | string[] | Fallback key fields used to find probable matches when no primary match exists. |
| matching.compare_fields | string[] | Fields compared on matched pairs to detect conflicts. |
| matching.amount_field | string | Numeric field used to compute financial exposure. Auto-detected from compare_fields if omitted. |
| matching.date_fields | string[] | Fields compared as calendar dates. |
| matching.status_fields | string[] | Fields compared as status values. |
Matching & normalization
Matching is deterministic and indexed (approximately O(n)). Values are normalized before comparison: whitespace collapsed, case folded, emails lowercased, and phone numbers reduced to digits (a leading US country code is dropped). Original values are always preserved in the response as evidence and never modified or invented.
A record whose key fields are missing is never given a fabricated key; it is reported as unmatched. When one source record could match several records in the other source, the match is classified needs_review rather than guessed.
Classifications
| Field | Type | Description |
|---|---|---|
| exact_match | class | Matched on primary key with no conflicting compare fields. |
| conflict | class | Matched on primary key, but one or more compare fields differ. |
| probable_match | class | Matched on a secondary key. Includes a confidence score (0-1). |
| duplicate | class | A repeated primary key within a single source. |
| unmatched | class | No match found in the other source. |
| needs_review | class | Ambiguous or low-confidence match requiring human review. |
Financial exposure
Financial exposure is only returned when it is directly supported by real numeric values on both sides of a matched pair. For example, a CRM opportunity of $25,000 matched to a $22,000 invoice yields $3,000 of exposure. Counts are never converted into dollars, and exposure is never fabricated. When no numeric evidence exists, financial_exposure is null. Every exposure item names the exact records and field that produced it.
Errors
Reconciliation uses the same structured error format as the rest of the platform. Oversized requests (more than 10,000 records) return 413 payload_too_large; invalid bodies return 400 invalid_request; and exceeding your monthly record allowance returns 429 usage_limit_exceeded.
Example request
POST /api/v1/reconcile
Authorization: Bearer stx_live_...
Content-Type: application/json
{
"source_a": {
"name": "crm",
"records": [
{ "email": "acme@example.com", "customer": "Acme Co", "amount": 25000, "status": "won" }
]
},
"source_b": {
"name": "billing",
"records": [
{ "email": "acme@example.com", "customer": "Acme Co", "amount": 22000, "status": "paid" }
]
},
"matching": {
"primary_keys": ["email"],
"secondary_keys": ["customer"],
"compare_fields": ["amount", "status"],
"amount_field": "amount"
}
}Example response
{
"request_id": "req_xxx",
"status": "completed",
"summary": {
"source_a_count": 1,
"source_b_count": 1,
"records_processed": 2,
"exact_matches": 0,
"probable_matches": 0,
"conflicts": 1,
"duplicates": 0,
"unmatched_source_a": 0,
"unmatched_source_b": 0,
"needs_review": 0,
"processing_time_ms": 1
},
"matches": [
{
"classification": "conflict",
"confidence": 1,
"matched_on": "primary",
"keys": ["email"],
"key_value": "acme@example.com",
"source_a_index": 0,
"source_b_index": 0,
"field_diffs": [
{ "field": "amount", "type": "amount", "source_a_value": 25000, "source_b_value": 22000, "difference": 3000 },
{ "field": "status", "type": "status", "source_a_value": "won", "source_b_value": "paid" }
]
}
],
"findings": [
{ "type": "amount", "classification": "conflict", "field": "amount", "message": "amount differs on key \"acme@example.com\": 25000 vs 22000." }
],
"financial_exposure": {
"field": "amount",
"currency": null,
"net_difference": 3000,
"absolute_exposure": 3000,
"items": [
{ "field": "amount", "key_value": "acme@example.com", "source_a_value": 25000, "source_b_value": 22000, "difference": 3000, "source_a_index": 0, "source_b_index": 0 }
]
},
"usage": { "records_processed": 2 }
}Code examples
curl -X POST https://crossstax.dev/api/v1/reconcile \
-H "Authorization: Bearer $CROSSSTAX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_a": { "name": "orders", "records": [{ "order_id": "ORD-1", "total": 149.99 }] },
"source_b": { "name": "payments", "records": [{ "order_id": "ORD-1", "total": 149.99 }] },
"matching": { "primary_keys": ["order_id"], "compare_fields": ["total"], "amount_field": "total" }
}'import { CrossStax } from "@crossstax/sdk"
const crossstax = new CrossStax({ apiKey: process.env.CROSSSTAX_API_KEY! })
const result = await crossstax.data.reconcile({
source_a: { name: "crm", records: crmRows },
source_b: { name: "billing", records: invoiceRows },
matching: {
primary_keys: ["email"],
secondary_keys: ["customer"],
compare_fields: ["amount", "status"],
amount_field: "amount",
},
})
console.log(result.summary, result.financial_exposure)import os, requests
resp = requests.post(
"https://crossstax.dev/api/v1/reconcile",
headers={"Authorization": f"Bearer {os.environ['CROSSSTAX_API_KEY']}"},
json={
"source_a": {"name": "crm", "records": crm_rows},
"source_b": {"name": "billing", "records": invoice_rows},
"matching": {
"primary_keys": ["email"],
"secondary_keys": ["customer"],
"compare_fields": ["amount", "status"],
"amount_field": "amount",
},
},
)
data = resp.json()
print(data["summary"], data["financial_exposure"])