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

# Developer guide

> Step-by-step guide to integrate @vizkraft/embed—backend session mint and frontend mount. Product managers can share this page with engineering.

export const DocsDemoVideo = ({category = 'cookbooks', slug, caption}) => {
  const base = `/public/recordings/${category}/${slug}`;
  return <figure className="not-prose my-8 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-800">
      {caption ? <figcaption className="border-b border-gray-200 bg-gray-50 px-4 py-2 text-center text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-400">
          {caption}
        </figcaption> : null}
      <video autoPlay muted loop playsInline className="w-full dark:hidden">
        <source src={`${base}/mist.webm`} type="video/webm" />
        <source src={`${base}/mist.mp4`} type="video/mp4" />
      </video>
      <video autoPlay muted loop playsInline className="hidden w-full dark:block">
        <source src={`${base}/carbon.webm`} type="video/webm" />
        <source src={`${base}/carbon.mp4`} type="video/mp4" />
      </video>
    </figure>;
};

<Note>
  **Product managers:** send this page to your developers. They only need an embed id, an API key, your Vizkraft base URL, and the liquid tag names you configured.
</Note>

This guide walks engineering through a complete integration of [@vizkraft/embed](https://www.npmjs.com/package/@vizkraft/embed). For why the API key stays on the server, read [Security](/embed/security) first.

Base URL in production: `https://app.vizkraft.com` · Product: [vizkraft.com](https://vizkraft.com)

## What you will build

1. A **backend** route that calls Vizkraft with your embed API key and tenant liquid values, then returns only a short-lived `sessionToken` to the browser.
2. A **frontend** page that installs `@vizkraft/embed` and mounts the dashboard with `getSessionToken`.

```mermaid theme={null}
flowchart TB
  FE[Frontend_SDK] -->|getSessionToken| BE[Your_backend]
  BE -->|Bearer_vk_embed_liquid| VK[Vizkraft_POST_session]
  VK -->|sessionToken| BE
  BE -->|sessionToken| FE
  FE -->|iframe_token| Viewer[Embed_viewer]
```

## Prerequisites from your PM

| Item           | Example                           | Notes                                       |
| -------------- | --------------------------------- | ------------------------------------------- |
| Base URL       | `https://app.vizkraft.com`        | Staging may differ                          |
| Embed id       | `emb_…`                           | Public; safe in frontend config             |
| API key        | `vk_embed_…`                      | **Server secret only**                      |
| Liquid tags    | `customer_id`, `department_id`, … | Values come from **your** auth at mint time |
| Allowed origin | `https://your-app.com`            | Must include the host that loads the SDK    |

## Step 1 — Install

```bash theme={null}
npm install @vizkraft/embed
```

Or with a CDN (HTML):

```html theme={null}
<script type="module">
  import { VizkraftEmbed } from 'https://cdn.jsdelivr.net/npm/@vizkraft/embed/+esm'
</script>
```

## Step 2 — Backend mint

Create a server route your frontend can call (cookie / session auth on **your** side). That route calls Vizkraft:

`POST {baseUrl}/api/embed/session`

Headers:

* `Authorization: Bearer <YOUR_EMBED_API_KEY>`
* `Content-Type: application/json`

Body:

```json theme={null}
{
  "embedId": "emb_...",
  "liquid": {
    "customer_id": "C1"
  }
}
```

Return JSON to the browser with **only** `sessionToken` (and optionally `expiresAt`). Never return the API key.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST 'https://app.vizkraft.com/api/embed/session' \
      -H 'Authorization: Bearer <YOUR_EMBED_API_KEY>' \
      -H 'Content-Type: application/json' \
      -d '{
      "embedId": "emb_...",
      "liquid": { "customer_id": "C1" }
    }'
    ```
  </Tab>

  <Tab title="Node.js">
    ```js theme={null}
    // Express example — keep the API key on the server only
    app.post('/api/vizkraft-embed-session', async (req, res) => {
      const customerId = req.user.customerId // from YOUR auth
      const r = await fetch('https://app.vizkraft.com/api/embed/session', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.VIZKRAFT_EMBED_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          embedId: process.env.VIZKRAFT_EMBED_ID,
          liquid: { customer_id: customerId },
        }),
      })
      const body = await r.json()
      if (!r.ok) return res.status(r.status).json(body)
      res.json({ sessionToken: body.sessionToken, expiresAt: body.expiresAt })
    })
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    # FastAPI example — keep the API key on the server only
    import os, httpx
    from fastapi import APIRouter, Depends

    router = APIRouter()

    @router.post("/api/vizkraft-embed-session")
    async def mint_embed_session(user=Depends(current_user)):
        async with httpx.AsyncClient() as client:
            r = await client.post(
                "https://app.vizkraft.com/api/embed/session",
                headers={
                    "Authorization": f"Bearer {os.environ['VIZKRAFT_EMBED_API_KEY']}",
                    "Content-Type": "application/json",
                },
                json={
                    "embedId": os.environ["VIZKRAFT_EMBED_ID"],
                    "liquid": {"customer_id": user.customer_id},
                },
            )
        data = r.json()
        if r.status_code >= 400:
            return JSONResponse(data, status_code=r.status_code)
        return {"sessionToken": data["sessionToken"], "expiresAt": data.get("expiresAt")}
    ```
  </Tab>
</Tabs>

## Step 3 — Frontend mount

Call your backend from `getSessionToken`. Pass `baseUrl` when you are not on the default production host.

<Tabs>
  <Tab title="JavaScript">
    ```ts theme={null}
    import { VizkraftEmbed } from '@vizkraft/embed'

    const handle = VizkraftEmbed.mount({
      container: '#analytics',
      embedId: 'emb_...',
      baseUrl: 'https://app.vizkraft.com',
      theme: 'dawn',
      height: '100%',
      getSessionToken: async () => {
        const r = await fetch('/api/vizkraft-embed-session', {
          method: 'POST',
          credentials: 'include',
        })
        if (!r.ok) throw new Error('Failed to mint embed session')
        const { sessionToken } = await r.json()
        return sessionToken
      },
      onReady: () => console.log('embed ready'),
      onError: (e) => console.error(e),
    })

    // later
    handle.unmount()
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={null}
    import { useEffect, useRef } from 'react'
    import { VizkraftEmbed } from '@vizkraft/embed'

    export function AnalyticsEmbed() {
      const ref = useRef<HTMLDivElement>(null)

      useEffect(() => {
        if (!ref.current) return
        const handle = VizkraftEmbed.mount({
          container: ref.current,
          embedId: 'emb_...',
          baseUrl: 'https://app.vizkraft.com',
          theme: 'dawn',
          height: '100%',
          getSessionToken: async () => {
            const r = await fetch('/api/vizkraft-embed-session', {
              method: 'POST',
              credentials: 'include',
            })
            if (!r.ok) throw new Error('Failed to mint embed session')
            const { sessionToken } = await r.json()
            return sessionToken
          },
        })
        return () => handle.unmount()
      }, [])

      return <div ref={ref} className="h-full min-h-[480px]" />
    }
    ```
  </Tab>

  <Tab title="HTML">
    ```html theme={null}
    <div id="analytics" style="height: 100%; min-height: 480px;"></div>
    <script type="module">
      import { VizkraftEmbed } from 'https://cdn.jsdelivr.net/npm/@vizkraft/embed/+esm'

      VizkraftEmbed.mount({
        container: '#analytics',
        embedId: 'emb_...',
        baseUrl: 'https://app.vizkraft.com',
        theme: 'dawn',
        height: '100%',
        getSessionToken: async () => {
          const r = await fetch('/api/vizkraft-embed-session', {
            method: 'POST',
            credentials: 'include',
          })
          if (!r.ok) throw new Error('Failed to mint embed session')
          const { sessionToken } = await r.json()
          return sessionToken
        },
      })
    </script>
    ```
  </Tab>
</Tabs>

### Height tip

Use `height: '100%'` when the host container is a flex/grid pane that should scroll **inside** the iframe. Percentage heights do not auto-grow from content. Pixel heights (or omitting height) allow the SDK to resize from viewer `vizkraft:resize` messages.

## Step 4 — Session refresh

You do **not** build refresh timers. The SDK calls `getSessionToken`:

* On mount
* Before session expiry (\~5 minutes)
* After auth errors from the viewer

Implement `getSessionToken` as a fresh mint each time (or a short cache on your backend if you prefer).

## Step 5 — Verify

1. `onReady` fires and charts load for tenant A.
2. Sign in as tenant B (or mint with different liquid) and confirm KPIs change—isolation works.
3. Confirm your app origin is listed under Allowed origins in Vizkraft.
4. Optional: walk through [Partner demo](/embed/partner-demo) on the same deployment.

<DocsDemoVideo category="embed" slug="sdk-mount" caption="Mounting @vizkraft/embed and loading the dashboard" />

## API quick reference

### `VizkraftEmbed.mount(options)`

| Option            | Type                                         | Description                                            |
| ----------------- | -------------------------------------------- | ------------------------------------------------------ |
| `container`       | `string \| HTMLElement`                      | CSS selector or element                                |
| `embedId`         | `string`                                     | `emb_…`                                                |
| `getSessionToken` | `() => Promise<string>`                      | Preferred; called on mount / refresh / auth error      |
| `sessionToken`    | `string`                                     | One-shot token (no refresh)—prefer `getSessionToken`   |
| `baseUrl`         | `string`                                     | Defaults to `https://app.vizkraft.com`                 |
| `theme`           | `dawn \| mist \| twilight \| dusk \| carbon` | First-paint chrome; admin settings win after bootstrap |
| `height`          | `number \| string`                           | px number, CSS string, or `'100%'` fill                |
| `onReady`         | `() => void`                                 | Viewer ready                                           |
| `onError`         | `(err) => void`                              | `{ code, message }`                                    |

