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"]
}| Option | Effect |
|---|---|
bundles | Restricts the key to the listed capabilities; omission grants full partner access |
allowedEmbedParentOrigins | Exact parent origins for every session the key issues or renews |
allowedIpCidrs | Restricts 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"
}| Field | Rule |
|---|---|
sub | Your immutable external user ID; always the same value for the same person |
teamId | The managed team the embedded UI opens |
bundles | Read-only view capabilities; must be a subset of the API key's grant |
embedOrigin | Exact embedded-application origin; must be in the API's CORS policy |
parentOrigin | Exact parent-page origin; must match the token's declared policy |
viewPath | Reports URL starting with /reports, resolved against embedOrigin |
autoProvision | Whether 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 theGET /accounts,GET /listings, andGET /contactscollections used by report filtersembed:exports:v1: report exports, valid only together withembed: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:
| Situation | autoProvision: true | Omitted or false |
|---|---|---|
| Active membership exists | Session issued | Session issued |
| User or membership missing | Both created, session issued | 403 |
| Membership revoked | 403, never reactivated | 403 |
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
- Authenticate the user in your product.
- Authorize their access to the selected team.
- Request the smallest required bundles, both origins, and the exact view path.
- Keep
idfor revocation and returnembedUrl,channelId, andparentOriginto your frontend without logging the URL. - 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 canonicalteamId, invalid exactembedOriginorparentOrigin, anembedOriginoutside the API's CORS policy, or aviewPathoutside reports.401: missing, invalid, inactive, or expired partner key.403: the key cannot grant a requested bundle, the exchange comes from a different origin thanembedOrigin, 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.
