> ## Documentation Index
> Fetch the complete documentation index at: https://docs.immutable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Use your own website

> Put the Funnel Page sign-up drawer on your existing marketing site with one script tag

Instead of sending traffic to a Play-hosted URL, you can embed the [Funnel Page](/docs/products/attribution/funnel-page) sign-up drawer directly into your own marketing site. A loader script mounts an overlay that opens on demand and carries the whole sign-up flow — email capture and one-time passcode (OTP) verification — without sending the visitor anywhere else.

## Prerequisite: a first-party subdomain

You delegate a subdomain to Immutable by CNAME (for example `landing.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.

<Warning>
  **The delegated subdomain and the page framing it must share a registrable domain** — the same apex, per the public suffix list. `landing.studio.com` and `www.studio.com` both resolve to `studio.com`, so they qualify; under a two-part public suffix the apex is one level deeper (`studio.co.uk`, not `co.uk`). Matching apexes are what make the browser treat the iframe as first-party, so the session started inside it survives.

  When they don't match, sign-up runs fine through email entry and the OTP step, then fails silently right after. Watch for preview hosts: `vercel.app` is itself a public suffix, so `my-app.vercel.app` is its own apex and fails the same way. Always test from a genuine subdomain of the apex you delegated.
</Warning>

## Install

```html theme={null}
<script src="https://landing.studio.com/v1/embed.js"></script>
```

Replace `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 for `DOMContentLoaded` — but mounts slightly later.
* Use a real `<script src="...">` reference, not an inline script. The loader reads its own origin from the tag, so without `src` it 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:

```
Content-Security-Policy: script-src 'self' https://landing.studio.com; frame-src 'self' https://landing.studio.com;
```

<Info>
  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.
</Info>

## Zero-JavaScript path

The minimum integration is the script tag plus one attribute — no JavaScript of your own:

```html theme={null}
<button data-imtbl-cta>Get early access</button>
```

or, with a fallback destination:

```html theme={null}
<a data-imtbl-cta href="/signup">Get early access</a>
```

Give every `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](#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

```js theme={null}
imtbl.embed.open();
imtbl.embed.close();
const unsubscribe = imtbl.embed.on(event, callback);
```

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="close()">
    Closes the drawer immediately: it goes invisible and click-through right away, without waiting on a reply from the frame.
  </Accordion>

  <Accordion title="on(event, callback)">
    Registers a handler for one of the [events](#events) below, and returns a function to unregister it. `ready` is sticky — if it already fired, your callback runs immediately and synchronously.
  </Accordion>
</AccordionGroup>

**Bootstrap queue for code that runs before the script loads.** The script loads asynchronously, so your own inline setup above it runs first. Queue calls against `window.imtbl` as an array and the loader drains them on init:

```html theme={null}
<script>
  window.imtbl = window.imtbl || [];
  window.imtbl.push(['on', 'ready', function () {
    // runs once the drawer is connected
  }]);
</script>
<script src="https://landing.studio.com/v1/embed.js"></script>
```

Each queued entry is `[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.

<Info>
  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.
</Info>

## Events

Register handlers with `imtbl.embed.on(event, callback)`.

| Event              | Fires when                                                                            | Payload                            |
| ------------------ | ------------------------------------------------------------------------------------- | ---------------------------------- |
| `ready`            | The drawer is connected and the API is usable. Fires once per page load; sticky.      | none                               |
| `open`             | The drawer becomes visible and interactive.                                           | none                               |
| `close`            | The drawer becomes hidden and click-through again.                                    | none                               |
| `signup:started`   | A visitor has submitted an email inside the drawer.                                   | none                               |
| `signup:completed` | The visitor has verified their OTP.                                                   | none                               |
| `error`            | The connection to the drawer failed and can't recover for the rest of this page load. | `{ code }` — see [Errors](#errors) |

<Info>
  `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.
</Info>

## Errors

| Code                | Meaning                                                                          | Typical cause                                                                                                          |
| ------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `handshake-timeout` | The drawer never answered the initial handshake.                                 | CSP missing your delegated subdomain, or framing from outside your apex.                                               |
| `version-mismatch`  | The drawer answered with a wire-protocol version this loader doesn't understand. | Rare and transient — a browser holding a cached loader from just before a protocol change. Resolves on the next fetch. |

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`, and `lang` are 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](#debugging) — whole-word matches for terms including `email`, `mail`, `phone`/`telephone`/`tel`, `fname`/`firstname`, `lname`/`lastname`/`surname`, `addr`/`address`, `passw`/`password`, `secret`, `ssn`, and `dob`/`birth`/`birthdate`. Matching is on separator and camelCase boundaries (`userEmail`, `user_email`, `firstname`), not raw substrings (`hotel_id` and `adobe_mc` are 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.

Only origin and path are forwarded as `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.

<Info>
  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).
</Info>

## Debugging

Add `data-mode="debug"` to the script tag for console warnings prefixed `[imtbl-embed]` — dropped attribution parameters, handshake failures, and other loader-side problems:

```html theme={null}
<script src="https://landing.studio.com/v1/embed.js" data-mode="debug"></script>
```

Remove the attribute once you're done; the console stays silent by default.

| Symptom                                                            | Likely cause                                                                                                                                                                               |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Nothing appears when you click the CTA, or the drawer never mounts | CSP — check `script-src` and `frame-src` include your delegated subdomain. If they do, confirm you're testing from a genuine subdomain of the apex you delegated.                          |
| Sign-up looks fine, then dies right after the OTP step             | The delegated subdomain and the framing page don't share a registrable domain — see [Prerequisite](#prerequisite-a-first-party-subdomain).                                                 |
| Clicking a `data-imtbl-cta` element does nothing                   | Check the console in debug mode for `handshake-timeout` or `version-mismatch`. Once either fires the loader stops intercepting clicks, so an element with no `href` fallback does nothing. |

## 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

<CardGroup cols={2}>
  <Card title="Conversion Goals" icon="bullseye" href="/docs/products/attribution/conversion-goals">
    Define which player actions count as a conversion
  </Card>

  <Card title="Engage" icon="heart-pulse" href="/docs/products/audience/engage">
    Nurture the signups you capture
  </Card>
</CardGroup>
