Inquiry embed

Embed the Folio inquiry in any web page with one script tag and window.FolioInquiry.mount.

Preview

This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.

The inquiry embed runs the Folio inquiry on any web page without npm, a bundler or React. Your page loads one module script from Folio and calls window.FolioInquiry.mount. The inquiry then runs in an iframe served from Folio's origin: the SDK, its WebAssembly module, the illustrations, the animations and the API requests all stay on that origin, and your page talks to the iframe only through postMessage to one pinned origin.

To run the inquiry inside your own React app instead, use the npm package: see Web setup.

How it works

  1. Your backend creates an inquiry and receives a one-time launch code. The embed never creates an inquiry.
  2. Your page loads the loader embed.v<major>.js. It registers window.FolioInquiry.
  3. FolioInquiry.mount(options) loads the embed runtime next to the loader, adds an iframe that shows <baseUrl>/inquiry.html and sends it your app metadata.
  4. The iframe starts the SDK and reports READY with its SDK version. The loader checks that version against the major version in its own file name.
  5. Only then does the loader send the entry with your launch code to the iframe. The inquiry runs, and your onLifecycle callback receives every lifecycle change.

Load the loader

index.html
<script type="module" src="<embed-loader-url>"></script>

<embed-loader-url> is the URL of the loader that Folio gives you; Folio does not publish one URL for everyone. The iframe accepts messages only from host origins Folio has configured for it, so ask Folio for the loader URL and give it every origin your pages run on, including development and staging origins. A page on an origin Folio has not configured fails with READY_TIMEOUT; see Launch code handling and security.

  • Load it as a module script: type="module" is required.
  • The loader finds the rest of the embed from its own URL. It fetches runtime-manifest.json from the directory it was loaded from and imports the runtime chunk that the manifest names. Load it from the URL Folio gives you, so that these files are next to it.
  • The v<major> in the file name is the major version the loader is pinned to. See Versions and caching.

Mount the inquiry

Call mount from a module script that comes after the loader, so that window.FolioInquiry exists when it runs:

index.html
<div id="folio-inquiry"></div>
<script type="module">
    const handle = window.FolioInquiry.mount({
        container: '#folio-inquiry',
        app: { version: '2.3.4', build: '567' },
        mode: 'built-in',
        backdrop: { color: 'rgba(0, 0, 0, 0.5)' },
        panel: { background: '#ffffff', shadow: '0 12px 48px rgba(0, 0, 0, 0.24)' },
        entry: { type: 'START', value: { launchCode: backendMintedLaunchCode } },
        onLifecycle(lifecycle) {
            if (lifecycle.type === 'FAILED') console.error(lifecycle.value.error);
        },
    });
</script>

backendMintedLaunchCode stands for the launch code your backend passed to the page.

Options

OptionTypeDefaultMeaning
containerHTMLElement | stringRequiredThe element to render into, or a CSS selector for it.
app{ version: string; build: string }RequiredYour app's version and build. Both must be non-empty after trimming. With your page's origin as the identifier they are the application identity the SDK in the iframe reports; see Application identity.
entryInquiryEntryRequiredThe inquiry to open. See Entry.
mode'modal' | 'built-in''modal''modal' shows the iframe in an overlay above the page. 'built-in' puts the iframe into container.
backdrop{ color: string }RequiredThe overlay behind the modal. color is any CSS color. Required in every mode; 'built-in' does not use it.
panel{ background: string; shadow: string }RequiredThe modal panel that holds the iframe. background is any CSS background and shadow any CSS box-shadow. Required in every mode; 'built-in' does not use it.
baseUrlstringThe directory the loader was loaded fromThe Folio location that serves the iframe document, the WebAssembly module, the illustrations, the animations and the API.
allowedOriginstringThe origin of baseUrlThe one origin the loader exchanges messages with. '*' is rejected.
onLifecycle(lifecycle: InquiryLifecycle) => voidNoneReceives every lifecycle the inquiry reports, and the embed's own failures.

backdrop.color, panel.background and panel.shadow must be non-empty after trimming. The embed has no defaults for them, so your page decides how the modal looks.

Leave baseUrl and allowedOrigin out unless Folio tells you otherwise: the defaults point at the location you loaded the loader from.

Entry

entry is the generated InquiryEntry in its JSON shape:

EntryMeaning
{ type: 'START', value: { launchCode } }Start the inquiry that your backend created.
{ type: 'RESUME', value: { source } }Continue an inquiry. source is a ResumeSource.

