Listing disclosure records
Sealed, verifiable records of what the listing showed at a moment in time — the listing-side companion to UnitVault document snapshots.
A disclosure record is a sealed snapshot of what your listing said at a specific moment in time — title, price, condition, year, model, serial number, equipment parameters, the photos and videos shown on the page, the documents attached. After that moment, even if you edit the listing, the disclosure record stays put, and any buyer (or court, or auditor) can independently verify what was on the page when the record was sealed.
Where document snapshots prove which documents were disclosed, disclosure records prove what the listing itself showed. The two run side by side and reference each other.
When a disclosure record is created
Three ways:
- Automatically on first publish. Every listing that goes live for the first time gets one. Source label:
First publish. - Automatically on a material edit. When you change a field that's part of the disclosure surface (title, price, year, condition, description, serial number, equipment parameters, the photos and videos shown on the page, …) Haubot creates a new record. Cosmetic edits — moderation notes, internal counters, FX recomputation of the USD-normalized price — do not trigger a record. The whitelist itself defines "material". Source label:
Listing updated. - Manually. You can create one any time from the seller dashboard, with a free-form reason.
Auto-records default to Public full record — buyers see the sealed listing fields, equipment parameters, a media-count summary, and can download the disclosure package straight from the listing page. The v2 sealed payload was designed to be public-safe by construction (no internal IDs, resolved brand / category / model names), so exposing it by default trades nothing for real buyer value.
You can downgrade an individual record to Public summary (metadata only — the seal date and hash, no listing fields) or to Private (only you and Haubot staff see it exists) at any time. Manual records you create yourself default to Private so a deliberate decision is always required to expose them.
What gets sealed
The record captures the listing's public/commercial details:
- Title, description
- Category (resolved name)
- Brand, model, serial number, year
- Condition
- Listing type (sale / auction / rental / lease / wanted)
- Status (published, expired, sold, …)
- Currency and the seller's stated price (in the seller's currency)
- Rental period (when applicable)
- Publication and expiry dates
- Optional report URL
- Location (country and city — never a precise address)
- Equipment parameters — every spec the seller filled in, with a stable code, a human-readable label, a value, and a unit (e.g.
engine_power_kw/Engine power/250/kW)
It also records a structured reference to the listing's media — photos and videos by their public id, position, primary-photo flag, MIME type, size, and a SHA-256 of the file's bytes. The bytes themselves stay on the listing page (the package records what was disclosed, not the actual image / video files). And — when a document snapshot was sealed at the same moment — a link to that document snapshot.
What's not sealed:
- Internal numeric IDs that buyers never see (seller user id, brand FK, category FK, …)
- Storage backend keys, private URLs to media files
- Moderation notes, processing counters, USD-normalized price, FX recompute timestamps
- Anything not on the disclosure whitelist
What the sealed listing record looks like (payload v2)
The sealed listing payload is human-readable. It uses public identifiers and resolved brand/category/model names — never internal database ids:
{
"payload_version": 2,
"listing_public_id": "PF2197BP",
"seller_public_id": "SLR12345",
"title": "Cat D11T for sale",
"description": "Excellent condition",
"category": "Dozers",
"brand": "Caterpillar",
"model": "D11T",
"serial_number": "SKB20020",
"year": 2020,
"condition": "GOOD",
"listing_type": "FOR_SALE",
"status": "PUBLISHED",
"price": {
"amount": "250000.00",
"currency": "USD"
},
"rental_period": null,
"report_url": null,
"published_at": "2026-04-28T14:55:39Z",
"expires_at": null,
"location": {
"country": "US",
"city": "Houston"
},
"equipment_parameters": [
{
"key": "bucket_capacity",
"label": "Bucket capacity",
"unit": "m3",
"value": "2.4"
},
{
"key": "engine_power_kw",
"label": "Engine power",
"unit": "kW",
"value": "250.5"
}
]
}Records sealed before payload v2 stayed at v1 (DB-shaped) — Haubot does not rewrite history. Their hashes still verify against their original bytes; only newly created records use v2. The equipment_parameters field is part of v2; legacy v1 rows have no equipment-parameter data and the buyer panel hides the section for them.
Why the sealed value uses option codes, not localized labels
For "drop-down" parameters (e.g. fuel type with options Diesel / Gasoline / Electric), the sealed value is the option's stable code (DIESEL), not its localized label. Renaming or translating an option label later does not shift the hash — the seal records what was actually selected, and the UI is free to relabel the same code in any language client-side.
The same idea applies to parameter labels themselves: the sealed label is the canonical English name at the time the seal was taken. A future translation pass on the parameter directory does not invalidate older seals.
Disclosure history — what buyers see across versions
When a listing goes through several material edits, several records accumulate. Buyers see the timeline as part of the trust panel:
- The current record (the latest visible one) renders in full: sealed listing fields, equipment parameters, media summary, download button.
- Older records render below as a compact list — date, source label (
First published,Listing updated), short hash, copy-hash button. No fields, no download button.
This is intentional. The history shows that the listing's disclosure was sealed at every material edit, which is the integrity property buyers care about. It is not meant to be a complete archive of every prior version of the listing — exposing every old payload by default would turn the public listing page into a research surface that nobody asked for, and that some sellers would object to.
If a buyer requests the package for an older snapshot directly (by guessing or copying a snapshot id), the server returns a summary-only package — date, source, hashes, and a copy of the original sealed hashes for cross-reference, but no sealed listing fields and no media manifest. The seller / staff dashboard is unaffected: from /dashboard/listings/{id}/vault you can still pull a full owner package for any record at any time.
Visibility levels
Each record has one of three visibility levels. You can change the level at any time without altering the record's content or its hash.
- Public full record (
BUYER_FULL). Buyers see the sealed listing fields (the buyer-safe view — internal IDs are stripped), equipment parameters, a media-count summary, and can download a verifiable disclosure package. Default for auto records (first publish, material edit). - Public summary (
BUYER_SUMMARY). Buyers see a small panel on the public listing page — the seal date, the source, and a snapshot hash they can copy. The sealed listing details and document set are not exposed. Useful when you want to advertise that the listing was sealed without disclosing what's in the seal. - Private (
SELLER_ONLY). Only you and Haubot staff can see the record exists. Default for manual records you create yourself.
The disclosure package
For records at Public summary or Public full record, the buyer can download a ZIP. What the package contains depends on visibility, on whether the row is the current (latest visible) row, and on what documents the buyer is entitled to:
| File | Public summary | Public full (current) | Public full (older) | Owner / staff |
|---|---|---|---|---|
README.txt | yes | yes | yes | yes |
combined_manifest.json (sealed hashes + manifest cross-references) | yes | yes | yes | yes |
listing_manifest.json | — | buyer-safe projection + separate buyer_projection_hash | — | full canonical payload |
media_manifest.json | — | per-item buyer-safe entries | — | full media payload |
document_manifest.json | filtered to allowed docs | filtered to allowed docs | filtered to allowed docs | full set |
documents/ folder | filtered to allowed docs | filtered to allowed docs | filtered to allowed docs | full set |
package_warnings.json (when applicable) | yes | yes | yes | yes |
buyer_projection_hash is not the same as the sealed listing_payload_hash. The buyer projection deliberately drops internal-only fields, so its bytes — and therefore its hash — differ from the original sealed record. The package always carries the original sealed hash too, so the buyer can prove "this is what I saw" while the original sealed record stays intact and untouched. The README in the package explains both.
What's inside media_manifest.json
For a Public full record on the current row, the buyer media manifest lists every photo and video that was sealed — one entry per item, public-safe by construction:
{
"manifest_kind": "BUYER_MEDIA_MANIFEST",
"schema_version": 1,
"snapshot_id": 55,
"listing_public_id": "PF2197BP",
"media_count": 3,
"photo_count": 2,
"video_count": 1,
"media_payload_hash": "…",
"media": [
{
"media_public_id": "PHOTOAAA",
"type": "IMAGE",
"display_order": 1,
"is_primary": true,
"mime_type": "image/jpeg",
"size_bytes": 1024000,
"content_hash": "a4f3…",
"created_at": "2026-04-28T14:55:39Z"
}
]
}content_hash is the SHA-256 of the photo / video bytes that were on the listing page at seal time, computed server-side. The manifest tells the buyer what media was disclosed, not where to download it — the bytes themselves stay on the listing page. Two snapshots taken before and after a photo was replaced will have different content_hash values for that media row, which is what makes "the seller swapped the inspection photo" detectable.
Internal media database ids and storage backend keys are intentionally absent from the manifest. The only stable reference exposed is media_public_id.
What package_warnings.json means
Sometimes a document is listed in document_manifest.json but its bytes are missing from documents/. That's not a bug, but you should know why. The package_warnings.json file appears when there's anything to explain:
| Reason | Expected? | What it means |
|---|---|---|
INTERNAL_ONLY_FILTERED | yes | A restricted document was excluded from your package. Its filename is intentionally omitted. |
APPROVAL_REQUIRED_NOT_GRANTED | yes | A document needs seller approval and you don't have an active grant. Filename is shown so you can request access. |
REGISTERED_REQUIRED | yes | A document needs sign-in. Filename is shown so you can sign in and try again. |
STORAGE_READ_FAILED | no | A document was supposed to ship but the storage backend failed. Re-running the export usually recovers the bytes. |
If the file isn't in the ZIP, the package is clean.
How to verify a package on your own laptop
The package is designed so anyone — buyer, lawyer, third-party auditor — can verify it offline. There are two layers of verification: a SHA-256 hash chain (works without any Haubot involvement) and an Ed25519 platform signature (proves Haubot itself produced the manifest and didn't alter it later).
Quick path: the in-browser verifier
For most people, the easiest way to confirm a package is genuine is the public verifier at /verify/unitvault-disclosure. Drop the downloaded ZIP into the page; the verifier reads it locally in your browser, compares the manifest's SHA-256 against the signed hash, and checks the Ed25519 signature against Haubot's published key registry at /.well-known/disclosure-keys.json. Nothing is uploaded to Haubot — the file is processed entirely in the browser tab.
A green "Verified: Signed by Haubot" result means: the manifest hash matches the signed hash, the signature checks out against an official Haubot key, and the registered key is currently active.
By hand on the command line
The exact steps are inside README.txt, but the gist:
# 1. The combined manifest is the verification anchor.
sha256sum combined_manifest.json
# Compare the result against the snapshot hash you saw on the listing page,
# AND against signedObjectSha256 inside combined_manifest.sig.json.
# 2. Each enclosed manifest's hash is listed inside combined_manifest.json
# under "manifest_hashes". Re-hash each one and compare.
sha256sum listing_manifest.json
sha256sum media_manifest.json
sha256sum document_manifest.json
# 3. Each document's content_hash is listed inside document_manifest.json.
# Re-hash each document and compare.
sha256sum documents/*
# 4. (Optional) Save the photo / video files from the listing page and
# sha256 them against the content_hash entries in media_manifest.json.
sha256sum saved-photo.jpg
# 5. (Optional) Verify the Ed25519 platform signature directly. The
# signing-key.json shipped inside the ZIP is a convenience copy; the
# authoritative public key lives at /.well-known/disclosure-keys.json.
curl -s https://haubot.com/.well-known/disclosure-keys.json \
| jq -r '.keys[] | select(.keyId=="<keyId-from-sig.json>").publicKeyBase64' \
| base64 -d > haubot-pub.der
openssl pkeyutl -verify -pubin -keyform DER -inkey haubot-pub.der \
-rawin -in combined_manifest.json \
-sigfile <(jq -r .signature combined_manifest.sig.json | base64 -d)If any hash doesn't match, the package has been altered after export — or, far more often, you got an incomplete download. Try again.
What hashes prove and what they do not
A SHA-256 hash proves byte integrity — that the data you have is byte-for-byte identical to what was sealed at the listed time. It is a powerful and tamper-evident signal in a dispute, and it is computable by anyone with sha256sum and the package.
It does not prove:
- That the seller had legal title to the equipment.
- That the listing's description was true — only that this exact text was published.
- Transfer of ownership or any contractual obligation.
- That a regulatory body accepts SHA-256 as sufficient evidence in your jurisdiction (some do, some require additional notarization).
A disclosure package supports due diligence. It does not replace it. Inspections, title checks, and contracts still apply — the package just gives everyone an honest, time-stamped picture of what was disclosed when, so neither side can later edit history.
Platform signature (Ed25519)
In addition to the hash chain, every disclosure package now ships with an Ed25519 platform signature over the bytes of combined_manifest.json. The signature lives in combined_manifest.sig.json inside the ZIP, and proves two distinct things:
- It was Haubot that produced this manifest — not an impostor with the same JSON shape.
- The manifest hasn't been altered since signing — even by Haubot.
The signature key Haubot uses is published at /.well-known/disclosure-keys.json (and a convenience copy travels inside the ZIP as signing-key.json). For an adversarial check, always cross-reference the embedded copy against the public registry — the in-browser verifier and the openssl recipe above both do this automatically.
Anyone can verify the signature offline once they have:
- the ZIP,
- Haubot's public key (X.509 SPKI form lives at
/.well-known/disclosure-keys.json, raw 32-byte form is also published there aspublicKeyRawBase64for tools that want it directly).
A failed signature, or a key Haubot doesn't list, means the package has been altered or wasn't produced by Haubot.
Inspecting a record on the seller dashboard
On /dashboard/listings/{id}/vault, the Disclosure record panel lists every record on the listing's vault, newest first. Each row carries:
- The seal date
- The source label (
First publish,Listing updated,Manual) - A short combined hash you can copy
- Visibility (
Private/Public summary/Public full record) - A "Doc snapshot" badge when a document snapshot was linked at the same moment
For each record you can:
- Verify — re-check that the sealed details and the snapshot hash still match.
- Compare — see what's changed in the listing since the record was sealed (whitelisted fields only, including equipment parameters and media references).
- Export manifest — download the canonical JSON for your own archives.
- Visibility — flip between Private / Public summary / Public full record. The record's content and hash never change.
- Details — see the sealed listing fields in plain language. A "Show raw manifest" toggle exposes the canonical bytes for advanced users.
What buyers see
On the public listing page, buyers see a Disclosure record panel under the documents block — only when at least one record is Public summary or Public full record. The panel shows:
- The seal date of the latest visible record
- A short snapshot hash
- A "Document set was sealed at the same moment" line if a document snapshot is linked
- For
Public full record: the sealed listing fields, equipment parameters, and a media-count summary - A Verify button (works for guests too)
- A Download disclosure package button when the seller's visibility allows it
- A Disclosure history timeline of older sealed records — date / source / short hash, no payload, no download
If the latest record is Private — or there's no record yet — the panel doesn't appear. Haubot creates the first one automatically when you publish.


