Evidence Packs & Verification

What HDEP evidence packs contain, how content hashing works, chain continuity between versions, and what "tamper-evident" means in practice.

7 min read Evidence & Verification As of Feb 9, 2026

CommunityPay generates institutional evidence packs — structured documents that prove what an HOA's financial and governance posture looked like at a specific point in time. This page explains what they contain, how they're verified, and why the verification model matters.

What Is an Evidence Pack?

An evidence pack is a frozen snapshot of an HOA's financial, governance, and compliance data, generated at a specific timestamp and bound to a cryptographic hash.

CommunityPay supports five pack types:

Type Name Scope
HDEP HOA Disclosure & Evidence Packet Per unit
GCA Governance Controls Attestation Per HOA/period
FADR Funds Authorization & Disbursement Record Per HOA/period
RC Resale Certificate Per unit
RSR Reserve Study Report Per HOA/period

Each pack type answers a different question. An HDEP answers "what is the complete financial and governance posture of this HOA, as it relates to this specific unit?" A GCA answers "what controls were active and what did they find during this period?"

Anatomy of a Pack

Every evidence pack contains:

Identification

  • Public ID: A UUID for external API references. Immutable after creation.
  • Reference Number: A human-readable identifier auto-generated at creation.
  • Version Number: Increments with each regeneration for the same HOA/type/scope.

Data Snapshot

  • evidence_snapshot: A JSON document containing all relevant financial and governance data, frozen at generation time. This is the canonical source of truth — not the PDF.
  • as_of_at: The timestamp of the underlying data. This is when the data was captured, not when the pack was generated.
  • generated_at: When the pack was actually generated. These two timestamps are distinct because you might regenerate a pack from the same data snapshot.

Proof References

The evidence snapshot includes references to the governance artifacts that produced it:

  • policy_snapshot_ids: Which policy versions were active when the pack was generated
  • evaluation_trace_ids: Which rule evaluations were performed
  • enforcement_decision_ids: Which enforcement decisions were rendered

This creates a traceable chain from the pack back to the specific policy versions, rule evaluations, and decisions that produced the underlying data.

Verification

  • content_hash: SHA-256 hash of the canonical JSON evidence snapshot.
  • previous_packet_hash: Content hash of the previous version, creating a chain.

Lifecycle

  • Status: draft, generated, delivered, superseded, void
  • Sharing: Optional time-limited external sharing via secure token

Content Hashing

The content hash is the core verification mechanism. Here is how it works:

Canonical JSON

Before hashing, the evidence snapshot is serialized into canonical JSON:

  • Keys are sorted alphabetically
  • Separators are minimal (no extra whitespace): (",", ":")
  • Encoding is UTF-8

This produces a deterministic byte sequence. The same data will always produce the same bytes, regardless of when or where the serialization happens.

SHA-256

The canonical bytes are hashed with SHA-256, producing a 64-character hexadecimal string. This hash is stored in the content_hash field.

What This Proves

If you have the evidence snapshot and the content hash, you can independently verify the pack:

  1. Serialize the evidence_snapshot to canonical JSON (sorted keys, minimal separators, UTF-8)
  2. Compute SHA-256 of the resulting bytes
  3. Compare with the stored content_hash

If they match, the evidence snapshot has not been modified since generation. If they don't match, the data has been tampered with.

Important: The hash is computed from the JSON evidence snapshot, not from the PDF. PDFs are rendered from the snapshot, but the snapshot is the authoritative data. Two PDF renderings of the same snapshot might differ in formatting, but the content hash will be identical.

Chain Continuity

Each pack version links to its predecessor via the previous_packet_hash field.

How Chaining Works

When a new version of a pack is generated for the same HOA/type/scope:

  1. The system looks up the current pack (the one being superseded)
  2. It copies the superseded pack's content_hash into the new pack's previous_packet_hash
  3. The new pack's own content_hash is computed from its new evidence snapshot

This creates a hash chain:

Pack v1:
  content_hash = sha256(snapshot_v1)
  previous_packet_hash = (empty, first version)

Pack v2:
  content_hash = sha256(snapshot_v2)
  previous_packet_hash = sha256(snapshot_v1)  ← links to v1

Pack v3:
  content_hash = sha256(snapshot_v3)
  previous_packet_hash = sha256(snapshot_v2)  ← links to v2

What Chain Continuity Proves

