VRPlatformVRPlatform
Embed the Hosted UI

Issue Sessions

Issue one-time embedded bootstrap URLs without exposing partner secrets

Your backend needs one partner API token, then issues a short-lived session per iframe mount.

Create the Partner API Token

A signed-in admin of the partner team creates the token once through POST /api-tokens:

POST https://hostaway.api.vrplatform.app/api-tokens
Authorization: Bearer <federated-admin-session>
x-team-id: <hostaway-partner-team-id>
Content-Type: application/json
{
  "allowedEmbedParentOrigins": [
    "https://app.hostaway.com",
    "https://admin.hostaway.com"
  ],
  "allowedIpCidrs": ["203.0.113.10/32"],
  "bundles": ["partner:provisioning:v1", "embed:reports:v1", "embed:exports:v1"]
}
OptionEffect
bundlesRestricts the key to the listed capabilities; omission grants full partner access
allowedEmbedParentOriginsExact parent origins for every session the key issues or renews
allowedIpCidrsRestricts API-key calls to these public egress addresses

Store the returned apiToken only in your backend or secrets manager. Tokens follow the ordinary /api-tokens lifecycle, API-key-authenticated requests cannot create more keys, and issuance, renewal, and revocation all use the embedded-sessions:issue scope in partner:provisioning:v1.

allowedEmbedParentOrigins is optional for now; omission leaves parent origins unrestricted and an explicit empty list is rejected. Declare it on production tokens: a leaked key without one can bind sessions to any parent page, and the policy will become mandatory in a later release.

Request a Session

Call from your trusted backend when a signed-in user opens an embedded view:

POST https://hostaway.api.vrplatform.app/partner/embed-sessions
x-api-key: <partner-api-key>
Content-Type: application/json
{
  "autoProvision": true,
  "sub": "partner-user-123",
  "teamId": "4f8f94de-2cc0-4ec7-a7f8-f0c7f560e59a",
  "bundles": ["embed:reports:v1", "embed:exports:v1"],
  "embedOrigin": "https://hostaway.embed.vrplatform.app",
  "parentOrigin": "https://app.hostaway.com",
  "viewPath": "/reports/profit-and-loss?view=non-zero-rows"
}
FieldRule
subYour immutable external user ID; always the same value for the same person
teamIdThe managed team the embedded UI opens
bundlesRead-only view capabilities; must be a subset of the API key's grant
embedOriginExact embedded-application origin; must be in the API's CORS policy
parentOriginExact parent-page origin; must match the token's declared policy
viewPathReports URL starting with /reports, resolved against embedOrigin
autoProvisionWhether unknown users are created on first use; see below

Select both origins from trusted integration configuration, never from unchecked browser values.

Available bundles:

  • embed:reports:v1: GET /me, report reads, and the GET /accounts, GET /listings, and GET /contacts collections used by report filters
  • embed:exports:v1: report exports, valid only together with embed:reports:v1

The report bundle grants no account, listing, or contact detail routes, no CSV exports, and no mutations. Every read stays bound to the session's single teamId.

User Provisioning

Embedded identities are keyed by your sub, not by an email address. autoProvision decides what happens when that sub is not yet an active member of the selected team:

SituationautoProvision: trueOmitted or false
Active membership existsSession issuedSession issued
User or membership missingBoth created, session issued403
Membership revoked403, never reactivated403

Send autoProvision: true only after your own product has authorized the user for that team: it creates a real, persistent membership. A revoked (inactive) membership is never reactivated by provisioning; restore access explicitly first. Embedded users have no email address, so their GET /me returns user.email: null. Do not invent email addresses.

Response

{
  "id": "6dbfd424-3c66-450c-b11d-f5099b0e70f7",
  "channelId": "cc0528eb-8a9e-48ac-96fd-743101caac97",
  "parentOrigin": "https://app.hostaway.com",
  "embedUrl": "<embed-origin>/reports/trial-balance?frameAncestorToken=<policy>#code=<code>",
  "bootstrapExpiresAt": "2026-07-19T12:01:00.000+00:00",
  "sessionTtlSeconds": 900,
  "teamId": "4f8f94de-2cc0-4ec7-a7f8-f0c7f560e59a",
  "userId": "018f9c2b-7f4b-7d72-bf2e-9ab983f07018"
}

