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

FieldTypeDescription
source_a*objectFirst dataset: { name, records[] }. Records are arbitrary JSON objects.
source_b*objectSecond dataset in the same shape as source_a.
matching.primary_keys*string[]Fields whose normalized composite forms the exact-match key.
matching.secondary_keysstring[]Fallback key fields used to find probable matches when no primary match exists.
matching.compare_fieldsstring[]Fields compared on matched pairs to detect conflicts.
matching.amount_fieldstringNumeric field used to compute financial exposure. Auto-detected from compare_fields if omitted.
matching.date_fieldsstring[]Fields compared as calendar dates.
matching.status_fieldsstring[]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

FieldTypeDescription
exact_matchclassMatched on primary key with no conflicting compare fields.
conflictclassMatched on primary key, but one or more compare fields differ.
probable_matchclassMatched on a secondary key. Includes a confidence score (0-1).
duplicateclassA repeated primary key within a single source.
unmatchedclassNo match found in the other source.
needs_reviewclassAmbiguous 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

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

200 OK
{
  "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
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" }
  }'
JavaScript / TypeScript
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)
Python
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"])