Skip to content

Customer 360

Unify a single customer identity ("John Doe") across 4 source systems with email and name variations into one golden record.

The Problem

John Doe is a real customer. He exists in your Stripe billing system, Salesforce CRM, HubSpot marketing platform, and an internal PostgreSQL database. Each system captured his information at different times, through different forms, with different data quality standards. The result: four records that all represent the same person but look different enough to defeat naive deduplication.

Your sales team sends him duplicate outreach. Your billing system cannot reconcile his subscription with his CRM record. Your support team creates a new ticket profile every time he contacts a different channel.

The Data

Stripe (Billing)

FieldValue
Sourcestripe
IDcus_N8x4kLm2pQ
NameJohn Doe
Email[email protected]
Phone+1-555-0142
Companynull
Last Updated2026-01-18

Salesforce (CRM)

FieldValue
Sourcesalesforce
ID003Dn00000Lx8FQIAZ
NameJonathan Doe
Email[email protected]
Phone+1-555-0142
CompanyAcme Corporation
Last Updated2026-01-10

HubSpot (Marketing)

FieldValue
Sourcehubspot
IDhub_29847163
NameJ. Doe
Email[email protected]
Phonenull
CompanyAcme Corp
Last Updated2025-12-22

Internal Database

FieldValue
Sourceinternal
IDusr_00419
NameJohn D.
Email[email protected]
Phone+1-555-0142
Companynull
Last Updated2025-11-30

Summary View

SourceIDNameEmailPhoneCompanyLast Updated
Stripecus_N8x4kLm2pQJohn Doe[email protected]+1-555-0142null2026-01-18
Salesforce003Dn00000Lx8FQIAZJonathan Doe[email protected]+1-555-0142Acme Corporation2026-01-10
HubSpothub_29847163J. Doe[email protected]nullAcme Corp2025-12-22
Internalusr_00419John D.[email protected]+1-555-0142null2025-11-30

Challenges in this data:

  • Name variations: "John Doe", "Jonathan Doe", "J. Doe", "John D.", four representations of the same person
  • Email variations: [email protected], [email protected], [email protected], three different email addresses
  • Missing fields: HubSpot has no phone; Stripe and Internal have no company
  • Company variations: "Acme Corporation" vs "Acme Corp" vs null

No single field uniquely identifies this customer across all four systems. Identity resolution requires combining multiple signals.

The Spec

yaml
api_version: kanoniv/v1
identity_version: "1.0"

entity:
  name: customer
  description: "Unified customer identity across billing, CRM, marketing, and internal systems"

sources:
  - name: salesforce
    adapter: csv
    location: data/salesforce_contacts.csv
    primary_key: contact_id
    attributes:
      name: name
      email: email
      phone: phone
      company: company
      last_updated: last_updated

  - name: stripe
    adapter: csv
    location: data/stripe_customers.csv
    primary_key: customer_id
    attributes:
      name: name
      email: email
      phone: phone
      company: company
      last_updated: last_updated

  - name: hubspot
    adapter: csv
    location: data/hubspot_contacts.csv
    primary_key: hub_id
    attributes:
      name: name
      email: email
      phone: phone
      company: company
      last_updated: last_updated

  - name: internal
    adapter: csv
    location: data/internal_users.csv
    primary_key: user_id
    attributes:
      name: name
      email: email
      phone: phone
      company: company
      last_updated: last_updated

rules:
  # Email is the strongest signal. Even though John has 3 different
  # email addresses, two systems share [email protected] -- that alone
  # is enough to link Stripe and HubSpot.
  - name: email_exact
    type: exact
    field: email
    weight: 1.0

  # Fuzzy name matching catches the variations. "John Doe" vs
  # "Jonathan Doe" scores ~0.87 in Jaro-Winkler (prefix "Jo" helps).
  # "J. Doe" vs "John Doe" scores ~0.82 -- above our 0.85 threshold
  # when combined with the shared prefix weighting.
  - name: name_fuzzy
    type: similarity
    field: name
    algorithm: jaro_winkler
    threshold: 0.85
    weight: 0.6

  # Phone is a strong corroborating signal. Three of four systems
  # share +1-555-0142. This bridges records that don't share an email.
  - name: phone_exact
    type: exact
    field: phone
    weight: 0.8

blocking:
  strategy: exact
  keys: [email, phone]

decision:
  scoring: weighted_sum
  thresholds:
    match: 0.85
    review: 0.6

survivorship:
  default: source_priority
  overrides:
    - field: email
      strategy: most_recent
    - field: last_updated
      strategy: aggregate
      function: max

metadata:
  owner: [email protected]
  tags: [customer-360, production]
  description: "Customer 360 resolution across 4 source systems"

