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
_metablock 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:
- Serialize the evidence_snapshot to canonical JSON (sorted keys, minimal separators, no ASCII escaping, UTF-8)
- Compute SHA-256 of the resulting bytes
- 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:
- The system looks up the current pack (the one being superseded)
- It copies the superseded pack's
content_hashinto the new pack'sprevious_packet_hash - The new pack's own
content_hashis 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:
- This version hasn't been tampered with (content_hash matches snapshot)
- The previous version existed and had specific content (previous_packet_hash matches the predecessor's content_hash)
- 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_hashprevious_packet_hashevidence_snapshotpublic_idreference_numberpacket_typeversionscopeas_of_atcreated_byhoacreated_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_emaildelivered_at,voided_at,void_reasonis_current,superseded_atgenerated_at,generation_time_ms,updated_atnotes
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:
- Obtain the pack's evidence_snapshot and content_hash (included in the pack itself)
- Compute:
sha256(canonical_json(evidence_snapshot)) - Compare: If it matches
content_hash, the data is intact - Optionally verify the chain: Check
previous_packet_hashagainst the predecessor'scontent_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