All docs pages

Contract: inbox host

Version 1.

Purpose

Profile owners answer their visitors from your site's owner dashboard (I-1). The dashboard shows the inbox in an iframe served from the platform, where your page's styles and scripts cannot reach the conversations. It gives the inbox a user token for the owner signed in on your site, so the owner signs in once, on your site, and never registers with the messenger (D-77).

The inbox shows the conversations of the profiles this owner has on your site and nothing of any other site (I-2). This contract fixes everything that crosses the iframe boundary: the iframe's address, the messages each side sends, and the checks on both sides. Nothing else crosses it.

The generated API schema does not cover this: it is a browser protocol between two documents, not an HTTP call.

Parties

Party Runs on
Host Your owner dashboard page, at one of the site's allowed origins (PC-1)
App The inbox, at the platform's inbox origin, inside an iframe the host creates

Transport: the iframe's address (host to app, when the iframe is created) and window.postMessage (both ways).

Opening the inbox

The host creates one iframe with this address:

{inboxUrl}?v=1&site={site}&host={host}
Parameter Required Value
v yes The protocol version, 1
site yes The site's publishable key, pk_…, the same key the widget uses
host yes The dashboard page's origin, window.location.origin, for example https://pro.spaguide.example. Scheme, host and port only
theme no The inbox's colours: light (the default), dark, or system to follow the owner's system setting. Pick the one that matches your dashboard; a light dashboard can leave it out

{inboxUrl} is the inbox address of the platform you integrate with, such as https://inbox.messenger.example. Give the iframe the title "Messages", so assistive technology names it.

From go-live, the platform serves the inbox only into a frame on one of the site's allowed origins: the inbox document carries Content-Security-Policy: frame-ancestors with those origins, and the platform refuses a host outside them. Until then any page can frame the inbox. Add your dashboard's origin to the site's allowed origins in the partner console now, so nothing changes for you on that day, and the inbox origin to frame-src in your dashboard's own Content Security Policy.

What the app does with the address:

  • A site that is no site's publishable key shows an error that says the inbox cannot open here. From go-live, so does a host the site does not allow. When the address itself is wrong, or the page around the frame is not at host, the app sends no messages and accepts none. When the platform refuses the site, the app has already sent its first token: after that it sends nothing, not even ready, and accepts no message.
  • A missing or unknown v shows the same error. Version 1 is the only one.
  • A missing or unknown theme means light. The inbox reads it when it opens; if your dashboard switches between light and dark, give the iframe its new address to change the inbox too.

Messages from the app to the host

Each message is a plain object with these fields:

Field Type Value
protocol string "mp-inbox"
version number 1
type string "ready" or "token"
Type Sent when The host must
ready Once per iframe, after the app shows its first view: the conversations, or a state such as "no profile on this site" or an error. Not sent while the app is still loading, and never when the inbox cannot open here Nothing. It may run its own follow-up
token The app needs to know which owner is signed in: when it starts, and when its session has ended or expired and it needs a new one Answer with one signin or signout (see "Signing in")

Messages carry nothing else.

Messages from the host to the app

Each message has the same protocol and version fields:

Type Fields Sent when The app must
signin token: string, an inbox user token (contracts/user-token.md, aud messenger-inbox) In answer to token when an owner is signed in on the dashboard, or unasked when another user signs in without loading a new page Start an owner session with the token, ending any session it had. A token that fails a check signs nobody in: the app says it could not sign the owner in, and the partner console shows why
signout none In answer to token when nobody is signed in, or unasked when the user signs out without loading a new page End the owner's session at once and show that nobody is signed in. No conversation stays on screen

The app accepts a message only when event.source is window.parent and event.origin equals the host parameter. Anything else is ignored without a reply.

Signing in

Your backend signs an inbox token for the user signed in on your dashboard (contracts/user-token.md): aud is messenger-inbox, and sub is the same user ID your profile sync sends as the profile's owner (contracts/profile-sync.md, "The owner"). The page needs little more than this:

const inboxUrl = "https://inbox.messenger.example";
const frame = document.createElement("iframe");
frame.title = "Messages";
frame.src = `${inboxUrl}?v=1&site=pk_live_…&host=${encodeURIComponent(location.origin)}`;
document.querySelector("#messages").append(frame);

