Skip to main content
Identity data is only as trustworthy as the guarantees that it has not been altered after the fact. Tally builds those guarantees into every snapshot using cryptographic hashing and a tamper-evident hash chain. Any modification to any historical snapshot — even changing a single character in an attribute value — produces a hash mismatch that you can detect by re-computing hashes yourself. No trust in Tally’s infrastructure is required to verify your data.

How hashing works

When Tally stores a snapshot, it computes a deterministic hash of the envelope using a two-step process based on the RFC 8785 JSON Canonicalization Scheme and SHA-256: Step 1 — RFC 8785 canonicalization. The envelope JSON is serialized using the JSON Canonicalization Scheme (JCS, RFC 8785). JCS produces a byte-for-byte identical UTF-8 serialization regardless of the original key ordering, whitespace, or number formatting in the source JSON. This eliminates any ambiguity about what bytes are being hashed. Step 2 — SHA-256. The SHA-256 digest is computed over the UTF-8 bytes of the JCS-serialized envelope. The output is encoded as a lowercase hexadecimal string. This value is the envelope_hash.
envelope_hash = hex( SHA-256( JCS( envelope_without_integrity ) ) )
The integrity block itself — if present — is excluded from the hash input so that embedding the hash does not create a circular dependency.
The hash is computed solely from the envelope JSON: envelope_version, snapshot_id, snapshot_version, generated_at, subject, attributes, evidence, and attribute_paths (if present). Fields like audit and diff are excluded from the canonical snapshot object. Tenant IDs, grant records, and all other authorization metadata are never part of the hash input — authorization state changes never invalidate snapshot hashes.

The hash chain

Each snapshot v2 and later stores the envelope_hash of its immediate predecessor in the integrity.prev_envelope_hash field. This creates a tamper-evident chain:
Snapshot v1  →  envelope_hash: "a1b2c3..."
Snapshot v2  →  prev_envelope_hash: "a1b2c3..."  →  envelope_hash: "d4e5f6..."
Snapshot v3  →  prev_envelope_hash: "d4e5f6..."  →  envelope_hash: "7g8h9i..."
If an attacker modifies snapshot v1 — changing, say, an ownership percentage — the envelope_hash of v1 changes. But snapshot v2’s prev_envelope_hash still points to the original hash. Recomputing v2’s chain verification immediately reveals the mismatch. An optional chain_hash strengthens this further:
chain_hash = hex( SHA-256( prev_envelope_hash + "\n" + envelope_hash ) )
where the two hex strings are concatenated with a single LF byte (\n) between them.

Verification endpoints

Tally exposes dedicated endpoints for hash proof retrieval and verification.

Single snapshot proof

GET /v1/snapshots/:snapshot_id/proof
Returns the integrity proof for one snapshot.
{
  "snapshot_id": "3f0b2c2c-2e46-4b58-8c45-3b5c58f4e9b2",
  "envelope_hash": "d4e5f6a7b8c9...",
  "prev_hash": "a1b2c3d4e5f6...",
  "canonicalization_method": "rfc8785",
  "hash_algorithm": "sha-256"
}

Full chain proof

GET /v1/subjects/:subject_type/:subject_id/chain-proof
Returns proof records for every snapshot in the subject’s history, ordered from root to latest, enabling full chain verification.
All snapshot read endpoints also accept a verify query parameter to trigger inline verification:
ValueBehavior
none (default)Returns the snapshot data without any verification overhead
hashTally re-computes the envelope hash server-side and returns a verification block confirming whether the stored hash matches
chainTally walks the full chain from the root snapshot and returns the chain validity result alongside the hash verification
Example using verify=chain:
curl "https://api.tally.so/v1/subjects/entity/ent_acme_001/snapshots/latest?verify=chain" \
  -H "Authorization: Bearer <token>"
{
  "envelope": { "...": "..." },
  "verification": {
    "mode": "chain",
    "chain_supported": true,
    "hash": {
      "alg": "sha-256",
      "value": "7g8h9i0j1k2l..."
    },
    "chain": {
      "prev_hash": "d4e5f6a7b8c9...",
      "valid": true
    }
  }
}
chain.valid is:
  • trueprev_hash matches the predecessor’s envelope_hash
  • false — both hashes exist but do not match (data may have been tampered with)
  • null — the predecessor hash or prev_hash is absent (chain verification is inconclusive for this snapshot)
Root snapshots (v1, with no predecessor) always return chain.valid: true with prev_hash: null.

Export and offline verification

For complete offline auditability, export all snapshots for a subject:
curl "https://api.tally.so/v1/subjects/entity/ent_acme_001/export" \
  -H "Authorization: Bearer <token>" \
  -o export.json
The export file contains every snapshot envelope plus its stored envelope_hash and prev_hash. Use the tally CLI to re-verify all hashes locally without making any further API calls:
tally verify-ledger export.json
Verifying ent_acme_001 (3 snapshots)...
  v1  3f0b2c2c  ✓ hash OK   (root, no prev)
  v2  9c7d1e3f  ✓ hash OK   ✓ chain OK
  v3  a8b4c2d1  ✓ hash OK   ✓ chain OK

All 3 snapshots verified. Chain is intact.
The CLI re-computes SHA-256(JCS(envelope)) for every snapshot from scratch and checks that each prev_hash matches the preceding snapshot’s computed hash. No stored values are trusted — every hash is independently re-derived.
Include the exported file in your compliance archive alongside the CLI verification output. This gives auditors a self-contained proof that the identity data they are reviewing has not changed since it was written, with no dependency on Tally’s live systems.

Verify modes reference

ModeAPI parameterUse case
none?verify=none or omitNormal reads where integrity is not the focus — fastest response
hash?verify=hashSpot-check that a single snapshot’s stored hash is correct
chain?verify=chainFull tamper-evidence check across the snapshot lineage
verify=chain triggers a walk of the full snapshot history for the subject and is more expensive than verify=hash. For high-throughput read paths, prefer verify=none and run periodic chain verification offline using the export + CLI workflow.