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

> ## Agent Instructions
> Authenticate with the raw API key in the Authorization header. Never add a Bearer prefix.
> Use the sandbox base URL https://api.test.doola.com and a dk_test_ key in examples unless the user asks for production.
> Send an Idempotency-Key header on POST /v1/partner/customers and POST /v1/partner/companies. No other endpoint takes one.
> Every response is wrapped as {payload, error}. Read fields from payload and branch on error.code, never on the message.
> Drive formation progress from webhooks plus a reconciliation sweep, never a polling loop.
> Embedded SDK (@doola/js): the dk_ key stays on the server and mints customer sessions; the browser gets only a pk_ key and the session. onFormed carries only companyId and can fire more than once. Before charging, read the company with the dk_ key, require formationSubmissionStatus AWAITING_PAYMENT, check the owner's email, and price on the server. Then POST payment-confirmed, which answers 200 with an empty body, and replace the SDK element with a new doola.create() (never append a second one).
> The doola Formation MCP server at https://mcp.doola.com is for founders forming their own company with their own doola account. Partners integrate the Partner API or the Embedded SDK. The /mcp endpoint on this docs site only searches these docs.
> For a full capability summary with the rules above, read https://docs.doola.com/skill.md.

# Embed the SDK

> Install @doola/js, mount the formation flow on your page, and keep it working across your framework's lifecycle.

Your page loads the SDK once, then mounts its element wherever the founder should see it. You need your publishable key (`pk_test_...` or `pk_live_...`) and a [session route](/sdk/sessions) on your server.

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @doola/js
  ```

  ```bash pnpm theme={null}
  pnpm add @doola/js
  ```

  ```bash yarn theme={null}
  yarn add @doola/js
  ```
</CodeGroup>

The package is under 1 KB gzipped, ships its TypeScript types, and works as ESM or CommonJS. It contains no UI: it loads doola's versioned loader from `js.doola.com`, which mounts the iframe.

## Mount

```ts theme={null}
import { loadDoola } from '@doola/js';

const doola = await loadDoola({
  publishableKey: 'pk_test_your_publishable_key',

  // Called on mount, before every expiry and on Try again. Always fetch a fresh session.
  fetchAccessToken: async () => {
    const r = await fetch('/doola-session', { method: 'POST' });
    if (!r.ok) {
      // Reject with the status and doola's error code: the loader maps them to error types.
      const { code } = await r.json().catch(() => ({}));
      throw Object.assign(new Error('doola session'), { status: r.status, code });
    }
    return r.json();
  },

  // Can fire more than once, so keep it idempotent.
  onAuthError: (error) => {
    if (error.type === 'partner_session_expired') location.assign('/login');
  },

  // The founder submitted the wizard, or asked to pay again. Open your checkout.
  onFormed: ({ companyId }) => openCheckout(companyId),
});

// <div id="doola"></div> on your page
document.getElementById('doola')!.append(doola.create());
```

* **`create()` takes no arguments.** The iframe shows the wizard, the payment screen or the dashboard depending on the customer's state.
* **The element is `<doola-embed>`,** a full-width block whose height follows its content.
* **Append it to mount, remove it to unmount.** To show a fresh state, such as the founder's company after payment, replace it with another `doola.create()`. Each call creates a separate iframe, so never append a second one beside it.

Every option is described in the [client reference](/sdk/reference/client).

## One instance per page

`loadDoola()` creates one SDK instance for the page, and **its options are fixed by the first call.**

* Calling `loadDoola()` again with the same publishable key returns the live instance and **ignores the new options**, callbacks included. React StrictMode's double invoke is therefore safe.
* Calling it with a different key rejects until you call `destroy()`.

So keep the instance outside your components, and route its callbacks to whichever screen is mounted now. One small module does both:

```ts doola.ts theme={null}
import { type Doola, loadDoola } from '@doola/js';

let formedHandler: (companyId: string) => void = () => {};
let instance: Promise<Doola> | undefined;

/** The screen that is mounted now decides what happens on submit. */
export function setFormedHandler(handler: (companyId: string) => void) {
  formedHandler = handler;
}

export function getDoola(): Promise<Doola> {
  instance ??= loadDoola({
    publishableKey: 'pk_test_your_publishable_key',
    fetchAccessToken: async () => {
      const r = await fetch('/doola-session', { method: 'POST' });
      if (!r.ok) {
        // Reject with the status and doola's error code: the loader maps them to error types.
        const { code } = await r.json().catch(() => ({}));
        throw Object.assign(new Error('doola session'), { status: r.status, code });
      }
      return r.json();
    },
    onAuthError: (error) => {
      if (error.type === 'partner_session_expired') location.assign('/login');
    },
    onFormed: ({ companyId }) => formedHandler(companyId),
  }).catch((error: unknown) => {
    instance = undefined; // let the next mount try again
    throw error;
  });

  return instance;
}