async function answer() {
  const response = await fetch("/messenger/inbox-token", { method: "POST" });
  const token = response.ok ? await response.text() : "";
  const message = token
    ? { protocol: "mp-inbox", version: 1, type: "signin", token }
    : { protocol: "mp-inbox", version: 1, type: "signout" };
  frame.contentWindow.postMessage(message, inboxUrl);
}

window.addEventListener("message", (event) => {
  if (event.origin !== inboxUrl || event.source !== frame.contentWindow) return;
  const data = event.data;
  if (data?.protocol === "mp-inbox" && data.version === 1 && data.type === "token") answer();
});
  • Your token route. /messenger/inbox-token is a route on your own server that signs an inbox token for the user its session cookie names. Accept POST requests only and send no CORS headers, so a script on another site cannot read the token. Give your session cookie SameSite=Lax or SameSite=Strict. Sign the token only for a user signed in to your owner dashboard: it opens their conversations.
  • The dashboard's own origin. Serve your owner dashboard and this route on an origin of their own, such as https://pro.spaguide.example, with no third-party scripts, and answer the route only there. The browser cannot tell two pages of one origin apart: if your public pages share the dashboard's origin, any script on any of them can ask the route for the token of an owner who is browsing them.
  • When the user changes. After a sign-out or a sign-in that loads no new page, send signout, or signin with the new user's token, without waiting to be asked.
  • Answer every token. Send signout when nobody is signed in, or when your token route fails. The app waits 10 seconds for an answer and then says it could not sign the owner in. A later answer still counts.

No profile on this site

An owner session reaches the profiles on your site whose owner is the token's sub. The platform looks them up on every request and every live event, so a profile you give to another owner leaves the previous owner's inbox at once (D-74).

An owner session reaches at most 100 profiles. When one user owns more, the inbox shows the 100 whose IDs come first in byte order and leaves the rest out.

When no profile on your site names this user as its owner, the inbox does not open. It tells the user that no profile on your site lists them as its owner, and that your site decides who owns a profile. It shows no conversation. The same happens when an owner's last profile passes to someone else while the inbox is open. If an owner sees this by mistake, check that your profile sync sends their user ID as owner, exactly as your token sends it as sub.

Ordering, retries and idempotency

  • postMessage is fire and forget. Neither side acknowledges or retries.
  • ready comes at most once per iframe.
  • token may come before or after ready, and again whenever the app needs a new session. The host answers each one with one signin or signout. When an older answer is still on its way, send only the newest, so a slow answer from before a sign-out never signs the user back in.
  • signin and signout may come unasked at any time after the host created the iframe. A second signout in a row changes nothing. Each signin starts a new session.

Security

  • Host side. Accept a message only when event.origin equals the origin of inboxUrl and event.source is the iframe's contentWindow. Post only with targetOrigin set to that origin, never "*".
  • App side. The app posts only with targetOrigin set to the host parameter and accepts only messages from window.parent at that origin.
  • Framing. From go-live, only pages at the site's allowed origins can frame the inbox (frame-ancestors), so a page elsewhere cannot show it or trick an owner into clicking inside it. Until then any page can frame it.
  • What signin and signout can do. Any script on your dashboard page can send either message. signin signs nobody in by itself: the platform checks the token with your site's key and its audience, and a token for another site or for the widget fails (contracts/user-token.md). The token goes only to the iframe at the inbox origin, and the app sends it only to the platform's API, never back to the page.
  • What never crosses the boundary. Message text, visitors' names and identifiers, profile data, the owner session and its token, and unread counts stay inside the iframe. The only thing that crosses is the user token in signin, from the host to the app.
  • Storage. The app sets no cookies. It keeps the owner's session token in session storage, keyed by site, for one thing only: to end that session when the page loads again. Each load sends token and starts a new session. signout ends the session and removes the token. The platform ties each session to the one site it was created on.

Layout and keyboard

The inbox fills the iframe and scrolls inside it; it never asks the page to resize the frame. Give the iframe a fixed height, such as the viewport's height below your dashboard's header, and the width of your content area. The inbox works from 320 px wide: on a narrow frame it shows the conversation list or one conversation at a time, and on a wide one both side by side.

Tab moves into the iframe in the page's own order, and out of it again. The host handles no keys for the inbox.

Versioning

v in the address and version in each message carry the version. Each side ignores messages with another version. A later version may add parameters and message types without changing the ones above. A breaking change raises the version, and the platform keeps serving the old one while a live dashboard uses it.

Not in version 1: an unread count for your dashboard's own badge, opening one conversation from a link, and browser notifications, which browsers refuse to a cross-origin iframe.