> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.vitablehealth.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.vitablehealth.com/_mcp/server.

# Set Up Vitable Drops

> Install the SDK, issue secure bound tokens, and configure the shared React provider.

Every Drop uses the same three-part setup: install the SDK, expose an authenticated backend endpoint that issues bound tokens, and configure `VitableConnectProvider` in your React application.

## Before You Begin

* A Vitable API key stored on your backend. See [Authentication](/getting-started/authentication).
* A Vitable employee or employer ID for the signed-in user.
* React 18 or later.

## 1. Install the SDK

```bash
npm install @vitable-inc/drops
```

## 2. Create a Server-Side Token Endpoint

Your backend must authenticate the caller, confirm that they may access the requested employee or employer, and exchange its API key for a bound access token.

```python
import os

import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

VITABLE_API_URL = os.environ["VITABLE_API_URL"]
VITABLE_API_KEY = os.environ["VITABLE_API_KEY"]


@app.post("/api/vitable/token")
def create_vitable_token():
    payload = request.get_json(silent=True) or {}
    entity_type = payload.get("entity_type")
    entity_id = payload.get("entity_id")

    if entity_type not in {"employee", "employer"} or not entity_id:
        return jsonify(error="A valid entity_type and entity_id are required"), 400

    # Authenticate the caller and authorize access to this entity here.

    response = requests.post(
        f"{VITABLE_API_URL}/v1/auth/access-tokens",
        headers={
            "Authorization": f"Bearer {VITABLE_API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "grant_type": "client_credentials",
            "bound_entity": {"type": entity_type, "id": entity_id},
        },
        timeout=10,
    )
    response.raise_for_status()

    token = response.json()
    return jsonify(
        token=token["access_token"],
        expiresIn=token["expires_in"],
    )
```

> **Warning**
>
> The authorization check in this example is intentionally application-specific. Do not issue a token based only on an ID supplied by the browser. Confirm that the signed-in user may access that employee or employer, and apply appropriate CORS, CSRF, and rate-limit controls.

## 3. Configure the Provider

The provider calls your backend on mount and again when the token needs to be refreshed:

```typescript
import { VitableConnectProvider } from "@vitable-inc/drops/react"
import type { AccessTokenResponse } from "@vitable-inc/drops/react"
import { useCallback } from "react"
import type { PropsWithChildren } from "react"

const VITABLE_WIDGET_URL = "https://app.vitablehealth.com"

function createTokenFetcher(
  entityType: "employee" | "employer",
  entityId: string,
): () => Promise<AccessTokenResponse> {
  return async () => {
    const response = await fetch("/api/vitable/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        entity_type: entityType,
        entity_id: entityId,
      }),
    })

    if (!response.ok) {
      throw new Error(`Token request failed: ${response.status}`)
    }

    return response.json()
  }
}

type EmployeeDropSessionProps = PropsWithChildren<{ employeeId: string }>

function EmployeeDropSession({ employeeId, children }: EmployeeDropSessionProps) {
  const fetchToken = useCallback(
    createTokenFetcher("employee", employeeId),
    [employeeId],
  )

  return (
    <VitableConnectProvider
      baseUrl={VITABLE_WIDGET_URL}
      fetchToken={fetchToken}
      contextKey={employeeId}
    >
      {children}
    </VitableConnectProvider>
  )
}
```

Use `employee` for the Employee Dashboard and `employer` for both employer widgets.

### Provider Props

