diff --git a/docs/website-widget.md b/docs/website-widget.md
new file mode 100644
index 0000000..a78f876
--- /dev/null
+++ b/docs/website-widget.md
@@ -0,0 +1,239 @@
+# Website Chat Widget — Operations Guide
+
+## Overview
+
+A floating chat/booking/contact widget that hospitals embed on their website via a single `
+```
+
+| Parameter | Description |
+|---|---|
+| `src` | The sidecar URL + `/widget.js` |
+| `data-key` | Site key — generated and rotatable from the admin portal |
+
+The widget renders in a **shadow DOM** — its styles don't leak into or get affected by the host website's CSS.
+
+---
+
+## Admin Configuration
+
+### Settings Page
+
+URL: `https://ramaiah.engage.healix360.net/settings/widget` (login as admin/supervisor)
+
+| Setting | Description |
+|---|---|
+| **Enabled** | Master kill switch — when off, widget.js no-ops |
+| **Site Key** | Read-only HMAC-signed key. Copy-to-clipboard for the embed snippet |
+| **Site ID** | Read-only identifier |
+| **Rotate Key** | Generates a new key, invalidates the old embed snippet |
+| **Hosting URL** | Public base URL for widget.js. Leave blank to use same origin as sidecar |
+| **Allowed Origins** | Whitelist of domains allowed to embed. Empty = any origin (test mode) |
+| **Show on Login Page** | Toggle to display widget on the Helix Engage login page |
+
+### Configuration API
+
+```bash
+# Read config (public — no auth)
+curl https://ramaiah.engage.healix360.net/api/config/widget
+
+# Read full config (admin)
+curl https://ramaiah.engage.healix360.net/api/config/widget/admin
+
+# Update config
+curl -X PUT https://ramaiah.engage.healix360.net/api/config/widget \
+ -H "Content-Type: application/json" \
+ -d '{"enabled": true, "allowedOrigins": ["https://ramaiahmedical.com"]}'
+
+# Rotate site key (invalidates old embeds)
+curl -X POST https://ramaiah.engage.healix360.net/api/config/widget/rotate-key
+```
+
+---
+
+## Widget Key Security
+
+- Key format: `{siteId}.{hmacSignature}`
+- HMAC computed as: `sha256(siteId, secret=WIDGET_SECRET env var)`
+- Validated with `timingSafeEqual()` to prevent timing attacks
+- Origin checked against `allowedOrigins` whitelist
+- Empty whitelist = test mode (any origin allowed)
+
+### For Production
+
+Before going live on a real hospital website:
+1. Set `allowedOrigins` to the hospital's domain(s): `["https://ramaiahmedical.com", "https://www.ramaiahmedical.com"]`
+2. Ensure `WIDGET_SECRET` env var is set in the sidecar (auto-generated on first run if missing)
+
+---
+
+## Widget API Endpoints
+
+All endpoints require `WidgetKeyGuard` (key as query param `?key=...` or header `X-Widget-Key`).
+
+| Endpoint | Method | Purpose |
+|---|---|---|
+| `/api/widget/init` | GET | Returns brand name, logo, colors, reCAPTCHA key |
+| `/api/widget/doctors` | GET | All doctors with departments, specialties, fees, visit slots |
+| `/api/widget/slots?doctorId=X&date=YYYY-MM-DD` | GET | Available time slots for a doctor on a date |
+| `/api/widget/book` | POST | Book appointment (requires captcha token) |
+| `/api/widget/lead` | POST | Create lead from contact form (requires captcha token) |
+| `/api/widget/chat-start` | POST | Start chat session — body: `{name, phone}`, returns `{leadId}` |
+| `/api/widget/chat` | POST | Stream AI reply — body: `{leadId, messages[], branch?}`, returns SSE stream |
+
+---
+
+## How the Chat Flow Works
+
+1. Visitor opens widget → Chat tab shows name + phone form
+2. Visitor enters name + phone → `POST /api/widget/chat-start` → returns `leadId`
+ - Creates or finds existing lead by phone (deduplication)
+3. Visitor types a message → `POST /api/widget/chat` with `leadId` + message history
+ - AI streams a reply via Server-Sent Events
+ - AI has tools: branch selection, doctor search, slot lookup, booking suggestions
+4. After conversation ends, transcript is saved to lead activity timeline
+5. CC agent sees the WhatsApp/chat history when calling the patient back
+
+---
+
+## How the Booking Flow Works
+
+1. Visitor opens widget → Book tab
+2. Selects department → fetches doctor list
+3. Selects doctor → fetches available dates/slots
+4. Enters patient name, phone, chief complaint
+5. `POST /api/widget/book` with captcha token
+6. Creates patient + lead + appointment in the platform
+7. Shows confirmation with reference number
+
+---
+
+## How Lead Capture Works
+
+1. Visitor opens widget → Contact tab
+2. Enters name, phone, interest, optional message
+3. `POST /api/widget/lead` with captcha token
+4. Creates lead with source "Website Widget"
+5. Shows success confirmation
+
+---
+
+## Lead Deduplication
+
+All three flows (chat, book, contact) use `CallerResolutionService` to find existing leads by phone number. If a visitor chats, then books, then contacts — all within 24 hours — they create ONE lead, not three. Activities are appended to the same lead.
+
+---
+
+## reCAPTCHA Protection
+
+- Booking and lead endpoints use reCAPTCHA v3 (invisible — no user friction)
+- Chat endpoints do NOT use reCAPTCHA (session already verified by name+phone gate)
+- reCAPTCHA site key returned by `/api/widget/init`
+- Server validates tokens via Google reCAPTCHA API
+- Captcha can be bypassed for webhooks using `captchaToken: "webhook-bypass"`
+
+---
+
+## Branding & Theming
+
+The widget pulls branding from the sidecar theme config:
+- **Hospital name** — displayed in the widget header
+- **Logo** — shown in the header
+- **Brand color** — applied to buttons, links, active states
+- **Location** — shown under the hospital name
+
+Configure via: `https://ramaiah.engage.healix360.net/settings` → Theme section
+
+---
+
+## Widget Source Code
+
+| Location | Description |
+|---|---|
+| `packages/widget-src/` | Preact source (chat.tsx, booking.tsx, contact.tsx, api.ts, etc.) |
+| `public/widget.js` | Compiled IIFE bundle (served by NestJS static assets) |
+| `src/widget/` | Backend API (controller, service, chat service, key guard) |
+| `src/config/widget-keys.service.ts` | Key generation + HMAC validation |
+| `src/config/widget-config.service.ts` | Config file management (data/widget.json) |
+
+### Rebuilding widget.js
+
+```bash
+cd packages/widget-src
+npm install
+npm run build
+# Output goes to ../../public/widget.js (configured in vite.config.ts)
+```
+
+After rebuilding:
+1. Commit `public/widget.js`
+2. Build Docker image and deploy sidecar
+3. Widget auto-updates on next page load (1h cache)
+
+---
+
+## Deployment Checklist
+
+### First-time setup on a new tenant:
+
+1. **Sidecar serves widget.js** — verify `https://{tenant}.engage.healix360.net/widget.js` returns JS, not HTML
+2. **Caddy routing** — `/widget.js` must route to sidecar, not frontend. Add to the `@api path` matcher in Caddyfile:
+ ```
+ @api path /api/* /widget.js /graphql /auth/* ...
+ ```
+3. **Widget config exists** — `GET /api/config/widget` should return `{enabled: true, key: "..."}`
+4. **Generate key if needed** — `POST /api/widget/keys/generate`
+5. **Set allowed origins** (for production) — `PUT /api/config/widget` with `allowedOrigins: ["https://hospital.com"]`
+6. **Test embed** — paste the `
+