# Vacation Rental Protocol - Specification v0.1

**Status:** Public draft  
**Published:** 2026-05-20  
**Canonical URL:** https://vacationrentalprotocol.com  
**Repository:** https://github.com/HemmaBo-se/vrp-spec

## 1. Scope

Vacation Rental Protocol (VRP) v0.1 defines how a host-owned vacation rental domain publishes discovery metadata, signing keys, and signed verified stay offers so AI agents can verify provenance, freshness, exact price, and direct booking URL.

VRP v0.1 is not an OTA, marketplace, central registry, public traffic proxy, payment processor, or central key issuer.

VRP v0.1 is a no-gatekeeper protocol. Verification MUST NOT require HemmaBo, a VRP-operated trusted issuer registry, a host accreditation program, a certification company, or a central discovery index.

VRP may compose with adjacent agent-commerce standards. UCP can handle checkout and order lifecycle, including lodging flows as the UCP lodging profile matures; AP2 can handle payment mandates; a future agent-native settlement rail (e.g. x402) is reserved but not defined (see §5.2); MCP can expose future tool bindings; and A2A can carry future agent-to-agent negotiation. VRP v0.1 does not define those runtime flows.

## 2. Discovery Document

A VRP host domain publishes:

```text
https://{host-domain}/.well-known/vacation-rental.json
```

Required fields:

- `protocol`: MUST be `vacation-rental-protocol`.
- `protocol_version`: MUST be `0.1`.
- `canonical_domain`: MUST match the host-owned domain being verified.
- `jwks_url`: MUST point to the host-domain JWKS.
- `verified_stay_offer_endpoint`: MUST point to the host-domain verified stay offer endpoint.

Recommended fields:

- `node_id`
- `capabilities`
- `operator` — MAY carry `name`, `role`, and `key_custody`. `key_custody` declares who holds the node's Ed25519 signing key: `platform` (an operator holds and operates the key on the host's behalf — offers are signed under the host's domain identity and remain independently verifiable against `jwks_url`, but the host does not hold the key itself) or `self` (the host self-custodies their own key). Absent = unspecified; a verifier MUST NOT assume self-custody. This lets an agent reason about exactly what a valid signature guarantees — provenance and integrity in both cases, host self-sovereignty only when `self`.
- `endpoints`

## 3. JWKS

A VRP host domain publishes an Ed25519 public key set:

```text
https://{host-domain}/.well-known/jwks.json
```

Keys used for signing verified stay offers MUST use:

- `kty`: `OKP`
- `crv`: `Ed25519`
- `alg`: `EdDSA`
- `kid`: stable key identifier
- `x`: base64url Ed25519 public key

Such keys SHOULD also set:

- `use`: `sig`
- `key_ops`: include `verify`

The JWKS structure is pinned by
[`schemas/jwks-v0.1.schema.json`](../schemas/jwks-v0.1.schema.json).

### 3.1 Key Rotation and Revocation

A node's signing authority is the Ed25519 key set published at its `jwks.json`
(and mirrored by `did:web`). Because there is no central issuer, **the JWKS is the
only revocation authority**: a key is valid exactly while it appears in the node's
JWKS.

**Rotation.** To rotate, a node MUST publish the new key alongside the old one in
its JWKS and sign new offers with the new key's `kid`. It SHOULD retain the old
key in the JWKS until every offer signed with it has passed its `valid_until`, so
the retention overlap equals the maximum offer lifetime. A node SHOULD use a `kid`
that encodes a date and sequence (e.g. `example.com-2026-05-18-01`) so operators
can order keys; a verifier MUST treat `kid` as opaque and MUST NOT infer trust,
recency, or status from its value.

**Refresh on miss.** If a verifier receives an offer whose `kid` is not present in
its cached JWKS, it MUST re-fetch the node's JWKS before acting, and only treat
the offer as unverifiable if the `kid` is still absent. This makes a rotation take
effect immediately (the new key is picked up on first use) and decouples rotation
from the cache bound below, so the cache bound governs revocation latency alone. To
avoid amplification, a verifier SHOULD rate-limit or coalesce refresh-on-miss per
domain, so a stream of offers bearing unknown `kid`s cannot drive it into excessive
JWKS fetches against the node.

**Revocation.** A key's *presence in the JWKS is the entire trust signal*: an offer
verifies only against a key that is in the JWKS now. Removing a key therefore makes
**every** offer signed with it unverifiable, regardless of when it was signed —
there is no retroactive "historically valid" exemption (and a conforming verifier
could not honor one anyway, since it only knows whether a key is currently present).