Mount embedUrl without extracting, moving, or logging either credential. Before serving the HTML, the embedded-application host resolves the short-lived frameAncestorToken through VRPlatform and emits Content-Security-Policy: frame-ancestors <parentOrigin>. It fails closed if the policy is missing, invalid, expired, already exchanged, or belongs to a different embed origin.

The fragment code succeeds once and expires at bootstrapExpiresAt. The iframe exchanges it for a bearer valid for sessionTtlSeconds and keeps it in memory; your backend and parent page never receive that bearer. The embedded app removes both bootstrap credentials from its visible URL before exchange. Keep id: it is the handle for renewal and revocation.

This makes the browser policy dynamic per mount. Each partner can select a configured parentOrigin for a session, while the issuing API token's allowedEmbedParentOrigins remains the upper bound. To add a production domain, create a replacement partner API token whose allowlist includes that exact origin, rotate the backend secret, then issue sessions with that parentOrigin. No embedded-UI rebuild or static CSP change is required.

Renewal

When the iframe reports that its bearer is expiring, request a successor without sending any grant fields:

POST /partner/embed-sessions/6dbfd424-3c66-450c-b11d-f5099b0e70f7/renewals HTTP/1.1
Host: hostaway.api.vrplatform.app
x-api-key: <partner-api-key>

The response has the same bootstrap shape with a new id and embedUrl and preserves the partner, user, team, bundles, scopes, both origins, channel ID, and original view path. The renewing key must still grant every preserved scope and, when it declares allowedEmbedParentOrigins, list the preserved parent origin. Send the complete successor URL through the dedicated lifecycle channel; the iframe exchanges its code without navigating.

Issuing a successor does not revoke the active session. A successful successor exchange activates the new session and revokes the predecessor in one transaction; concurrent successors are first-success-wins. A rejected or expired successor leaves the predecessor usable until its existing expiry.

Revocation

Keep the session id associated with the signed-in user. To invalidate a pending bootstrap or an exchanged bearer session:

DELETE /partner/embed-sessions/6dbfd424-3c66-450c-b11d-f5099b0e70f7 HTTP/1.1
Host: hostaway.api.vrplatform.app
x-api-key: <partner-api-key>
{
  "id": "6dbfd424-3c66-450c-b11d-f5099b0e70f7",
  "status": "deleted"
}

Every bearer request verifies that the session record is still active, so revocation rejects subsequent requests immediately. Revoke on logout, account switching, permission changes, and incident response; removing the iframe from the page only discards its in-memory token.

Backend Flow

  1. Authenticate the user in your product.
  2. Authorize their access to the selected team.
  3. Request the smallest required bundles, both origins, and the exact view path.
  4. Keep id for revocation and return embedUrl, channelId, and parentOrigin to your frontend without logging the URL.
  5. Mount promptly, then replace the current session through the renewal endpoint before bearer expiry.

Never return the partner API key. Do not accept a browser-supplied team ID without checking it against the signed-in user's access in your own product.

Failures

  • 400: invalid bundle combination, missing canonical teamId, invalid exact embedOrigin or parentOrigin, an embedOrigin outside the API's CORS policy, or a viewPath outside reports.
  • 401: missing, invalid, inactive, or expired partner key.
  • 403: the key cannot grant a requested bundle, the exchange comes from a different origin than embedOrigin, session creation or renewal violates the key's declared parent-origin policy, the embedded user is unknown or inactive, or the partner lacks access to the selected team.
  • 404: the selected team does not exist in the credential's scope, or a session ID is absent, expired, or belongs to another partner during renewal or revocation.

Correct the credential, user provisioning, bundle, or access assignment; do not retry with a different team automatically.

See the generated create-session, renew-session, and revoke-session contracts. The embedded host, not the partner page, uses the generated frame-ancestor-policy contract.

On this page