VRP Receipt Envelope — Specification v1
Status: Public draft
Updated: 2026-07-04 — §14 issuance wrapper & log anchoring, §15 delivery (verbatim rule), §16 verifier walk-through
Published: 2026-06-24 (revised 2026-07-04)
Envelope version: 1.0
Repository: https://github.com/vacationrentalprotocol/vrp-spec
Machine-readable schema: schemas/vrp-receipt.v1.schema.json
Reference verifier: lib/vrp-receipt.mjs (Apache-2.0)
1. Scope
VRP Receipt v1 defines a versioned verifiable booking record: a flat array of signed attestations from independent trust layers (offer, transport, payment, …) assembled by a host node. It composes the existing VRP signed stay offer (v0.1.md) and composition profiles (profiles/mcp-composition-profile.md) without making HemmaBo or any operator a gatekeeper.
This specification is not a runtime tool surface, OTA, marketplace, central registry, or booking intermediary. It does not change Portable Attestations v0.1 (attestations-v0.1.md) — especially not its §3 trust model.
The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are interpreted as in RFC 2119 and RFC 8174 when capitalized.
2. References
- Core VRP v0.1:
v0.1.md— JWKS discovery (§3), signed offers (§5) - Portable Attestations v0.1:
attestations-v0.1.md—did:webVC layer (§3–§8) - MCP composition profile:
profiles/mcp-composition-profile.md—transportlayer (D6) - Receipt JSON Schema:
schemas/vrp-receipt.v1.schema.json - Conformance vectors:
examples/conformance/receipt/ - ADR 0007: receipt envelope decisions D1–D8
- ADR 0008: key lifecycle K1–K6 (forward spec; §10 below)
3. Envelope shape (D1)
A v1 receipt MUST be a JSON object with:
| Field | Required | Description |
|---|---|---|
vrp_receipt_version | MUST | Exactly "1.0" for v1 verifiers |
subject | MUST | What the receipt is about (property, stay window, offer id — generic object) |
issuer | MUST | Node identity that assembled the receipt |
attestations | MUST | Flat array; min length 1 |
Each attestation object MUST include:
| Field | Required | Description |
|---|---|---|
layer | MUST | Open vocabulary string, e.g. offer, transport, payment |
valid_from | MUST | ISO 8601 date-time (D2) |
valid_until | MUST | ISO 8601 date-time (D2) |
Each attestation object MAY include:
| Field | Description |
|---|---|
source | URL where the verifying public key is published (typically host JWKS) |
signature | Compact JWS over the layer artifact (D5) |
ref | Opaque correlator (offer id, transaction id, …) |
tlog | Optional transparency-log inclusion proof (D3) |
sub_receipt | Reserved v2 — recursive sub-receipt; v1 verifiers MUST NOT recurse |
disclosure | Reserved v2 — selective-disclosure pointer; v1 verifiers MUST NOT interpret |
A new trust layer = a new attestations[] entry. No central approval is required.
4. Per-attestation freshness (D2)
Every attestation MUST carry valid_from and valid_until. A v1 verifier MUST reject an attestation whose verification time is before valid_from (not_yet_valid) or after valid_until (sig_expired) after the JWS signature is validated.
Freshness is evaluated per attestation, not once for the whole receipt.
5. Transparency log field (D3)
Each attestation MAY carry a tlog inclusion proof. A v1 verifier MUST:
- Treat a missing
tlogas signature-only verification — not a failure. - NOT claim log-anchored properties for an attestation without
tlog.
Receipts kept as dispute or insurance evidence that must survive key revocation MAY require tlog (see §10 K4).
6. Verification result and error registry (D4)
A v1 verifier MUST return partial verification: per-attestation status, not a single boolean. Envelope-level fields:
| Field | Meaning |
|---|---|
receipt_valid | Structurally valid v1 receipt (version + schema) |
fully_verified | true only when every attestation has status verified |
attestations[] | Per-layer { index, layer, status, error, kid } |
errors[] | Envelope-level codes when receipt_valid is false |
Per-attestation status values:
| Status | Meaning |
|---|---|
verified | Signature valid and inside freshness window |
expired | Signature valid but outside freshness window |
invalid | Signature check failed (e.g. sig_invalid) |
unverifiable | Layer present but cannot be checked (missing signature or key) |
unverifiable is distinct from verified. A receipt MUST NOT represent an unverifiable layer as verified.
6.1 Normative error codes (v1)
Envelope / signature level:
unsupported_version—vrp_receipt_version≠"1.0"malformed_receipt— fails schema / emptyattestations[]malformed_attestationmissing_validity_windowsig_invalidsig_expirednot_yet_validkey_unresolvablelayer_unverifiablecanonicalization_mismatch
VRP offer-layer (reused from core offer verification):
agent_permission_denied,not_available,price_not_exact,direct_booking_url_missing
AP2 payment-layer (reused from AP2 mandate verification):
mandate_expired,mandate_missing_amount,invalid_charge_amount,amount_exceeds_mandate,currency_mismatch,merchant_mismatch,cart_mismatch
The reference verifier in lib/vrp-receipt.mjs implements the envelope-level codes above for v1 receipt verification. Layer-specific offer/AP2 codes apply when a profile-aware verifier interprets attestation payloads.
7. Signature input rule (D5)
The signature MUST be verified over the compact-JWS bytes as received. A v1 verifier MUST NOT re-canonicalize JSON to re-derive the signing input.
Where canonical JSON is required for non-JWS correlators (e.g. MCP arguments_sha256), implementations MUST use JCS (RFC 8785). That canonicalization applies only to the hash input — not to JWS verification. See profiles/mcp-composition-profile.md.
8. Composition profiles (D6)
Composition profiles define how an external interaction becomes an attestation entry. The first normative profile is MCP transport — see profiles/mcp-composition-profile.md.
Profiles MUST sign an assertion about the interaction, not unsigned transport bytes. Additional profiles (e.g. AP2 payment) follow the same envelope rules.
8.1 Argument key casing
For MCP transport assertions, tool-call argument keys that feed arguments_sha256 MUST be snake_case: check_in, check_out, guests — matching core VRP endpoint parameters (v0.1.md §4). This is distinct from camelCase booking URL query parameters (v0.1.md §5.1). Normalization before hashing is the caller's responsibility; see the normative pin in the MCP profile.
9. Key-discovery layer separation
VRP uses two key-discovery paths. Implementers MUST NOT conflate them:
| Layer | Artifact | Key discovery | Typical kid form |
|---|---|---|---|
| Offer + receipt attestations | Compact JWS (offer, transport, …) | Fetch https://{host}/.well-known/jwks.json directly (v0.1.md §3) | Plain string kid matched against JWKS |
| Portable Attestations (VC) | application/vc+jwt credentials | Resolve issuer did:web:{host} → https://{host}/.well-known/did.json (attestations-v0.1.md §3) | DID URL kid on verification method |
The live reference verifier for receipt v1 resolves keys via attestation.source → JWKS and matches JWS kid — it does not resolve did:web documents for receipt layers. Portable Attestations remain a separate verification path with their own conformance vectors (examples/conformance/attestations/).
Offer signatures and attestation VC signatures MAY use separate private keys as long as each verifying key is published under the host-domain trust root (attestations-v0.1.md §3 — unchanged by this document).
10. Key lifecycle — forward spec (K1–K6)
The v1 receipt verifier implements signature + freshness only. The following rules are forward spec for receipts kept as dispute or insurance evidence (ADR 0008). They do not change v1 verifier behavior today.
| Rule | Summary |
|---|---|
| K1 | Every signature carries JWS kid; receipts surface verifying kid per attestation |
| K2 | JWKS MUST retain retired public keys for the receipt-retention horizon; metadata: vrp_key_status (active · retired · revoked), vrp_not_before, vrp_not_after |
| K3 | Rotation is additive with overlap — retired keys remain resolvable |
| K4 | Revocation ≠ rotation; vrp_compromised_at anchors fail-closed rules; receipts intended to survive revocation MUST carry tlog |
| K5 | Per-layer key separation is permitted (payment MAY use a distinct kid from offer) |
| K6 | Lifecycle-aware codes: key_retired_out_of_window, key_revoked, tlog_required |
Until a lifecycle-aware verifier ships, public copy MUST NOT claim long-term post-revocation verifiability.
11. Licensing (D7)
- Specification text (this document): CC0 1.0
- Reference code + conformance vectors: Apache-2.0 with explicit patent grant
12. Conformance
Conformance vectors live in examples/conformance/receipt/. They use a shared throwaway Ed25519 test key (kid: vrp-vectors-2026-01-01-01) — never a production host key (D6 test isolation).
Every npm test run executes scripts/verify-receipt-vectors.mjs, which MUST pass at minimum:
| Vector | Expectation |
|---|---|
01-offer-transport-verified.json | fully_verified: true; offer + transport both verified |
03-tampered-signature.json | Manipulated JWS byte → sig_invalid; fully_verified: false |
Additional vectors cover partial verification (02), expiry (04), unsupported version (05), and malformed envelope (06).
Implementers SHOULD treat vector output equality with lib/vrp-receipt.mjs as the interoperability bar for v1 receipt verification.
13. Neutrality (D8)
The spec, schema, and vectors are vendor-neutral. HemmaBo is a reference implementer, not an approval authority. Composition profiles are published, not centrally granted.
14. Issuance wrapper and log anchoring (envelope level)
An issued receipt MUST be delivered as a compact JWS signed by the issuing node's key — the same did:web-published key material as the node's other VRP artifacts — whose payload is the v1 envelope (§3). This wrapper is what makes the receipt log-anchorable at the envelope level:
- The transparency-log leaf for a receipt is
sha256:{hex}over the exact wrapper-JWS string as issued (Transparency Log §4.1), recorded withartifact_type: vrp_receipt. No JSON canonicalization is ever applied — the signed bytes are the one hashable form. - Envelope-level anchoring is external by hash: the inclusion proof is fetched from the log by leaf hash and lives outside the receipt. It cannot live inside the receipt — the leaf hash does not exist until the receipt's bytes are final. The per-attestation
tlogmember (§5) is unchanged and remains available for anchoring individual layer artifacts. - Wrapper verification follows D5 (§7): over the JWS bytes as received, never re-canonicalized.
The wrapper does not alter the envelope's trust semantics: attestations inside the receipt still verify per layer (§6), and log inclusion never makes an invalid attestation valid (Transparency Log §8).
15. Delivery (normative)
Because the log leaf binds the receipt's exact bytes (§14), delivery MUST preserve them:
- A receipt wrapper JWS MUST be delivered to its holder byte-verbatim.
- A receipt wrapper JWS MUST NOT be embedded inline in the flowing text of an email body, chat message, or any medium that re-wraps or re-encodes text. Line wrapping, whitespace normalization, or character substitution silently change the bytes — the signature may even still validate after trimming, but the leaf hash will not match, making a correctly issued receipt unprovable against the log.
- The holder-facing message SHOULD instead carry a link to a surface that serves the JWS string verbatim with a copy affordance (for example the node's own guest surface), together with the verification link.
16. Proving a receipt against the log (verifier walk-through)
Any party can execute this procedure with the receipt and public interfaces only — no credentials, no contacting the node's operator:
- Obtain the receipt wrapper JWS verbatim (§15).
- Verify the wrapper signature against the issuing node's published keys (
did:web/ JWKS). This proves who issued the receipt and that its content is intact. - Compute
sha256:{hex}over the exact JWS string. This is the leaf identity. - Fetch the inclusion proof and an STH for that hash — from the node's read-through proxy or from the log operator directly; the two surfaces are interchangeable because of step 5.
- Verify the STH signature against the log operator's published key (Transparency Log §5.1) — never against a key the node serves.
- Recompute the Merkle root from the leaf hash, leaf index, tree size, and audit path (RFC 6962 §2.1.1). The inclusion claim holds iff the recomputed root equals the STH's
root_hash; a failed recomputation falsifies it. - To additionally remove trust in the log operator, obtain a second tree head (archived, Transparency Log §7.1, or independently observed) and verify the consistency proof between the two tree sizes.
What this proves, in the layered formulation of Transparency Log §8.1: the exact promised terms — price, conditions, issuer, time — existed and were recorded in the log no later than the STH timestamp, tamper-evidently; steps 1–6 require no trust in the node, and step 7 removes the remaining trust in the log operator by making any rewrite cryptographically detectable.
17. License
Specification text: dedicated to the public domain under CC0 1.0. Reference code and conformance test vectors: Apache-2.0 (ADR 0007 D7).