@masheev/embed-sdk
v0.2.0
Published
Masheev Embed SDK — iframe, React hooks, and headless API for customer chat embedding
Maintainers
Readme
@masheev/embed-sdk
Embeddable chat widget for any website. Available as a script tag, React hook, or headless API with full control over the UI.
Installation
pnpm add @masheev/embed-sdkOr load via script tag (no build step):
<script src="https://unpkg.com/@masheev/embed-sdk"></script>Entry Points
| Import path | Use case |
|---|---|
| @masheev/embed-sdk/js | Vanilla JS — iframe widget with postMessage API |
| @masheev/embed-sdk/react | React — hook wrapper around the iframe widget |
| @masheev/embed-sdk/headless | React — direct WebSocket API, bring your own UI |
Quick Start
Script Tag
<script src="https://unpkg.com/@masheev/embed-sdk"></script>
<script>
Masheev.init({
inboxId: "your-inbox-id",
mode: "chat-widget",
position: "right",
agentName: "Support",
});
Masheev.on("message", function (msg) {
console.log(msg.role, msg.content);
});
</script>React (iframe)
import { useMasheev } from "@masheev/embed-sdk/react";
function App() {
const { open, isReady, on } = useMasheev({
inboxId: "your-inbox-id",
mode: "chat-widget",
position: "right",
agentName: "Support",
});
useEffect(() => {
const unsub = on("message", ({ role, content }) => {
console.log(role, content);
});
return unsub;
}, [on]);
return (
<button onClick={open} disabled={!isReady}>
Chat with us
</button>
);
}Headless (custom UI)
import { MasheevProvider, useMasheev } from "@masheev/embed-sdk/headless";
function Chat() {
const { messages, sendMessage, isLoading } = useMasheev();
return (
<div>
{messages.map((m) => (
<div key={m.id} className={m.role}>
{m.content}
{m.isLoading && <span>Typing...</span>}
</div>
))}
<input
onKeyDown={(e) => {
if (e.key === "Enter") {
sendMessage(e.currentTarget.value);
e.currentTarget.value = "";
}
}}
/>
</div>
);
}
function App() {
return (
<MasheevProvider
inboxId="your-inbox-id"
turnstileSiteKey="your-site-key"
customerInfo={{ name: "Jane", email: "[email protected]" }}
>
<Chat />
</MasheevProvider>
);
}JS SDK API
When loaded via script tag the global Masheev object exposes these methods. The same functions are available from @masheev/embed-sdk/js.
init(config)
Initialize the widget. Creates an iframe and attaches it to the page.
Masheev.init({
inboxId: "inbox_abc", // required
mode: "chat-widget", // "chat-widget" | "prompt-input" | "embedded"
position: "right", // "left" | "right"
baseUrl: "https://iframe.masheev.com",
placeholder: "Ask anything...",
agentName: "Support Bot",
agentTitle: "AI Assistant",
questions: ["How do I get started?", "Pricing?"],
privacyUrl: "https://example.com/privacy",
requireConsent: false, // true to show consent gate (regulated industries)
user: {
name: "Jane Doe",
email: "[email protected]",
phone: "+1234567890",
userId: "usr_123",
company: "Acme Inc",
customAttributes: { plan: "pro" },
},
});open() / close() / toggle()
Control widget visibility.
Masheev.open();
Masheev.close();
Masheev.toggle();sendMessage(text)
Send a message programmatically.
Masheev.sendMessage("I need help with billing");updateContext(context)
Update user context mid-conversation. This is a client-side only operation — the data is forwarded to the iframe and used for the current session, but it is not persisted to the database. Use updateContact when you need server-side persistence.
Masheev.updateContext({ name: "Jane Doe", email: "[email protected]" });updateContact(fields)
Update the contact record associated with the current widget session. Changes are persisted to the server immediately via PATCH /api/widget/contact. Only non-empty string fields are applied; omitted or empty fields are left unchanged on the server.
This is the method to use when your app learns new information about the user (e.g. after sign-up, profile update, or form submission) and you want the contact record in Masheev to stay in sync.
Masheev.updateContact({
name: "Jane Doe",
email: "[email protected]",
phone: "+14155551234",
company: "Acme Inc",
});Accepted fields:
| Field | Type | Max length | Notes |
|---|---|---|---|
| name | string | 255 chars | Trimmed before saving. |
| email | string | 255 chars | Trimmed before saving. |
| phone | string | — | Normalized to E.164 format on the server. Invalid numbers are silently ignored. |
| company | string | 255 chars | Trimmed before saving. |
| userId | string | — | Accepted by the SDK type signature but not currently persisted by the server. Use updateContext to pass userId to the session. |
Rate limiting: 5 requests per minute per session. Exceeding the limit returns 429 Too Many Requests.
Requires an active session. If called before the widget has established a session (i.e. the user has not yet started a conversation), the command is queued and sent once the iframe is ready — but the server call will only succeed once a session token exists.
updateContact vs updateContext
| | updateContact | updateContext |
|---|---|---|
| Persists to DB | Yes — writes to the contact table immediately | No — client-side only |
| Server call | PATCH /api/widget/contact | None |
| Use case | Syncing user profile data (post-login, form submit) | Passing ephemeral context to the AI (page URL, cart contents) |
| Fields | name, email, phone, company (+ userId in type) | All UserContext fields including customAttributes |
| Rate limited | Yes (5/min per session) | No |
How it works
- Your code calls
masheev.updateContact({ name: "Jane" }). - The SDK sends a
postMessageof type"updateContact"to the widget iframe. - The iframe handler receives the message and issues a
PATCHrequest to/api/widget/contactwith the session token as a Bearer header. - The API validates the session token (HMAC-signed, stateless), checks the rate limit, sanitizes the fields, and updates the
contactrow in the database. - The call is fire-and-forget from the SDK's perspective — errors are caught silently to avoid disrupting the host page.
setQuestions(questions)
Replace the list of suggested questions.
Masheev.setQuestions(["How does pricing work?", "Do you offer a free trial?"]);on(event, callback) / off(event, callback)
Subscribe to widget events. on() returns an unsubscribe function.
const unsub = Masheev.on("message", (msg) => {
console.log(msg.role, msg.content);
});
// later
unsub();isReady()
Returns true once the iframe has loaded and is accepting commands.
destroy()
Remove the iframe and clean up all listeners.
version
SDK version string (e.g. "0.2.0").
Configuration
MasheevConfig
| Property | Type | Default | Description |
|---|---|---|---|
| inboxId | string | — | Required. Inbox to connect to. |
| mode | WidgetMode | "chat-widget" | Display mode (see below). |
| position | "left" \| "right" | "right" | Bubble position (chat-widget mode only). |
| baseUrl | string | "https://iframe.masheev.com" | Widget iframe host. |
| placeholder | string | — | Input placeholder text. |
| agentName | string | — | Agent display name in the header. |
| agentTitle | string | — | Agent role / title. |
| questions | string[] | — | Suggested questions shown to the user. |
| privacyUrl | string | — | Link to your privacy policy. |
| requireConsent | boolean | false | Show a consent gate before the chat starts (see AI Disclosure & Consent). |
| hideHeader | boolean | false | Hide the chat header (embedded mode only). Useful when you provide your own header. |
| user | UserContext | — | Pre-fill user identity (see below). |
UserContext
| Property | Type | Description |
|---|---|---|
| name | string | User name |
| email | string | User email |
| phone | string | User phone |
| userId | string | Your external user ID |
| company | string | Company name |
| customAttributes | Record<string, string \| number \| boolean> | Arbitrary metadata |
Widget Modes
chat-widget (default)
Floating bubble in the corner. Starts at 100 x 100 px, expands to 460 x 750 px when opened. Position controlled by the position prop.
prompt-input
Fixed input bar at the bottom of the page. Full width, 100 px tall. Expands on interaction.
embedded
Fills 100% of its parent container. Use this when you want to place the widget inside a specific element rather than floating it over the page. The parent must have defined dimensions.
AI Disclosure & Consent
Masheev automatically handles AI disclosure to comply with the EU AI Act (Art. 50), US FTC deception rules, and California SB 1001. Every channel informs end-users that they are interacting with AI.
How Disclosure Works
When an AI agent is configured on an inbox, end-users are informed they are interacting with AI. This is handled per channel:
- Chat widget — disclosure is shown as a small text label before the conversation begins, plus a persistent "AI-powered · may make mistakes" notice below the input (similar to ChatGPT). The greeting message itself stays conversational.
- SMS / WhatsApp — disclosure is prepended to the first AI response in a new conversation
- Voice — disclosure is spoken as part of the first message when the call starts
Disclosure text is auto-generated from the AI agent's name and the organization name. It can be customized per channel in the AI Agent settings (Dashboard > AI Agents > Disclosure tab).
Custom templates support {{agentName}} and {{orgName}} variables.
Consent Gate
For regulated industries that require explicit data-processing consent before a conversation, set requireConsent: true:
Masheev.init({
inboxId: "inbox_abc",
requireConsent: true,
privacyUrl: "https://example.com/privacy",
});When enabled, the widget shows a consent gate before the chat UI. The user must check a consent checkbox and click "Start Chat" to proceed.
Consent is stored both client-side (localStorage, for UX persistence) and server-side on the contact record (for GDPR Art. 7(1) proof). The server stores the consent timestamp in the contact's consent.dataProcessing and consent.dataProcessingAt fields.
Session Endpoint (consent field)
When the widget sends consent data, it is included in the session creation request:
{
"inboxId": "string",
"visitorId": "string",
"consent": { "timestamp": "2025-01-15T10:30:00.000Z" }
}The server merges the consent into the contact's existing consent record without overwriting other consent fields.
Events
| Event | Payload | Fires when |
|---|---|---|
| ready | — | Widget iframe has loaded |
| open | — | Widget panel is opened |
| close | — | Widget panel is closed |
| message | { role: "user" \| "ai", content: string } | A message is sent or received |
| error | { message: string, code?: string } | An error occurs |
All event subscriptions are type-safe:
Masheev.on("message", (payload) => {
// payload is typed as { role: "user" | "ai"; content: string }
});React Hook API
useMasheev(config)
import { useMasheev } from "@masheev/embed-sdk/react";
const {
open, // () => void
close, // () => void
toggle, // () => void
sendMessage, // (text: string) => void
updateContact, // (fields: Partial<Pick<UserContext, "name"|"email"|"phone"|"userId"|"company">>) => void
on, // (event, callback) => () => void (returns unsubscribe)
off, // (event, callback) => void
containerRef, // (node: HTMLElement | null) => void (callback ref for embedded mode)
isReady, // boolean
} = useMasheev({ inboxId: "..." });The hook creates the SDK on mount and destroys it on unmount. It re-initialises when inboxId or baseUrl change.
Updating the contact from React
Call updateContact after a user action that reveals new identity information. The method is stable (wrapped in useCallback) and safe to call from effects or event handlers.
import { useMasheev } from "@masheev/embed-sdk/react";
function ProfilePage() {
const { updateContact } = useMasheev({
inboxId: "inbox_abc",
mode: "chat-widget",
});
async function handleProfileSave(form: FormData) {
await saveProfile(form); // your app logic
// Sync the updated info to the Masheev contact record
updateContact({
name: form.get("name") as string,
email: form.get("email") as string,
company: form.get("company") as string,
});
}
return <form onSubmit={(e) => { e.preventDefault(); handleProfileSave(new FormData(e.currentTarget)); }}>...</form>;
}Headless API
The headless entry point gives you direct access to the conversation over WebSocket. No iframe is created — you build the UI yourself.
<MasheevProvider>
Wrap your chat UI with the provider.
| Prop | Type | Default | Description |
|---|---|---|---|
| inboxId | string | — | Required. Inbox to connect to. |
| apiBase | string | "https://api.masheev.com" | API base URL. |
| turnstileSiteKey | string | — | Cloudflare Turnstile site key for bot protection. |
| customerInfo | { name?, email?, phone? } | — | Pre-fill customer identity. |
useMasheev() (headless)
Must be called inside <MasheevProvider>.
const {
// Session
session, // WidgetSession | null
isConnected, // boolean
connectionStatus, // "connecting" | "connected" | "disconnected" | "error" | "gave_up"
error, // string | null
// Messages
messages, // Message[]
sendMessage, // (content: string) => Promise<void>
isLoading, // boolean
// UI state
isOpen, // boolean
setOpen, // (open: boolean) => void
// Manual session creation
createSession, // () => Promise<WidgetSession | null>
} = useMasheev();Lower-level hooks
| Hook | Returns | Use case |
|---|---|---|
| useWidgetChat() | { messages, sendMessage, isLoading, error } | Chat operations only |
| useWidgetSession() | { session, error, isLoading, createSession } | Session management |
| useWidgetSocket() | { status } | Connection indicator |
Message
interface Message {
id: string;
role: "user" | "ai";
content: string;
timestamp: Date;
isLoading?: boolean; // true while the AI is streaming
}WidgetSession
interface WidgetSession {
sessionToken: string;
conversationId: string;
greeting: string;
}How It Works
Iframe SDK (JS / React)
init()creates a hidden<iframe>pointing tohttps://iframe.masheev.com/widget/{inboxId}.- Communication happens via
window.postMessagewith strict origin validation. - Commands issued before the iframe is ready are queued and flushed on the
readyevent.
Headless SDK
- A session is created lazily on the first
sendMessage()call (or explicitly viacreateSession()). - The provider opens a WebSocket to
wss://api.masheev.com/ws/conversation/{conversationId}. - Messages from the AI arrive in real-time over the socket.
- Reconnection uses exponential backoff (1 s initial, 30 s max, 10 attempts, 30% jitter).
- A ping is sent every 25 s to keep the connection alive.
Bot Protection
When turnstileSiteKey is provided (headless only), the SDK embeds an invisible Cloudflare Turnstile challenge. The token is sent with the session-creation request and validated server-side.
REST Endpoints (headless internals)
These are called internally by the headless provider. You only need them if you are building a non-React integration from scratch.
POST /api/widget/session
Create a chat session.
Request:
{
"inboxId": "string",
"visitorId": "string",
"customerInfo": { "name": "string", "email": "string", "phone": "string" },
"turnstileToken": "string",
"consent": { "timestamp": "ISO-8601 string" }
}The consent field is optional. When provided, the server records dataProcessing: true and the timestamp on the contact record for GDPR Art. 7(1) compliance.
Response:
{
"sessionToken": "string",
"conversationId": "string",
"greeting": "string",
"disclosure": "string",
"isResumed": false
}greeting— the agent's conversational greeting (e.g. "Hello! How can I help you today?").nullfor resumed sessions.disclosure— the AI disclosure text shown as a label before the chat begins (e.g. "You're chatting with Alex, an AI assistant for Acme Corp.").nullfor resumed sessions or when no AI agent is configured.isResumed—truewhen continuing an existing conversation.
POST /api/widget/message
Send a message. Requires Authorization: Bearer {sessionToken}.
Request:
{ "content": "string" }Response:
{ "content": "string" }PATCH /api/widget/contact
Update the contact record for the current session. Requires Authorization: Bearer {sessionToken}. Rate limited to 5 requests per minute per session.
Request:
{
"name": "Jane Doe",
"email": "[email protected]",
"phone": "+14155551234",
"company": "Acme Inc"
}All fields are optional. Only non-empty string values are applied. Phone numbers are normalized to E.164 on the server. String fields are trimmed and capped at 255 characters.
Response (200):
{ "success": true }Error responses:
| Status | Body | Cause |
|---|---|---|
| 400 | { "error": "No valid fields to update" } | All provided fields were empty or invalid |
| 401 | { "error": "Missing session token" } | No Authorization header |
| 401 | { "error": "Invalid or expired session" } | Token failed HMAC verification |
| 429 | { "error": "Too many requests" } | Rate limit exceeded (5/min per session) |
WebSocket /ws/conversation/{conversationId}
Query params: ?token={sessionToken}&source=widget
Receives JSON frames:
{
"type": "message.new",
"payload": {
"messageId": "string",
"content": "string",
"sender": { "type": "ai" }
}
}Constants
import { SDK_VERSION, DEFAULT_API_BASE, DEFAULT_WIDGET_BASE } from "@masheev/embed-sdk/js";
SDK_VERSION // "0.2.0"
DEFAULT_API_BASE // "https://api.masheev.com"
DEFAULT_WIDGET_BASE // "https://iframe.masheev.com"