Verifying haven_ctx
An embed guidebook card loads your URL in a sandboxed iframe. When a guest has linked a stay (and passed any verified-stay gate you turned on), Haven appends a short-lived haven_ctx query parameter. Your app verifies it with the widget's signing secret and can skip its own login step.
This is not the public-API webhook scheme. The HMAC is keyed on the full whsec_… secret string, including the prefix.
Token
https://your-app.example/door?haven_ctx=<base64url(payload)>.<hex(hmac)>
- Construction:
base64url(JSON payload) + "." + hex(HMAC-SHA256(encoded, secret)) - TTL: 10 minutes (
expis a unix-ms timestamp) - The payload includes only the claims you checked in the embed editor. Haven never sends a default bag of guest data.
{
"v": 1,
"jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"exp": 1770000000000,
"spotId": "clxyz…",
"guestName": "Ada Lovelace",
"guestEmail": "ada@example.com",
"checkIn": "2026-09-12",
"checkOut": "2026-09-15",
"reservationRef": "11223344"
}
guestName, guestEmail, checkIn/checkOut, and reservationRef are present only when you allowed that claim and Haven had a non-null value.
Verify (Node / Next.js)
import crypto from 'crypto';
type HavenCtx = {
spotId: string;
jti: string;
guestName?: string;
guestEmail?: string;
checkIn?: string;
checkOut?: string;
reservationRef?: string;
};
export function verifyHavenCtx(
token: string | undefined,
secret: string,
nowMs = Date.now(),
): HavenCtx | null {
if (!token || !secret) return null;
const dot = token.lastIndexOf('.');
if (dot === -1) return null;
const encoded = token.slice(0, dot);
const signature = token.slice(dot + 1);
const expected = crypto
.createHmac('sha256', secret)
.update(encoded)
.digest('hex');
if (
signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return null;
}
const payload = JSON.parse(
Buffer.from(encoded, 'base64url').toString('utf8'),
) as { v: number; jti: string; exp: number; spotId: string } & HavenCtx;
if (payload.v !== 1) return null;
if (typeof payload.exp !== 'number' || nowMs > payload.exp) return null;
return payload;
}
Read haven_ctx from the request URL. Store the secret from the Haven editor after create or rotate — Haven shows the full value once.
The iframe is sandboxed (allow-scripts allow-forms allow-popups allow-same-origin) and cannot navigate the parent guidebook. Guests also get an "Open in new tab" escape hatch.