Prerequisite: a first-party subdomain
You delegate a subdomain to Immutable by CNAME (for examplelanding.studio.com), and Immutable serves the drawer from it. You then add a <script> tag to your own marketing page (for example www.studio.com), which mounts an iframe pointing at your delegated subdomain.
Install
landing.studio.com with your own delegated subdomain.
- Place the tag right before
</body>so<body>already exists and the drawer can mount immediately. In<head>it also works — it waits forDOMContentLoaded— but mounts slightly later. - Use a real
<script src="...">reference, not an inline script. The loader reads its own origin from the tag, so withoutsrcit does nothing. - Paste the tag once per page. A duplicate is detected and ignored.
Content Security Policy
This is the most common cause of a broken integration.landing.studio.com is a different origin from www.studio.com — the browser treats subdomains as distinct origins even on a shared apex, so script-src 'self' and frame-src 'self' do not cover your delegated subdomain. Until both are updated, the script and iframe loads are silently blocked.
Add your delegated subdomain to both directives, merging into your existing policy rather than replacing it:
Immutable sets
frame-ancestors on its side, scoped to your delegated subdomain’s apex and any subdomain of it, so any page under your own domain can frame the drawer with no extra configuration.Zero-JavaScript path
The minimum integration is the script tag plus one attribute — no JavaScript of your own:data-imtbl-cta element a real href. If the connection to the drawer fails, the loader stops intercepting clicks and the element degrades to a plain link instead of doing nothing — see Errors.
The loader listens for clicks across the whole document, so elements added later by your CMS or client-side rendering work with no re-initialization. It does not intercept a click when:
- The click was already handled. If your own handler on the element or an ancestor called
preventDefault(), the loader leaves it alone. - It’s a modified or non-primary click. Cmd/Ctrl/Shift/Alt-click and any non-left-button click go to the browser, so
<a data-imtbl-cta href="/signup">still opens in a new tab on Cmd/Ctrl-click.
JavaScript API
open()
open()
Asks the drawer to open. This is intent only — the
open event fires once the drawer confirms it has rendered, not synchronously when you call this.close()
close()
Closes the drawer immediately: it goes invisible and click-through right away, without waiting on a reply from the frame.
on(event, callback)
on(event, callback)
Registers a handler for one of the events below, and returns a function to unregister it.
ready is sticky — if it already fired, your callback runs immediately and synchronously.window.imtbl as an array and the loader drains them on init:
[methodName, ...args], using the same methods as above. After the loader runs, window.imtbl.push([...]) keeps working as an alias for calling the method directly.
You don’t need to queue anything just to load the drawer — the loader mounts its iframe as soon as it runs, so attribution is captured even for a visitor who never opens it.
Events
Register handlers withimtbl.embed.on(event, callback).
signup:completed carries no email, because any third-party script on your page could observe the event. The verified address reaches you through an authenticated export instead.Errors
Either error is terminal for the rest of the page load: the drawer never becomes usable, and the loader stops intercepting clicks on
data-imtbl-cta elements. An <a> reverts to a plain link and navigates to its href; a <button> with no href does nothing.
Attribution and query parameters
The loader reads your page’s query string and forwards it into the drawer’s URL, so ad-network click IDs and UTM parameters survive into attribution. Everything forwards except:- Reserved keys.
parentOrigin,ref,href, andlangare populated by the loader from your page’s context, so a same-named query parameter is ignored. iframe, always. Its presence tells Immutable’s own scripts to skip initialization, so forwarding it would silently disable attribution.- Keys that look like personal data, with a warning in debug mode — whole-word matches for terms including
email,mail,phone/telephone/tel,fname/firstname,lname/lastname/surname,addr/address,passw/password,secret,ssn, anddob/birth/birthdate. Matching is on separator and camelCase boundaries (userEmail,user_email,firstname), not raw substrings (hotel_idandadobe_mcare not caught). - Any value containing
@, regardless of key name. - Any single value over 512 characters, plus anything beyond a 2048-character total budget across everything forwarded — once spent, remaining parameters are dropped even if each would individually pass.
href — your page’s query string and hash are stripped, so a value already dropped by the per-parameter scan can’t slip back in.
Empty values are dropped as carrying no signal, except
onboarding and linkStatus — presence alone is the signal there, so both ?onboarding and ?onboarding= forward (and a bare ?onboarding link opens the drawer automatically).Debugging
Adddata-mode="debug" to the script tag for console warnings prefixed [imtbl-embed] — dropped attribution parameters, handshake failures, and other loader-side problems:
Versioning and caching
The script is served at a permanent path,/v1/embed.js — a breaking change ships at /v2/embed.js rather than changing what /v1/ returns underneath an integration you’ve already shipped.
Within /v1/, the contents are intentionally mutable: fixes are patched in place so every studio’s integration gets them without editing their CMS. The tag therefore carries no Subresource Integrity (SRI) hash.
Responses are served with Cache-Control: public, max-age=300, stale-while-revalidate=86400.
Next steps
Conversion Goals
Define which player actions count as a conversion
Engage
Nurture the signups you capture