| Prop             | Type                                                  | Required | Purpose                                                         |
| ---------------- | ----------------------------------------------------- | -------- | --------------------------------------------------------------- |
| `baseUrl`        | `string`                                              | Yes      | Vitable widget server URL                                       |
| `fetchToken`     | `() => Promise<{ token: string; expiresIn: number }>` | Yes      | Fetches a bound session token from your backend                 |
| `contextKey`     | `string`                                              | No       | Resets the session when the active employee or employer changes |
| `allowedOrigins` | `string[]`                                            | No       | Restricts postMessage origins; defaults to the `baseUrl` origin |
| `onError`        | `(code: string, message: string) => void`             | No       | Handles errors from child widgets                               |
| `theme`          | `VitableTheme`                                        | No       | Customizes the embedded UI; see [Theming](#theming)             |

## Token Lifecycle

The SDK fetches a token when the provider mounts, refreshes it before expiration, retries transient fetch failures with backoff, and sends updates to the iframe. Changing `contextKey` discards the existing session and requests a token for the new context.

Use a stable `contextKey` that matches the bound entity ID. Never reuse an employee-bound token for another employee or an employer-bound token for another employer.

## 4. Add a Widget

Continue with the component you need:

* [Employee Dashboard](/drops/employee-dashboard)
* [Employer Benefits](/drops/employer-benefits)
* [Employer Billing](/drops/employer-billing)

## Production Checklist

* Authenticate every token request and authorize the requested entity.
* Keep the Vitable API key in server-side secret storage.
* Allow postMessage communication only with the expected Vitable origin.
* Reset `contextKey` whenever the active employee or employer changes.
* Record token-fetch and widget errors without logging tokens.
* Use `@vitable-inc/drops` 0.1.6 or later so members can copy their ID card details in the Employee Dashboard. If your page sends a `Permissions-Policy` header or is itself embedded in a frame, allow `clipboard-write` for the Vitable origin.

## Theming

Every Drop supports theming through the `VitableConnectProvider`. Pass a `theme` prop to customize colors, fonts, and color mode so the embedded experience matches your application's look and feel.

```typescript
import type { VitableTheme } from "@vitable-inc/drops/core"

const theme: VitableTheme = {
  fontFamily: '"Inter", system-ui, sans-serif',
  primaryColor: "brand",
  colorMode: "light",
  colors: {
    brand: [
      "#eff6ff", "#dbeafe", "#bfdbfe", "#93c5fd", "#60a5fa",
      "#3b82f6", "#2563eb", "#1d4ed8", "#1e40af", "#1e3a8a"
    ],
    gray: [
      "#f8fafc", "#f1f5f9", "#e2e8f0", "#cbd5e1", "#94a3b8",
      "#64748b", "#475569", "#334155", "#1e293b", "#0f172a"
    ],
    dark: [
      "#cbd5e1", "#94a3b8", "#64748b", "#475569", "#334155",
      "#1e293b", "#172033", "#0f172a", "#0b1120", "#020617"
    ],
  },
}
```

Pass the theme to the provider from [step 3](#3-configure-the-provider). Use the same object for every provider that renders Drops, including employer providers.

```tsx
<VitableConnectProvider
  baseUrl={VITABLE_WIDGET_URL}
  fetchToken={fetchToken}
  contextKey={employeeId}
  theme={theme}
>
  {children}
</VitableConnectProvider>
```

### Theme Properties

| Property       | Type                            | Description                                                                               |
| -------------- | ------------------------------- | ----------------------------------------------------------------------------------------- |
| `fontFamily`   | `string`                        | CSS font family for all text                                                              |
| `primaryColor` | `string`                        | Key into the `colors` object that identifies the brand palette                            |
| `colorMode`    | `"light" \| "dark" \| "system"` | Color mode for the embedded UI                                                            |
| `colors`       | `Record<string, string[]>`      | Named color palettes. Each is a 10-element array of `#RRGGBB` colors, lightest to darkest |
| `fontSizes`    | `{ xs, sm, md, lg, xl }`        | CSS lengths for the type scale, such as `"14px"` or `"0.875rem"`                          |
| `lineHeights`  | `{ xs, sm, md, lg, xl }`        | CSS lengths for line heights, such as `"20px"` or `"150%"`                                |
| `logoUrl`      | `string`                        | Reserved. Must be an HTTPS URL; widgets don't display a logo yet                          |

### Color Palettes

Widgets read three palettes from `colors`. Other keys are ignored unless `primaryColor` names them.

| Palette                | Used for                                                                                                                                                             |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `colors[primaryColor]` | Buttons, links, focus rings, and accents. Light mode uses index 6 for primary actions and index 0 for accent backgrounds. Dark mode uses index 8 for primary actions |
| `colors.gray`          | Light mode surfaces: index 0 for backgrounds and cards, 1 for muted areas, 4 for borders and inputs, 6 for secondary text, and 9 for body text                       |
| `colors.dark`          | Dark mode surfaces: index 7 for backgrounds and cards, 6 for muted areas, 4 for borders and inputs, 2 for secondary text, and 0 for body text                        |

Any palette you leave out keeps Vitable's default colors for that role. If you use `colorMode: "dark"` or `"system"`, provide `colors.dark` so dark surfaces match your brand.

### Fonts

Widgets run in an iframe and can't use web fonts that are loaded only on your page. Name fonts that are installed on your users' devices, or end `fontFamily` with a fallback such as `system-ui, sans-serif`. `fontFamily` may contain only letters, numbers, spaces, hyphens, commas, and quotes; any other value is ignored.

> **Warning**
>
> The theme is validated together with the session token when a widget loads. An invalid theme stops the widget from loading, not only the theming. Check that every palette has exactly 10 six-digit hex colors (`#1d4ed8`, not `#fff` or `#1d4ed8ff`), that `primaryColor` is a key in `colors`, that `logoUrl` uses HTTPS, and that sizes are a number with a leading digit followed by `px`, `rem`, `em`, `%`, `pt`, `vh`, or `vw` (`"0.875rem"`, not `".875rem"`, `"1ch"`, or `calc(...)`).

Theme updates are applied in real time — if your app supports theme switching, the embedded widgets will update automatically when you change the `theme` prop.