Skip to main content

Embed an assessment or its results

Purpose​

Put an Assessor.io assessment, or a campaign's published results, inside an iframe on your own website — so a respondent never leaves your page, and a programme summary sits inside your own report.

This is the third external-consumption path, beside the public API (v2) and the MCP server. It is the only one where the browser, not a token, is the thing being trusted, so most of this page is about who is allowed to frame what.

The two embeddable addresses​

SurfaceAddressWhat it is
Assessment/embed/a/{assessmentKey}The taker, chromeless and branded — the same assessment as /a/{assessmentKey}
Results/embed/results/{publicKey}The published-results widget for a campaign that has published

Nothing else under /embed/ is a framed surface. Anything else there is served frame-ancestors 'none' and will not load in a frame at all.

Who may frame it — the part that decides everything​

Framing is refused by default, and there is no setting on the page that changes that. The decision is a Content-Security-Policy: frame-ancestors response header, resolved per request in src/proxy.ts:

  1. Register your origin on the campaign. An origin is a scheme, host and port — https://example.org, not https://example.org/programme/ and not example.org. Ports matter, and so does https versus http.
  2. The registered list becomes the directive. An empty list is frame-ancestors 'none': an unregistered origin gets a browser refusal, not a login screen or an error page you can read.
  3. The results widget uses the PUBLISHING CAMPAIGN's list. Publishing results is not itself permission to frame them, so a widget is embeddable exactly where that campaign's assessments are.

If the frame is blank and DevTools reports a CSP violation, the origin in the message is the exact string that must be registered.

Narrowing with an embed token​

POST /api/v2/public/accounts/{accountId}/embed-tokens (scope embed:issue) mints a short-lived token for one assessment and one origin. Append it as ?t=<token>.

A token narrows the policy and never widens it. The campaign's allowlist is still consulted and the effective set is the intersection of the two, so:

  • a token cannot admit an origin the campaign has not registered — the minting route refuses to sign one, and the proxy is the second lock on that door;
  • a token issued for client A's page stops the same assessment being framed by client B, even though both origins are registered — which is the whole value;
  • an invalid or expired token leaves the open-tier policy in place rather than granting anything. It never turns a refusal into an admission.

The page-to-host contract (postMessage), protocol v2​

The framed page posts asr:* messages to its host so you can size the frame and react to progress. The contract lives at src/lib/embed/postMessage.ts.

Every message, both directions, is the same envelope:

{ source: 'asr', version: 2, type: string, payload?: object }

Check source and version before you read anything else. A host page is a busy place — analytics tags, chat widgets, video players and consent managers all post on the same window, several of them sending bare strings or objects with a type.

Frame to your page

TypePayloadMeaning
asr:readyassessmentKeyThe assessment is mounted and interactive.
asr:resizeheightContent height changed; size the iframe to it.
asr:progressansweredThe respondent advanced. A count, never answers.
asr:completesubmissionIdFinalised — but read the warning below.
asr:errorcodeTerminal failure, coarse code only.

Your page to the frame

TypePayloadMeaning
asr:hostlang (optional)Handshake. It tells the frame which origin to reply to.

Always check event.origin against the Assessor.io origin you framed before acting on a message.

<iframe id="asr" src="https://assessor.bimexcellence.org/embed/a/KEY?t=TOKEN"
style="width:100%;border:0" title="Assessment"></iframe>
<script>
const ASR_ORIGIN = 'https://assessor.bimexcellence.org';
window.addEventListener('message', (event) => {
if (event.origin !== ASR_ORIGIN) return;
const message = event.data;
if (!message || message.source !== 'asr' || message.version !== 2) return;
if (message.type === 'asr:resize') {
document.getElementById('asr').style.height = `${message.payload.height}px`;
}
});
</script>

If you use embed.js, you listen for DOM events instead​

The drop-in script at https://assessor.bimexcellence.org/embed.js injects the iframe, handshakes, resizes it for you, and re-dispatches everything else as a CustomEvent on your container — so the names above are the event names you listen for, and event.detail is the payload:

<div id="asr-embed"></div>
<script src="https://assessor.bimexcellence.org/embed.js"
data-assessment-key="ABCD123456"
data-target="#asr-embed"
data-lang="en"></script>
<script>
document.getElementById('asr-embed')
.addEventListener('asr:complete', (event) => {
console.log('submission', event.detail.submissionId);
});
</script>

What changes for existing integrations​

Protocol v1 said source: 'aio', version: 1 and named the same six events aio:ready, aio:resize, aio:progress, aio:complete, aio:error and aio:host. That spelling is deprecated — the platform's old three-letter shorthand now means something else inside BIMei — and it is removed on 2026-12-31. Until then nothing you have written stops working:

  • The frame sends every event twice, once as the v2 envelope and once as the v1 envelope it always sent. A listener filtering on the v1 source: 'aio' and version: 1 keeps receiving exactly what it received before, including resize and completion.
  • embed.js dispatches both DOM event names. A container listening for aio:complete still fires, and so does one listening for asr:complete. Each fires once per event, not twice — the script drops the duplicate itself.
  • The handshake is accepted in either spelling, so a page that hand-rolled an aio:host message, or one still running an older self-hosted copy of embed.js, needs no change today.

What to do before 2026-12-31: change your envelope filter to source === 'asr' and version === 2, and rename the aio:* event names in your listeners to their asr:* equivalents. There is nothing else — the payloads, the origin rules and the tokens are untouched.

After that date the frame sends only the v2 envelope, embed.js dispatches only the asr:* event names, and a v1 listener goes quiet.

asr:complete is not a completion record​

It runs on your page, which means anything on your page can send it. Treat it as a UX signal — swap the frame for a thank-you, advance your own wizard — and never as the fact that a submission exists.

The trustworthy completion channel is the webhook: signed, server-to-server, retried, and carrying a delivery id you can deduplicate on. See Webhooks for the payload and the signature.

Walk-up participation​

An embedded taker finalises into a campaign-bound assessment: a respondent who arrives through your page, with no invitation and no account, is bound to the campaign on submit rather than being refused. That is what makes the embed a data-collection surface rather than a preview.

  • Public API (v2) — tokens, scopes, webhooks, and the signature you verify a completion with
  • MCP Server — the same operations as agent tools