Why These Rules Work

Record Pairemail_exactname_fuzzyphone_exactTotal ScoreDecision
Stripe / HubSpot1.0 ([email protected])0.6 (John Doe vs J. Doe)0.0 (null phone)1.6Match
Stripe / Salesforce0.0 (different emails)0.6 (John Doe vs Jonathan Doe)0.8 (+1-555-0142)1.4Match
Stripe / Internal0.0 (different emails)0.6 (John Doe vs John D.)0.8 (+1-555-0142)1.4Match
Salesforce / HubSpot0.0 (different emails)0.6 (Jonathan Doe vs J. Doe)0.0 (null phone)0.6Review
Salesforce / Internal0.0 (different emails)0.6 (Jonathan Doe vs John D.)0.8 (+1-555-0142)1.4Match
HubSpot / Internal0.0 (different emails)0.6 (J. Doe vs John D.)0.0 (null phone)0.6Review

All six pairs score above the review threshold. Five of six score above the match threshold. The engine clusters all four records into a single canonical entity through transitive closure, even though no single rule connects all four directly.

The Result

Golden Record

After survivorship resolution, the canonical entity contains:

FieldValueSourceReason
NameJonathan DoeSalesforcesource_priority: Salesforce is first in source order
Email[email protected]Stripemost_recent override: Stripe updated Jan 18 (most recent)
Phone+1-555-0142Salesforcesource_priority: Salesforce is first and non-null
CompanyAcme CorporationSalesforcesource_priority: Salesforce is first and non-null
Last Updated2026-01-18Stripeaggregate(max): latest timestamp across all sources

Cluster Visualization

Canonical Entity: canon_8f3a
├── salesforce/003Dn00000Lx8FQIAZ  (Jonathan Doe, [email protected])
│     ↕ phone_exact (0.8) + name_fuzzy (0.6)
├── stripe/cus_N8x4kLm2pQ          (John Doe, [email protected])
│     ↕ email_exact (1.0) + name_fuzzy (0.6)
├── hubspot/hub_29847163            (J. Doe, [email protected])
│     ↕ phone_exact (0.8) + name_fuzzy (0.6)
└── internal/usr_00419              (John D., [email protected])

Field Provenance

The API response includes field_provenance showing exactly which source contributed each field to the golden record:

json
{
  "canonical_id": "canon_8f3a",
  "entity_type": "customer",
  "canonical_data": {
    "name": "Jonathan Doe",
    "email": "[email protected]",
    "phone": "+1-555-0142",
    "company": "Acme Corporation",
    "last_updated": "2026-01-18"
  },
  "field_provenance": {
    "name": { "source": "salesforce", "record_id": "003Dn00000Lx8FQIAZ", "strategy": "source_priority" },
    "email": { "source": "stripe", "record_id": "cus_N8x4kLm2pQ", "strategy": "most_recent" },
    "phone": { "source": "salesforce", "record_id": "003Dn00000Lx8FQIAZ", "strategy": "source_priority" },
    "company": { "source": "salesforce", "record_id": "003Dn00000Lx8FQIAZ", "strategy": "source_priority" },
    "last_updated": { "source": "stripe", "record_id": "cus_N8x4kLm2pQ", "strategy": "aggregate_max" }
  },
  "cluster": {
    "size": 4,
    "sources": ["stripe", "salesforce", "hubspot", "internal"],
    "confidence": 0.94
  }
}

Reconciliation Summary

Sources:           4
Total records:     4
Clusters:          1
Golden records:    1
Merge rate:        75.0% (4 records → 1 golden record)
Match pairs:       5
Review pairs:      1
Rejected pairs:    0

Key Takeaway

Source priority survivorship ensures your most trusted system wins. By declaring Salesforce first in the sources list, CRM data takes precedence for name and company fields, even though Stripe has newer data. The most_recent override on the email field ensures the freshest contact information is preserved regardless of source rank.

This pattern is the foundation for most Customer 360 implementations: pick a system of record for identity fields, but allow per-field overrides for data that changes frequently.

Variations to Consider

If you trust the most recent data overall, replace source_priority with most_recent:

yaml
survivorship:
  default: most_recent

If name matching produces false positives (common with short names), raise the threshold:

yaml
- name: name_fuzzy
  type: similarity
  field: name
  algorithm: jaro_winkler
  threshold: 0.92    # stricter threshold
  weight: 0.6

If you want company name to corroborate matches, add a fuzzy company rule:

yaml
- name: company_fuzzy
  type: similarity
  field: company
  algorithm: jaro_winkler
  threshold: 0.90
  weight: 0.3

The identity and delegation layer for AI agents.