- *Routine rotation* avoids breakage through timing: retain the outgoing key until
  its outstanding offers have passed `valid_until`, then remove it — no live offer
  is still signed by it, so nothing breaks.
- *Compromise* requires immediate removal. This also invalidates the operator's own
  legitimate offers signed with that key — that is **intentional**: a verifier
  cannot distinguish the operator's offers from an attacker's, because they share
  the same key and produce equally valid signatures. Re-sign legitimate stays with
  the new key.

Offer freshness (`valid_until`, §6) does NOT bound a compromise — an attacker
holding the leaked private key can mint fresh offers with a future `valid_until`
that pass the freshness check. The only thing that stops a conforming verifier from
honoring them is the key being absent from the JWKS it holds. Therefore:

- A verifier MUST NOT cache a node's JWKS for longer than **5 minutes**. A node's
  `Cache-Control` may only *shorten* this window, never extend it — the 5-minute cap
  is absolute. This bound is the revocation latency: a compromised key is fully
  de-trusted by conforming verifiers within the cache window after it is removed
  from the JWKS.
- A node SHOULD serve `jwks.json` with a short `Cache-Control` (e.g. `max-age=300`
  or less). The JWKS is a small, CDN-cacheable document, so frequent re-fetching is
  inexpensive.

**Three distinct controls (do not conflate).** Freshness (`valid_until`, §6) bounds
how long an offer is quotable; refresh-on-miss gives immediate discovery of the
current key set (rotation); the JWKS cache cap bounds how long a revoked key is
still accepted (revocation latency). They are independent — none substitutes for
another.

**Threat model.** The root of trust is **domain control** (`did:web`): the key set
is whatever the host domain serves. Rotation and revocation protect the case where
a *signing key* leaks while the *domain remains under the operator's control*. They
do NOT protect against compromise of the domain itself — an attacker who can serve
the domain's `jwks.json` can publish their own keys, and no key-level mechanism can
detect that. Domain-level security (DNS, TLS, hosting) is out of scope for VRP and
is the operator's responsibility.

**Enforcement honesty.** VRP cannot *force* a verifier to cache for a bounded time.
The cache bound is a MUST on *conforming* verifiers; there is no protocol backstop
against a non-conforming verifier that caches a revoked key indefinitely. Treat the
bound as a conformance requirement, not a guarantee VRP can enforce.

**Optional transparency-log composition.** A node MAY record key-rotation events
(key added / key removed, with `kid` and timestamp) in its append-only transparency
log (see [`transparency-log-v0.1`](./transparency-log-v0.1.md)), giving a
tamper-evident history of which keys were valid when — useful for after-the-fact
disputes about whether an offer was signed by a then-valid key.

## 4. Verified Stay Offer Endpoint

The verified stay offer endpoint accepts at least:

- `check_in`: arrival date in `YYYY-MM-DD`
- `check_out`: departure date in `YYYY-MM-DD`
- `guests`: integer guest count

The endpoint returns a signed verified stay offer envelope. The signed payload contains the quoteable facts.

## 5. Signed Offer

The signature format is compact JWS using EdDSA over an Ed25519 key published in the host-domain JWKS.

The signed payload MUST include:

- `kind`: `verified_stay_offer`
- `protocol_version`: `0.1`
- `canonical_domain`
- `node_id`
- `generated_at`
- `valid_until`
- `request`
- `property`
- `availability`
- `price`
- `booking`
- `agent_permission`

The signed payload MAY also include host-verified direct-source facts, so an
agent can verify (not just read) the node's positioning:

- `source_authority` — `model: host_verified_direct_source`,
  `is_official_source_for_property`, `intermediary: none`,
  `payment_recipient: host`, `booking_model: direct_with_host`,
  `booking_commission_pct: 0`. A reselling marketplace cannot sign these.
- `price.no_add_on_fees` — `true` asserts the quoted total has no add-on
  booking, service, or cleaning fees (the displayed price is the total paid).
  This is the node's own fee structure, never an OTA comparison.

The envelope and payload structure is pinned by
[`schemas/verified-stay-offer-v0.1.schema.json`](../schemas/verified-stay-offer-v0.1.schema.json),
which is authoritative for v0.1 offer shape. A real, verifiable Ed25519/JWS
example and its failure modes are in
[`examples/conformance/`](../examples/conformance/).

### 5.1 Direct Booking URL

The signed payload `booking` object carries the direct booking URL the agent
routes the user to. The `booking` object MUST include:

- `direct_booking_url`