/** Call when your user signs out, never on an ordinary unmount. */
export async function signOutOfDoola() {
  const current = instance;
  instance = undefined;
  (await current?.catch(() => undefined))?.destroy();
}
```

## Frameworks

`loadDoola()` runs in the browser only and rejects on the server, so call it from a client-only path.

<Tabs>
  <Tab title="React">
    ```tsx DoolaFormation.tsx theme={null}
    import { useEffect, useRef, useState } from 'react';
    import { getDoola, setFormedHandler } from './doola';

    export function DoolaFormation({ onFormed }: { onFormed: (companyId: string) => void }) {
      const container = useRef<HTMLDivElement>(null);
      const [failed, setFailed] = useState(false);

      useEffect(() => setFormedHandler(onFormed), [onFormed]);

      useEffect(() => {
        let element: HTMLElement | undefined;
        let unmounted = false;

        getDoola()
          .then((doola) => {
            if (unmounted) return;

            element = doola.create();
            container.current?.append(element);
          })
          .catch(() => setFailed(true));

        return () => {
          unmounted = true;
          element?.remove();
        };
      }, []);

      if (failed) return <p>Company formation could not load. Check your connection and try again.</p>;

      return <div ref={container} />;
    }
    ```

    To show the founder's dashboard after you confirm payment, remount the component, for example by changing its `key`.
  </Tab>

  <Tab title="Next.js">
    Mark the component as client-only and render it like the React example. The session route is a [route handler](/sdk/sessions#nextjs-and-web-standard-runtimes).

    ```tsx app/formation/DoolaFormation.tsx theme={null}
    'use client';

    // The React component from the previous tab, unchanged.
    ```

    ```tsx app/formation/page.tsx theme={null}
    import { DoolaFormation } from './DoolaFormation';

    export default function FormationPage() {
      return <FormationScreen />; // a client component that renders <DoolaFormation onFormed={...} />
    }
    ```

    Never import `@doola/js` from a server component. It has nothing to do there, and `loadDoola()` rejects outside the browser.
  </Tab>

  <Tab title="Vue">
    ```vue DoolaFormation.vue theme={null}
    <script setup lang="ts">
    import { onBeforeUnmount, onMounted, ref } from 'vue';
    import { getDoola, setFormedHandler } from './doola';

    const emit = defineEmits<{ formed: [companyId: string] }>();
    const container = ref<HTMLDivElement>();
    const failed = ref(false);
    let element: HTMLElement | undefined;

    setFormedHandler((companyId) => emit('formed', companyId));

    onMounted(async () => {
      try {
        const doola = await getDoola();
        element = doola.create();
        container.value?.append(element);
      } catch {
        failed.value = true;
      }
    });

    onBeforeUnmount(() => element?.remove());
    </script>

    <template>
      <p v-if="failed">Company formation could not load. Check your connection and try again.</p>
      <div v-else ref="container" />
    </template>
    ```
  </Tab>
</Tabs>

## Sign out

Call `destroy()` when your user signs out of your product. It ends the doola session and removes every element, and the instance cannot be used again. To embed for the next user, call `loadDoola()` again.

Never call `destroy()` on an ordinary unmount or route change. Remove the element instead; the session stays alive for the next mount.

## Load faster

Start the connections early with two hints in your `<head>`:

```html theme={null}
<link rel="preconnect" href="https://js.doola.com" crossorigin />
<link rel="preconnect" href="https://sdk.doola.com" />
```

With test keys, point the second hint at `https://sdk.test.doola.com`.

## Content Security Policy

If your site sends a CSP, allow the loader and the iframe:

```text theme={null}
script-src https://js.doola.com;
frame-src https://sdk.doola.com;
```

With test keys, also allow `https://sdk.test.doola.com` in `frame-src`. If you enforce Trusted Types, allow the `doola-js` policy. See [Security and data](/sdk/security#content-security-policy).

## When it cannot load

`loadDoola()` rejects when the SDK cannot start, so your page shows its own fallback:

* It ran outside a browser, for example during server rendering.
* The loader script was blocked by a CSP, an ad blocker or the network.
* An option is invalid, such as a key that is not `pk_test_...` or `pk_live_...`. The message names it.

Once the iframe is running, it shows its own error screens. Pass `onLoadError` to log them, and see [Errors](/sdk/reference/errors) for the one case where your page is the only place to tell the founder.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.