A source of { type: 'ANOTHER_DEVICE', value: { code } } continues an inquiry that was handed off from another device with its handoff code. Inquiry store describes the resume sources.

mount checks only that entry.type is START or RESUME. A blank launch code or handoff code, or a blank inquiryId in a SAME_DEVICE source, passes that check and then fails in the inquiry with INVALID_ENTRY.

Modes

ModeWhere the iframe goesSize
modalAn overlay element appended to container. The overlay is fixed to the viewport, covers it, has the background backdrop.color, sits at z-index 2147483000 and centers a panel with rounded corners that holds the iframe. The panel has the background panel.background and the shadow panel.shadow.The panel is at most 460 by 720 pixels and keeps 24 pixels to each edge of the viewport.
built-inThe iframe is appended to container directly.Full width of container. The height follows the content of the inquiry: the iframe reports it and the loader sets it.

A modal removes itself when the lifecycle reaches CLOSED. The loader adds no close button of its own: the user leaves through the inquiry's own close control. A built-in iframe stays in the page after CLOSED; call destroy() to remove it.

The mount handle

mount returns a handle right away, before the embed runtime has loaded:

MethodEffect
open()Sends the entry of the mount options to the inquiry again. mount already sends it once.
close()Closes the inquiry inside the iframe (InquiryAction.close). The inquiry reports CLOSED.
destroy()Removes the iframe, or the modal overlay with it, and the message listener. Pending messages are dropped. A second call does nothing.
  • Messages to the iframe wait until the iframe reports READY. open() and close() called in that time are delivered after READY.
  • open() and close() called before the embed runtime has loaded do nothing.
  • destroy() called before the embed runtime has loaded prevents the mount.
  • After destroy(), open() and close() do nothing. Call mount again for a new inquiry.

Lifecycle

onLifecycle receives the generated InquiryLifecycle, in the order the inquiry reports it:

lifecycle.typelifecycle.valueMeaning
READY{ sdkVersion: string | undefined }The SDK in the iframe has started. The loader checks sdkVersion against its pinned major; a pinned loader rejects a runtime without one.
STARTED{ correlationId: string }The inquiry is active, also while its terminal screen shows.
CLOSED{ outcome: TerminalOutcome | undefined }The inquiry was closed: by its own close control, by the terminal screen after its redirect, or by close(). outcome is the terminal result, or undefined when there is none.
FAILED{ error: InquiryError }The inquiry cannot continue. For an error of the inquiry the iframe shows the failure notice, and its close button leads to CLOSED. The embed's own failures (EMBED, BOOT_FAILED) end there.

outcome is one of COMPLETED, FAILED, ERROR, CANCELED, EXPIRED and NEEDS_REVIEW. The iframe reports a lifecycle only when it differs from the last one it reported.

The inquiry UI runs inside the iframe, so its links act from there. When the inquiry reaches its terminal screen and has finished loading, it closes the inquiry; when that screen has a redirect, the inquiry first navigates the iframe to the redirect URL with window.location.assign. Your page is not navigated. An EXTERNAL link of the inquiry opens in a new browser tab.

lifecycle.js
function onLifecycle(lifecycle) {
    switch (lifecycle.type) {
        case 'READY':
            console.log('Folio SDK', lifecycle.value.sdkVersion);
            break;
        case 'STARTED':
            console.log('inquiry', lifecycle.value.correlationId);
            break;
        case 'CLOSED':
            console.log('closed', lifecycle.value.outcome);
            break;
        case 'FAILED':
            console.error(lifecycle.value.error.type, lifecycle.value.error.value);
            break;
    }
}

Failures

The embed reports its own failures as a FAILED lifecycle with an InquiryError, like the failures of the inquiry itself:

const lifecycle: InquiryLifecycle = {
    type: 'FAILED',
    value: {
        error: {
            type: 'EMBED',
            value: {
                reason: { type: 'READY_TIMEOUT' },
                message: 'Inquiry runtime did not report READY in time',
            },
        },
    },
};
error.typeerror.valueReported when
EMBED{ reason, message }The embed failed; reason.type says how. See the next table.
BOOT_FAILED{ message }The embed runtime could not load: runtime-manifest.json failed, named an unexpected file, or the runtime chunk failed to load or register. Or the iframe failed to start the SDK, for example when the WebAssembly module failed to load.
INVALID_ENTRYNoneThe entry carried a blank launch code or handoff code, or a blank inquiryId in a SAME_DEVICE source.
RECORD_MAPPING{ message }Saving a verified identity failed: the identity record mapper threw, or an image of the identity could not be read. Nothing was saved.
FILE_TOO_LARGE{ limitBytes }An image of a verified identity is larger than limitBytes. Nothing was saved.

