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 (exp is 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.