Skip to navigation

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.
  • A Vitable employee or employer ID for the signed-in user.
  • React 18 or later.

1. Install the SDK

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.

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"],
)

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:

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

PropTypeRequiredPurpose
baseUrlstringYesVitable widget server URL
fetchToken() => Promise<{ token: string; expiresIn: number }>YesFetches a bound session token from your backend
contextKeystringNoResets the session when the active employee or employer changes
allowedOriginsstring[]NoRestricts postMessage origins; defaults to the baseUrl origin
onError(code: string, message: string) => voidNoHandles errors from child widgets
themeVitableThemeNoCustomizes the embedded UI; see 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:

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.

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. Use the same object for every provider that renders Drops, including employer providers.

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

Theme Properties

PropertyTypeDescription
fontFamilystringCSS font family for all text
primaryColorstringKey into the colors object that identifies the brand palette
colorMode"light" | "dark" | "system"Color mode for the embedded UI
colorsRecord<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%"
logoUrlstringReserved. 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.

PaletteUsed 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.grayLight 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.darkDark 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.

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.