Skip to main content

How It Works

Every event in Immutable is linked to the previous event in a per-workspace SHA-256 hash chain. This creates a tamper-evident seal across your entire audit trail — if any event is modified, inserted, or deleted at the database level, the chain breaks and the tampering is detectable.
Event 1              Event 2              Event 3
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│ data        │      │ data        │      │ data        │
│ prev: null  │─────>│ prev: h1    │─────>│ prev: h2    │
│ hash: h1    │      │ hash: h2    │      │ hash: h3    │
└─────────────┘      └─────────────┘      └─────────────┘
The first event in a workspace has previous_event_hash set to null.

Hash Computation

Each event hash is a SHA-256 digest computed from all event fields:
Field GroupFields
Identityworkspace_id
Actoractor_id, actor_name, actor_type
Actionaction, action_category
Resourceresource_id, resource_name, resource
Contextmetadata, targets, occurred_at
Networkip_address, ip_country, ip_city, user_agent
Trackingtenant_id, session_id, idempotency_key, version
Chainprevious_event_hash
Metadata keys are recursively sorted and targets are sorted by type then id before hashing. This ensures deterministic hashes regardless of JSON key order or array ordering in the database.

Chain Ordering

Events are chained by created_at (server-side timestamp with microsecond precision), not occurred_at (client-provided time). This guarantees a consistent, linear chain even when clients report out-of-order timestamps.

Concurrency Safety

Immutable uses PostgreSQL advisory locks (pg_advisory_xact_lock scoped by workspace ID via crc32) to ensure concurrent event ingestion does not produce conflicting hash chains. Only one event per workspace is hashed at a time.

Integrity Block

Every event returned by the API includes an integrity block:
{
  "integrity": {
    "event_hash": "a3f2c8d1e4b567890abcdef1234567890abcdef1234567890abcdef12345678",
    "previous_event_hash": "7b9e1d3f5a2c67890abcdef1234567890abcdef1234567890abcdef12345678"
  }
}

Verifying the Chain

Use the verify endpoint to validate the entire chain or a date range:
curl https://getimmutable.dev/api/v1/verify \
  -H "Authorization: Bearer imk_your_api_key_here"
{
  "data": {
    "valid": true,
    "events_checked": 4821,
    "breaks": []
  }
}
See Hash Verification for programmatic monitoring.

Break Types

If tampering is detected, the breaks array describes each issue:
TypeDescription
hash_mismatchThe stored hash does not match the recomputed hash. The event data was modified.
chain_breakThe previous_event_hash does not match the prior event’s hash. An event was inserted or deleted.
{
  "data": {
    "valid": false,
    "events_checked": 4821,
    "breaks": [
      {
        "event_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "type": "hash_mismatch",
        "expected_hash": "a3f2c8d1...",
        "actual_hash": "ff00ab12..."
      }
    ]
  }
}
Legacy events ingested before hash chain activation have nullable hash fields. The verify endpoint skips these events and does not report them as breaks.

Batch Event Ordering

When events are ingested via the batch endpoint, each event’s created_at is offset by 1 microsecond to preserve insertion order. Without this offset, batch events would share the same created_at value and UUID-based ordering would not match insertion order, causing false chain_break errors during verification.