Skip to content

Entity History & Audit

Every change to a canonical (golden) entity is recorded as an audit event. You can view the full history of an entity, lock it to prevent automatic changes, revert it to a previous state, and configure temporal matching to handle time-varying source records.

Audit Events

When an entity is created, merged, split, or updated, the system records an AuditEvent:

FieldTypeDescription
idUUIDEvent ID
tenant_idUUIDTenant this event belongs to
actor_idUUIDWho made the change (user ID or system ID)
actor_typestring"user", "system", or "api_key"
actionstringWhat happened: "create", "merge", "update", "revert", "lock", "unlock"
resource_typestringType of resource changed (e.g., "canonical_entity")
resource_idUUIDID of the entity that changed
before_stateJSONEntity data before the change (null for create)
after_stateJSONEntity data after the change
reasonstringOptional human-readable reason
timestampdatetimeWhen the change occurred (UTC)

The before_state and after_state fields capture the full canonical_data JSONB at the time of the event. This gives you a complete snapshot — not just what fields changed, but what the entire record looked like before and after.

Viewing Entity History

Retrieve the audit trail for any canonical entity:

python
import httpx

entity_id = "550e8400-e29b-41d4-a716-446655440000"

resp = httpx.get(f"https://api.kanoniv.com/v1/canonical/{entity_id}/history",
    headers={"X-API-Key": "kn_..."})
events = resp.json()

for event in events:
    print(f"[{event['timestamp']}] {event['action']} by {event['actor_type']}")
    if event.get("reason"):
        print(f"  Reason: {event['reason']}")
bash
curl "https://api.kanoniv.com/v1/canonical/550e8400-e29b-41d4-a716-446655440000/history" \
  -H "X-API-Key: kn_..."

Example response:

json
[
  {
    "id": "a1b2c3d4-...",
    "action": "merge",
    "actor_type": "system",
    "before_state": {"name": "John Doe", "email": "[email protected]"},
    "after_state": {"name": "John Doe", "email": "[email protected]", "phone": "+1-555-0123"},
    "timestamp": "2026-02-10T14:30:00Z"
  },
  {
    "id": "e5f6a7b8-...",
    "action": "create",
    "actor_type": "system",
    "before_state": null,
    "after_state": {"name": "John Doe", "email": "[email protected]"},
    "timestamp": "2026-02-08T09:15:00Z"
  }
]

Events are returned in reverse chronological order (newest first).

Entity Locking

Lock an entity to prevent automatic merges and updates. Locked entities are skipped during reconciliation and queued for manual review instead.

python
# Lock an entity
httpx.post(f"https://api.kanoniv.com/v1/canonical/{entity_id}/lock",
    headers={"X-API-Key": "kn_...", "Content-Type": "application/json"},
    json={"locked": True})

# Unlock it later
httpx.post(f"https://api.kanoniv.com/v1/canonical/{entity_id}/lock",
    headers={"X-API-Key": "kn_...", "Content-Type": "application/json"},
    json={"locked": False})
bash
# Lock
curl -X POST "https://api.kanoniv.com/v1/canonical/550e8400-.../lock" \
  -H "X-API-Key: kn_..." \
  -H "Content-Type: application/json" \
  -d '{"locked": true}'

# Unlock
curl -X POST "https://api.kanoniv.com/v1/canonical/550e8400-.../lock" \
  -H "X-API-Key: kn_..." \
  -H "Content-Type: application/json" \
  -d '{"locked": false}'

The is_locked flag on CanonicalEntity controls this behavior. When is_locked is true:

  • Reconciliation will not modify the entity's canonical_data
  • New matching records that would merge into this entity are held for review
  • Manual overrides and explicit API updates still work

Reverting to a Historical State

If a merge or update produced bad results, you can revert an entity to the state it was in at any previous audit event.

Step-by-Step

  1. Get the entity history to find the event you want to revert to:
python
resp = httpx.get(f"https://api.kanoniv.com/v1/canonical/{entity_id}/history",
    headers={"X-API-Key": "kn_..."})