`direct_booking_url` is the only booking action a VRP agent may take for the
offer (see §7). The following rules define its structure, integrity, and
lifecycle.

**Structure.**

- `direct_booking_url` MUST be an absolute `https` URL.
- Its host MUST be the offer's `canonical_domain`, or a subdomain of that
  registrable domain. It MUST NOT point at a third-party domain, an OTA, a link
  shortener, a redirector, or a HemmaBo-operated domain. An agent that cannot
  confirm the URL host is on the offer's `canonical_domain` MUST treat the
  direct booking URL as `unknown` (see §9) and MUST NOT route booking to it.
- The URL SHOULD encode the quoted stay so that the booking page opens
  pre-filled for the same stay the agent quoted. The RECOMMENDED query
  parameters are:
  - `checkIn`: arrival date as `YYYY-MM-DD`, matching the offer `request.check_in`.
  - `checkOut`: departure date as `YYYY-MM-DD`, matching the offer `request.check_out`.
  - `guests`: integer guest count, matching the offer `request.guests`.
- The URL MAY include an offer reference identifier (for example an `offer`
  query parameter) so the host node can correlate the click with the signed
  offer it issued. The reference identifier, when present, MUST NOT be required
  by a verifier to validate the offer, and MUST NOT be treated as a substitute
  for verifying the JWS signature.
- The URL MAY include additional host-specific query parameters. A verifier
  MUST ignore unrecognized query parameters and MUST NOT treat their presence as
  a verification failure.

**Integrity.** `direct_booking_url` is a field inside the signed offer payload.
Its integrity derives solely from the compact JWS over that payload (§5): if the
JWS verifies against the host-domain JWKS, the direct booking URL is exactly the
URL the host signed. There is no separate signature over the URL, and an agent
MUST NOT accept a `direct_booking_url` delivered outside, or modified after, the
signed payload. A `direct_booking_url` whose enclosing offer fails signature
verification is `unknown` and MUST NOT be used.

**Lifecycle.** The direct booking URL is actionable only while the enclosing
offer is fresh, that is while `valid_until` holds (§6).

- While the offer is fresh, an agent MAY present and route the user to
  `direct_booking_url` subject to the Safe-to-Quote rules (§7).
- Once `valid_until` has passed, the offer is stale. The agent MUST treat the
  direct booking URL as `unknown` and MUST NOT present its associated price as
  current or claim the stay is bookable on the strength of the stale offer. The
  agent SHOULD fetch a fresh signed offer for the user's dates and guest count
  (§6) before routing a booking action.
- After expiry, the host node MAY continue to serve the same URL, MAY re-quote
  (return a new signed offer, which may carry a different price, availability,
  or `direct_booking_url`), or MAY return an unavailable or non-quoteable offer.
  Following an expired `direct_booking_url` is therefore an unverified action:
  the host, not the protocol, decides what the URL does after expiry.
- A host node SHOULD ensure that following `direct_booking_url` after expiry
  fails closed for the guest — for example by re-quoting on the landing page
  rather than silently honoring a stale price. VRP does not guarantee that a
  price observed in an expired offer is still available.

The booking object shape is pinned by
[`schemas/verified-stay-offer-v0.1.schema.json`](../schemas/verified-stay-offer-v0.1.schema.json).
The schema validates that `direct_booking_url` is present and is an `https` URL;
the `canonical_domain` host constraint and the query-parameter recommendations
above are normative protocol rules that a verifier enforces at runtime, not
structural schema constraints.

### 5.2 Reserved: optional agent-native payment rails

VRP v0.1 defines exactly ONE payment path: `direct_booking_url` (§5.1), settled
by the host's own processor (fiat, e.g. Stripe). This is deliberate — every
field VRP defines is one a live node actually honors and an agent can verify.

The optional `booking.payment_options[]` field is **RESERVED** for a future
agent-native payment-rail binding (e.g. `x402`) and is **NOT defined in v0.1**:
its shape, settlement, refund handling, and reconciliation are intentionally
left open. A node SHOULD omit it and rely on `direct_booking_url`; an agent MUST
NOT treat its presence as a defined, honorable payment path.

Why reserved, not specified: agent-native rails are a moving target (x402 added
sessions in a later revision; competing models such as ACP, AP2, and
session-based protocols are unsettled; refund and chargeback semantics for real
bookings are unsolved), and Stripe already ships x402 — so the concrete binding
will likely arrive largely for free once non-US stablecoin receipt opens for the
host's own account. Specifying the mechanics now would publish a field no live
node honors — the "coming soon" vapor VRP exists to replace — and risk locking a
shape the moving target will break. The name is reserved so the slot cannot be
squatted with a conflicting meaning; the binding is hand-specified later, when a
winning rail and a non-US settlement path exist.