### Handle methods

| Method            | Description                     |
| ----------------- | ------------------------------- |
| `unmount()`       | Tear down iframe and listeners  |
| `refresh()`       | Force token refresh + reload    |
| `setTheme(theme)` | Update chrome theme after mount |

### postMessage (origins checked)

| Type                | Direction     | Purpose                                   |
| ------------------- | ------------- | ----------------------------------------- |
| `vizkraft:ready`    | viewer → host | Loaded                                    |
| `vizkraft:resize`   | viewer → host | `{ height }` (ignored when height is `%`) |
| `vizkraft:error`    | viewer → host | auth / rate\_limit / missing\_liquid / …  |
| `vizkraft:setTheme` | host → viewer | `{ theme }`                               |

## Common errors

| Symptom                      | Likely cause                                   | Fix                                                                             |
| ---------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- |
| `missing_liquid` / 422       | Mandatory tag omitted at mint                  | Pass all mandatory liquid keys from your auth                                   |
| 401 / auth error             | Bad key, revoked key, or expired session       | Check server key; let SDK refresh via `getSessionToken`                         |
| Blank iframe / framing error | Origin not allowlisted                         | Add your host under Allowed origins                                             |
| Charts cut off / no scroll   | Host `overflow: hidden` + pixel-growing iframe | Use `height: '100%'` and a sized host pane                                      |
| Friendly chart empty state   | Query failed for that tile                     | Check coverage / liquid mapping in Vizkraft admin (raw errors show only in-app) |

## Related

* [Security](/embed/security) — API key + liquid model
* [Viewer options](/embed/viewer-options) — themes and chrome toggles
* [Admin setup](/embed/admin-setup) — where PMs create keys
* [Partner demo](/embed/partner-demo) — end-to-end sandbox
* npm: [@vizkraft/embed](https://www.npmjs.com/package/@vizkraft/embed)