events = resp.json()

# Find the event before the bad merge
for event in events:
    print(f"{event['id']}: [{event['timestamp']}] {event['action']}")
  1. Choose the target event — the revert restores the after_state of the selected event:
a1b2c3d4-...: [2026-02-10T14:30:00Z] merge    ← the bad merge
e5f6a7b8-...: [2026-02-08T09:15:00Z] create   ← revert to this
  1. Revert:
python
target_event_id = "e5f6a7b8-..."

resp = httpx.post(
    f"https://api.kanoniv.com/v1/canonical/{entity_id}/revert/{target_event_id}",
    headers={"X-API-Key": "kn_..."})

restored = resp.json()
print(f"Restored to: {restored['canonical_data']}")
bash
curl -X POST \
  "https://api.kanoniv.com/v1/canonical/550e8400-.../revert/e5f6a7b8-..." \
  -H "X-API-Key: kn_..."

The revert updates the entity's canonical_data to the after_state of the selected event. A new audit event is created recording the revert itself.

Lock After Reverting

After reverting, consider locking the entity to prevent the same bad merge from happening again on the next reconciliation run. Fix the underlying rule or threshold first, then unlock.

Temporal Matching

Source records can have time-validity windows via valid_from and valid_to fields. The temporal matching strategy controls how the engine handles these windows during reconciliation.

Configuration

Define temporal fields on a source in your spec YAML:

yaml
sources:
  - name: crm
    adapter: postgres
    location: customers
    primary_key: id
    attributes:
      name: name
      email: email
    temporal:
      valid_from_field: effective_date
      valid_to_field: expiry_date
      strategy: point_in_time

Strategies

StrategyBehaviorUse Case
latest_onlyOnly the most recent record per entity is consideredDefault. Good when you only care about current state
point_in_timeRecords are matched based on overlapping validity windowsRegulatory reporting, historical audits
bi_temporalBoth transaction time and valid time are trackedFull audit trail with corrections — know what was known at any point

How It Affects Matching

With latest_only (the default), each entity contributes one record to matching. With point_in_time, a single entity may have multiple valid records at different times — the engine matches records whose validity windows overlap.

Example: a customer record that was valid from Jan 1 to Mar 31 will only match against other records that overlap that window. A record valid from Apr 1 onward would be treated as a separate observation.

bi_temporal adds a second time axis — when the record was entered into the system. This lets you answer questions like "what did we know about this customer as of February 1st?" even if the record was later corrected.

Full Workflow: Investigating a Bad Merge

Here's a complete example: discovering a bad merge, investigating it, reverting, and preventing recurrence.

python
import httpx

API = "https://api.kanoniv.com"
HEADERS = {"X-API-Key": "kn_...", "Content-Type": "application/json"}

entity_id = "550e8400-e29b-41d4-a716-446655440000"

# 1. Get the entity and notice something is wrong
entity = httpx.get(f"{API}/v1/canonical/{entity_id}",
    headers=HEADERS).json()
print(f"Current: {entity['canonical_data']}")
# Shows merged data from two unrelated people

# 2. View history to understand what happened
events = httpx.get(f"{API}/v1/canonical/{entity_id}/history",
    headers=HEADERS).json()

for e in events:
    print(f"[{e['timestamp']}] {e['action']}")
    if e['before_state']:
        print(f"  Before: {e['before_state']}")
    print(f"  After:  {e['after_state']}")

# 3. Lock the entity to prevent further damage
httpx.post(f"{API}/v1/canonical/{entity_id}/lock",
    headers=HEADERS, json={"locked": True})

# 4. Revert to the state before the bad merge
good_event_id = events[1]["id"]  # the event before the bad merge
restored = httpx.post(
    f"{API}/v1/canonical/{entity_id}/revert/{good_event_id}",
    headers=HEADERS).json()
print(f"Restored: {restored['canonical_data']}")

# 5. Investigate: was the matching rule too aggressive?
#    (Fix the rule/threshold, then unlock)

See Also

The identity and delegation layer for AI agents.