Evidence Packs & Verification

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

9 min read Evidence & Verification Published Feb 4, 2025 Updated Apr 11, 2026

CommunityPay generates institutional evidence packs — structured documents that prove what an association'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 10 pack types:

Type Name
HDEP HOA Disclosure & Evidence Packet
GCA Governance Controls Attestation
FADR Funds Authorization & Disbursement Record
RC Resale Certificate
RSR Reserve Funding Status Report
LDS Loan Disclosure Statement
CEP Close Evidence Pack
BP Board Packet
FSR Fiduciary Shield Report
CMR CARI Market Report

A pack is versioned per association, type, and scope. A packet for one unit is scoped to that unit, and a packet for a period is scoped to that 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.

Provenance

Packets built by the shared attestation generator carry a _meta block with the schema version, the generator version, the time it was generated, the as-of date, and the hash algorithm. The block is inside the snapshot, so the content hash covers it. The disclosure evidence packet states its schema version at the top of its snapshot instead, and the board packet hashes its manifest, which has no such block. A snapshot reports the governance facts behind the thing it documents: a FADR, for example, records the approval chain and the policy snapshot behind a payment, as described in Disbursement Authorization Controls.

Verification

  • content_hash: SHA-256 hash of the canonical JSON evidence snapshot, including its _meta block where it has one.
  • 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): (",", ":")
  • Characters are written as themselves, with no ASCII escaping
  • 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. The one exception is the board packet: its manifest is hashed from JSON written with ASCII escaping, so a recipient verifying a board packet must escape non-ASCII characters.

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, no ASCII escaping, UTF-8)
  2. Compute SHA-256 of the resulting bytes
  3. Compare with the stored content_hash

For a board packet, serialize the manifest with sorted keys, minimal separators, and ASCII escaping instead.

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, shared_with_email
  • delivered_at, voided_at, void_reason
  • is_current, superseded_at
  • generated_at, generation_time_ms, updated_at
  • notes

How Immutability Is Enforced

When a packet is saved, the model checks what is being changed. If the save names fields with update_fields, any field outside the mutable set is rejected. On a full save, every field outside the mutable set is compared with the stored row, and any difference is rejected. Once a packet has been generated, its snapshot and as-of time are locked and the way to change them is a new version:

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

This check runs in the model's save(). A queryset update() does not call save(), so the check does not see it, and the model's own documentation bans that route. That is what the content hash is for: a snapshot changed by any route no longer matches its stored hash when the hash is recomputed. A later pack in the chain takes the stored hash as its previous-packet hash, so the chain alone does not show a changed snapshot; recomputing does.

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
restated A restatement was issued
pii_redacted Personal information was redacted
attested A signer attested the pack
attest_declined A signer declined to attest
fully_attested Every required attestation was collected
attest_expired An attestation expired
attest_invalid An attestation was invalidated

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: the model refuses to update an existing event, and events are written only by creating them. 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=(",", ":"), ensure_ascii=False).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.

Other Hashed Records

The same idea, a hash anyone can recompute, runs through the other records an association takes out of the system.

  • The whole-books export is one zip with a manifest that carries the scope, the row counts, and a SHA-256 for every file, including the README. One digest over the manifest identifies the delivery. Anyone holding the zip can recompute every hash without CommunityPay.
  • The annual financial statement package is hashed and verified by the report verification harness.
  • The figures on the platform page print, with an eight-character SHA-256 digest and the date they were read, at /verify/platform/.

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.


This describe

How CommunityPay Enforces This
  • SHA-256 content hash computed from canonical JSON (not PDF bytes), reproducible by anyone holding the snapshot
  • Previous-packet hash creates tamper-evident chain between versions
  • Immutability enforced at save: evidence_snapshot, content_hash, and the identifying fields cannot be modified after creation
  • Packet lifecycle tracked via append-only event log
  • Packets from the shared attestation generator record their schema version, generator version, generation time, and as-of date in a metadata block, and the hash covers it
  • The whole-books export and the annual financial statement package carry hashes of their own
Login