If you have consecutive packs in the chain, you can verify:

  1. This version hasn't been tampered with (content_hash matches snapshot)
  2. The previous version existed and had specific content (previous_packet_hash matches the predecessor's content_hash)
  3. Missing links are detectable when comparing expected continuity between consecutive versions

The chain is tamper-evident: if a party presents consecutive versions, links can be verified. An auditor or escrow company receiving Pack v3 can verify the entire history back to v1, provided earlier versions are available. No access to CommunityPay is required for this verification.

Immutability Enforcement

Evidence packs use a strict mutable/immutable field model.

Immutable Fields (Cannot Be Modified After Creation)

  • content_hash
  • previous_packet_hash
  • evidence_snapshot
  • public_id
  • reference_number
  • packet_type
  • version
  • scope
  • as_of_at
  • created_by
  • hoa
  • created_at

Mutable Fields (Can Be Updated)

  • status (e.g., draft to generated to delivered)
  • pdf_file (PDF can be re-rendered from immutable snapshot)
  • is_shareable, share_token, share_expires_at
  • delivered_at, voided_at, void_reason
  • notes

How Immutability Is Enforced

When saving a pack, the system checks which fields are being updated. If any update targets an immutable field, a ValueError is raised and the save is rejected:

immutable_updates = set(update_fields) - MUTABLE_FIELDS
if immutable_updates:
    raise ValueError(f"Cannot update immutable fields: {immutable_updates}")

This runs at the application layer, not just the database layer. You cannot bypass it through the Django admin, management commands, or bulk updates.

Lifecycle Tracking

Every significant event in a pack's lifecycle is recorded in an append-only event log.

Event Types

Event When It's Recorded
created Pack record first saved
generated Evidence assembled and hash computed
delivered Pack sent to recipient
shared External share link activated
accessed Someone uses the share link
downloaded Recipient downloads the PDF
voided Pack marked as void
superseded Newer version generated
share_revoked Sharing disabled

What Events Capture

Each event records:

  • Actor: Which user performed the action
  • Actor name: Denormalized so the record persists even if the user is later deleted
  • Detail: Human-readable description
  • IP address: Network origin of the action
  • Timestamp: When it happened

Events are append-only. They cannot be updated or deleted. This creates a complete audit trail of every pack from creation through delivery.

Practical Verification

Independent verification does not require credentials, API calls, or trust in CommunityPay. Any party with the evidence data can verify integrity.

For an external party (auditor, escrow company, prospective buyer) to verify a pack:

  1. Obtain the pack's evidence_snapshot and content_hash (included in the pack itself)
  2. Compute: sha256(canonical_json(evidence_snapshot))
  3. Compare: If it matches content_hash, the data is intact
  4. Optionally verify the chain: Check previous_packet_hash against the predecessor's content_hash

Verification Recipe

import json, hashlib

# 1. Load the evidence_snapshot from the pack
snapshot = pack["evidence_snapshot"]

# 2. Serialize to canonical JSON
canonical = json.dumps(snapshot, sort_keys=True, separators=(",", ":")).encode("utf-8")

# 3. Compute SHA-256
computed_hash = hashlib.sha256(canonical).hexdigest()

# 4. Compare to stored content_hash
assert computed_hash == pack["content_hash"], "Hash mismatch — data has been modified"

This is what "tamper-evident" means in practice: not that tampering is impossible, but that tampering is detectable by anyone with the data.

CARI Integration

CARI reports — including Lender, Insurer, Title, and Buyer variants — are institutional packets built on the same evidence infrastructure described here. Each CARI report carries a SHA-256 content hash, version chain linking via previous_packet_hash, and an append-only event log, identical to HDEP, GCA, FADR, and VECR artifacts. CARI reports extend the evidence pack ecosystem to external institutional consumers, enabling lenders, insurers, and title companies to independently verify the integrity of the data they receive.

For published methodology and component weights, see CARI Methodology and Scoring Framework.

How CommunityPay Enforces This
  • SHA-256 content hash computed from canonical JSON (not PDF bytes)
  • Previous-packet hash creates tamper-evident chain between versions
  • Immutability enforced: evidence_snapshot, content_hash, and proof references cannot be modified after creation
  • Packet lifecycle tracked via append-only event log
  • Proof references link packets to the policy snapshots, evaluation traces, and enforcement decisions that produced them
Login