EMBED reasons:

reason.typeReported when
INVALID_OPTIONSmount rejected its options. message names the problem: app without a non-empty version and build, an entry that is not an InquiryEntry, an empty or missing backdrop.color, panel.background or panel.shadow, a container not found, no baseUrl to use, or an allowedOrigin that is '*' or cannot be derived. mount then adds nothing to the page.
READY_TIMEOUTThe iframe did not report READY within 15 seconds after it was added.
ORIGIN_REJECTEDA Folio inquiry message came from another origin or another window than the pinned iframe, or the iframe received one from another origin or window than your page. The message is ignored.
VERSION_MISMATCHThe major version of the SDK in the iframe differs from the major in the loader's file name, or the iframe reported no SDK version or one without a readable major. The entry is never sent. Load the loader of the matching major.

After VERSION_MISMATCH the loader still waits for READY, so a READY_TIMEOUT follows it.

The other InquiryError values come from the inquiry itself; see Errors.

Launch code handling and security

  • Your backend creates the inquiry. It passes the one-time launch code to the page. Mint a fresh launch code for each inquiry. Keep the credentials your backend uses with Folio out of the page.
  • The launch code travels in one message only. The loader sends it in the body of the OPEN message to the pinned origin, after the iframe has reported READY and passed the version check. It never puts the launch code in a URL, a query string, srcdoc or a message addressed to '*'. The first message to the iframe carries only baseUrl, the origins and your app metadata.
  • One pinned origin in each direction. The loader posts only to allowedOrigin and accepts messages only from that origin and from its own iframe's window. The iframe accepts its first message only from the host origin Folio configured for it and from its parent window, and later messages only from that same origin and window.
  • Your page's origin must be configured. The iframe ignores the first message from any other origin, so a page on an origin Folio has not configured gets no READY and fails with READY_TIMEOUT.
  • The runtime chunk name is checked. The loader imports only a file whose name matches runtime.<hex>.js from the manifest next to it.
  • The inquiry's data stays on Folio's origin. The SDK runs inside the iframe, so its storage and its API requests belong to Folio's origin, not to your page.
  • Camera and microphone. The iframe is created with allow="camera; microphone" so that document capture and selfie steps can use the camera. If your page sends a Permissions-Policy header, it must allow camera and microphone for the Folio origin, for example Permissions-Policy: camera=*, microphone=*.

Content Security Policy

If your page sends a Content Security Policy, allow what the embed loads from the Folio origin:

DirectiveAllowBecause
script-src<embed-origin>The loader module script and the runtime chunk it imports.
connect-src<embed-origin>The loader fetches runtime-manifest.json.
frame-src<embed-origin>The iframe with inquiry.html.

Replace <embed-origin> with the origin of the loader URL Folio gives you, and with the origin of baseUrl if you set it. The iframe document, its WebAssembly module, the illustrations, the animations and the API requests are loaded by the iframe on Folio's origin and are not governed by your page's policy.

Versions and caching

The embed carries the version of the Folio web SDK. The loader file name holds only its major version: SDK 0.x.y is served as embed.v0.js, 1.x.y as embed.v1.js.

  • Patch and minor releases replace the files under the same loader URL. Your page gets them without a change.
  • A major release gets a new loader file name. The loader of the previous major stays published for pages that reference it. To upgrade, change the src of your script tag after reading the release notes. Do not add a query string to the loader URL to upgrade.
  • The version check. The loader reads its pinned major from the first v<digits> in its URL and rejects an iframe SDK of another major, or one that reports no version, with VERSION_MISMATCH. A loader URL without such a part skips the check.

Caching of the published files:

FileCache-Control
embed.v<major>.jspublic, max-age=300
runtime-manifest.jsonpublic, max-age=300
runtime.<hash>.js, runtime.<hash>.csspublic, max-age=31536000, immutable
Other filespublic, max-age=300

The loader and the manifest are cached for 5 minutes, so a fix reaches your page within minutes. The content-hashed runtime files are cached for one year. Keep the loader URL stable; the loader resolves the content-hashed runtime chunk itself.

On this page