Two invariants WILL hold whenever the binding is defined, and constrain it now:
the payee is the host's own (never an intermediary, OTA, or HemmaBo), and rails
MUST NOT gate discovery, verification, or offer retrieval (no-gatekeeper, §1).

### 5.3 Computable refund schedule (optional)

A node MAY publish its cancellation refund terms inside the signed offer as
`rules.refund_schedule` — an array of rows
`{ "hours_before_checkin": int ≥ 0, "refund_percent": int 0–100 }`.

The schedule is **computable, not descriptive**: an agent (or a future
agent-native settlement rail, §5.2) evaluates it as a pure predicate with no
platform context:

1. Compute `H = floor((check-in moment − cancellation time) / 1 hour)` —
   whole hours, **floored**. Flooring makes fractional boundaries resolve in
   the node's favour; implementations MUST NOT substitute other rounding.
2. Sort rows by `hours_before_checkin` descending. The FIRST row with
   `hours_before_checkin ≤ H` applies; the guest receives that row's
   `refund_percent` of the paid total.
3. No matching row — including any cancellation after the check-in moment,
   where `H` is negative — means 0 %.

A row with `hours_before_checkin: 0` therefore means "this percent up to the
check-in moment itself". An EMPTY array is a valid, honest schedule meaning
"no refund at any point". An ABSENT or `null` field means the node has not
published computable terms: the answer is unknown, and an agent MUST NOT
invent, infer, or default a refund promise.

Because the schedule is signed inside the offer (§5) it is **verifiable, not
merely attested**: the guest's agent can prove after the fact exactly which
refund terms were in force at quote time, regardless of what the node's pages
say later. Nodes and agents MUST relay the rows verbatim to guests — as
hours/percent terms, never re-labelled into named tiers ("flexible",
"moderate", …), which this field replaces.

### 5.4 Verifiability classes

VRP data falls into exactly three classes. When an agent (or a human) asks
"how do I know this?", the answer is one of these — and a surface MUST NOT
present a lower class as a higher one (an attested amenity is never
"verified"; a review aggregate is never a term of the booking).

1. **verifiable** — fields signed INSIDE the offer's compact Ed25519 JWS (§5)
   and, where the node emits receipts, bound byte-for-byte into the same
   receipt attestation and transparency-log leaf. One signature, one log
   entry, one timestamp: price (including `exact` and `no_add_on_fees`),
   `availability`, `valid_until`, `source_authority`, and
   `rules.refund_schedule` (§5.3). Tampering with any of them kills the same
   signature; a guest's agent can prove after the fact exactly which terms
   were in force at quote time. `refund_schedule` is the canonical example of
   a field deliberately MOVED into this class: cancellation terms used to be
   presentation-layer tier labels (attested at best), and are now signed rows
   inseparable from the price they govern.

2. **attested** — host-declared tri-state claims (amenities, policies)
   published in the node's discovery document and optionally as a
   `VRPPropertyAttestedClaimsCredential`
   ([attestations-v0.1.md](attestations-v0.1.md) §5.5). Actionable as the
   host's explicit statement — including negations ("cats are not allowed") —
   but not cryptographically bound to a specific purchase moment. Absence is
   Unknown, never a yes or a no.

3. **reputational** — review aggregates, stay history, and similar evidence
   about past experience. An agent MAY cite it (accurately attributed and
   time-bounded) but MUST NOT present it as a promise, a term, or a fact
   about the current offer.

**Determining the class of a field is mechanical, never editorial:** the
class follows from WHERE the value was read, not from how any surface phrases
it. Read from inside the offer JWS payload (after signature verification) →
verifiable. Read from the discovery document's claims or an attestation
credential → attested. Read from review data → reputational. A value that
appears in more than one place carries the class of the surface it was
actually read from — reading an amenity from the discovery document never
inherits the offer's signature, even when the same value also happens to
appear inside a signed offer. If a host asserts something OUTSIDE the signed
payload — in prose, frontmatter, marketing copy, or an unsigned field — a
renderer MUST treat it as attested at best, regardless of wording: language
like "verified", "guaranteed", or "signed" attached to an unsigned value is a
class violation, not a promotion.

## 6. Freshness

Agents MUST treat an offer as non-quoteable if `valid_until` is missing, malformed, or expired.

Agents SHOULD fetch a fresh offer for the user's specific dates and guest count before presenting a final price or booking URL.

## 7. Safe-to-Quote Rules

An agent may quote an offer as an official host-domain verified offer only when all of the following are true:

- The discovery document is fetched from the host-owned domain.
- The discovery document declares `protocol: "vacation-rental-protocol"`.
- The discovery document declares `protocol_version: "0.1"`.
- The JWKS contains an Ed25519 verification key matching the JWS.
- The compact JWS verifies against the host-domain JWKS.
- The signed payload matches the returned offer.
- `valid_until` is present and fresh.
- `availability.available` is true.
- `price.exact` is true.
- A direct booking URL is present.
- `agent_permission.may_quote_as_official_direct_offer` is true.

When safe, the canonical phrase is:

```text
I found the official host-domain verified offer for this stay.
```

Agents MUST NOT:

- Invent discounts, savings, promo codes, OTA comparisons, or price reductions.
- Say the stay is bookable unless `availability.available` is true and agent permission allows quoting.
- Quote a final total unless `price.exact` is true in the signed offer.
- Route booking action anywhere except the signed direct booking URL.

## 8. Fail-Closed Behavior

Agents and clients MUST fail closed. If signature verification, freshness, exact price, availability, direct booking URL, or citation permission cannot be verified, the offer MUST NOT be quoted as official or bookable.

## 9. Three-State Verification

Agents and clients interacting with VRP nodes MUST distinguish three states for any required endpoint or field:

- **Affirmed**: present, signed where required, fresh, schema-valid, and explicitly true or present. May be cited.
- **Negated**: present, signed where required, fresh, schema-valid, and explicitly false. May be cited as false.
- **Unknown**: absent, unsigned, stale, unreachable, schema-invalid, malformed, expired, or otherwise unverifiable. MUST NOT be cited as true or false.

Unreachable is not the same as negated. A timeout, DNS failure, HTTP failure, blocked fetch, invalid JSON, missing field, failed signature check, stale `valid_until`, or schema mismatch means unknown.

Inference from Unknown to either Affirmed or Negated is a protocol violation. Agents SHOULD tell the user that the value could not be verified and fetch a fresh signed offer before making booking, availability, price, or official-source claims.

Examples: see [three-state-verification.md](../examples/three-state-verification.md).

## 10. Core JSON Schemas

VRP v0.1 core artifacts have machine-readable JSON Schemas:

```text
https://vacationrentalprotocol.com/schemas/discovery-v0.1.schema.json
https://vacationrentalprotocol.com/schemas/jwks-v0.1.schema.json
https://vacationrentalprotocol.com/schemas/verified-stay-offer-v0.1.schema.json
https://vacationrentalprotocol.com/schemas/verified-stay-offer-verification-result-v0.1.schema.json
```

Repository copies are in [`schemas/`](../schemas/). The schemas are
interoperability aids for implementers and examples. They do not create a
central validator, issuer, registry, certification service, marketplace, OTA,
booking intermediary, or trust authority.

## 11. Portable Attestations

Portable attestations are a document-only extension to VRP v0.1 for privacy-minimized trust history. Core VRP proves that a concrete offer is real; portable attestations prove selected host-domain, payment-path, policy, and optional verified-stay facts without making HemmaBo or any other operator the central issuer, registry, scorer, OTA, marketplace, booking intermediary, or trust authority.

See [attestations-v0.1.md](./attestations-v0.1.md).

## 12. Node Structure Declarations

Node structure declarations are a document-only extension to VRP v0.1 for falsifiable commercial-structure claims. Core VRP proves that a concrete offer is real; structure declarations state what kind of commercial thing the node is — marketplace or not, commission or none, gatekeeper or none — where every claim carries its own verification procedure that any party can execute against the live node, and claims a node can only vouch for itself are strictly separated from claims anyone can verify.

See [structure-declarations-v0.1.md](./structure-declarations-v0.1.md).

## 13. Reference Proof

Villa Åkerlyckan is a live proof node:

```text
villaakerlyckan.se
  -> /.well-known/vacation-rental.json
  -> /.well-known/jwks.json
  -> signed verified_stay_offer
  -> direct booking URL
```

HemmaBo is a reference implementation and provider/federation using VRP. VRP is neutral and implementable by others.

## 14. License

Specification text: dedicated to the public domain under [CC0 1.0](../LICENSE). Reference code and conformance test vectors: [Apache-2.0](../LICENSE-CODE) (ADR 0010 D7). Patents: a royalty-free patent non-assertion commitment is offered under the Open Web Foundation Agreement 1.0 (Patent-Only) — see [PATENTS.md](../PATENTS.md).

