Technical Documentation
Integration guide for developers and technical administrators. Covers widget embedding, API reference, analytics, EHR integration, policy rules, experiments, and sandbox testing.
Widget Integration
Add the patient chat assistant to your website with a single script tag. The widget renders a floating chat bubble that opens an inline chat panel.
<script src="https://app.healthcareagent.com/widget.js" data-api-url="https://app.healthcareagent.com" data-clinic="your-clinic-id" data-accent="#0f766e" data-position="right" data-greeting="Hi! How can we help you today?"> </script>
Place the script tag before the closing </body> tag. The widget self-initializes and does not depend on any external frameworks. Your clinic ID and the exact embed code are available in the admin dashboard under Widget.
Data Attributes
Configure the widget via data-* attributes on the script tag. Appearance settings can also be configured in the admin dashboard, which overrides these defaults.
Before the widget will work, you need to verify your domain in the admin dashboard under Domains. The widget authenticates automatically — no API key is needed in your embed code.
| Attribute | Default | Description |
|---|---|---|
data-api-url | — | Required. Healthcare Agent platform URL. |
data-clinic | — | Required. Your clinic identifier. Found in the admin dashboard under Widget. |
data-accent | #2563eb | Primary accent color (hex). Applied to the bubble, header, buttons, and focus rings. |
data-position | right | right or left. Which corner the floating bubble appears in. |
data-greeting | Hi! How can I help you today? | Initial message shown when the widget opens. |
Server-Side Config
On load, the widget fetches GET /widget/config from your server. Server-side settings override the data-* attributes unless the attribute is explicitly set on the script tag. This lets you manage widget appearance from the admin dashboard without changing your website code.
The /widget/config endpoint also handles experiment variant assignment, returning the correct configuration for the visitor's assigned variant. It accepts an optional vid query parameter (visitor ID) for deterministic assignment.
Appearance Options
All appearance settings can be configured via the admin dashboard under Widget.
| Option | Type | Default | Description |
|---|---|---|---|
accentColor | string | #2563eb | Primary color (hex, max 20 chars) |
position | string | right | left or right |
greeting | string | Hi! How can I help you today? | Initial greeting message (max 500 chars) |
placeholder | string | Type a message... | Input field placeholder (max 200 chars) |
disclaimer | string | This is an AI assistant and does not provide medical advice. | Disclaimer text shown at the bottom (max 500 chars) |
showDisclaimer | boolean | true | Whether to show the disclaimer footer |
showPhotoUpload | boolean | true | Whether to show the photo upload button (for insurance cards) |
headerTitle | string | Patient Assistant | Title displayed in the chat header (max 100 chars) |
bubbleSize | integer | 56 | Floating bubble diameter in pixels (40–80) |
panelWidth | integer | 380 | Chat panel width in pixels (300–500) |
borderRadius | integer | 16 | Panel corner radius in pixels (0–32) |
userBubbleColor | string | — | Override color for the user's message bubbles (hex) |
selectedColor | string | — | Color for selected states (defaults to accent if empty) |
confirmedColor | string | — | Color for confirmed states (defaults to accent if empty) |
successColor | string | — | Color for success states (defaults to accent if empty) |
Schedule Picker Config
When the agent searches for appointment slots, the widget renders an inline schedule picker. These options control its appearance:
| Option | Type | Default | Description |
|---|---|---|---|
slotPickerDaysVisible | integer | 7 | Number of days shown in the date strip (3–14) |
slotPickerTimeFormat | string | 12h | 12h or 24h time display |
slotPickerConfirmLabel | string | Confirm appointment | Text on the confirm button (max 100 chars) |
slotPickerEmptyLabel | string | No availability this day | Text shown when a day has no slots (max 100 chars) |
Card Capture Config
When the agent requests a payment card (via Stripe), the widget renders an inline card form. These options control its labels:
| Option | Type | Default | Description |
|---|---|---|---|
cardButtonColor | string | — | Override color for the card submit button (hex) |
cardSaveLabel | string | Save card | Button text for card-on-file captures (max 50 chars) |
cardPayLabel | string | Pay now | Button text for copay payment captures (max 50 chars) |
cardSuccessMessage | string | Card saved | Message shown after successful capture (max 100 chars) |
Responsive Behavior and Accessibility
- Mobile — On small screens, the chat panel expands to full-height with safe area support for notched devices.
- Image upload — Supports camera and file picker for JPEG/PNG images up to 4MB (used for insurance card photos).
- Markdown — Agent responses render bold, italic, lists, and tables.
- Keyboard navigation — Full focus management with visible focus rings (WCAG AA compliant).
- Session persistence — Session data is stored in the browser so conversations survive page navigation within the same tab. A persistent visitor ID is stored in the browser for experiment assignment.
- Typing indicator — Animated dots displayed while waiting for the agent response.
Chat API — Authentication
The Chat API supports two authentication methods:
1. Widget (automatic)
The embeddable widget authenticates automatically. No API key is needed in your embed code — just register your domains in the admin dashboard under Settings → Allowed Domains.
2. API key (server-to-server)
For direct API integrations from your backend, pass your API key via the X-API-Key header:
Your API key is available in the admin dashboard. Only use it in server-side code — never expose it in client-side JavaScript or HTML.
POST /v1/chat
Send a message to the agent and receive a response.
Request
curl -X POST https://agent.yourclinic.com/v1/chat \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{ "message": "Do you accept Blue Cross Blue Shield?", "sessionId": "550e8400-e29b-41d4-a716-446655440000" }'
Request body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Required* | The patient's message (max 10,000 chars). Required unless image is provided. |
sessionId | string | Optional | Session ID for conversation continuity (max 100 chars). Auto-generated if omitted. |
image | string | Optional | Base64 data URL (data:image/jpeg;base64,...). JPEG and PNG only, max 4MB. Used for insurance card photos. |
Response — 200 OK
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"response": "Yes, we accept Blue Cross Blue Shield PPO and HMO plans...",
// Present when the agent searched for appointment slots
"slotPicker": {
"slots": [
{ "id": "s_123", "providerId": "P1", "providerName": "Dr. Chen",
"start": "2026-03-20T09:00:00", "duration": 30 }
],
"queryContext": { ... }
},
// Present when the agent requested a payment card
"cardCapture": {
"type": "setup_intent",
"clientSecret": "seti_...",
"publishableKey": "pk_...",
"reason": "self_pay_card_on_file"
}
}
The slotPicker and cardCapture fields are only present when the agent performs the corresponding action during the conversation turn. The widget handles these automatically; if you are building a custom integration, you will need to render a slot picker or Stripe card form accordingly.
POST /v1/chat/stream
Streaming variant using Server-Sent Events (SSE). Same request body as POST /v1/chat.
curl -N -X POST https://agent.yourclinic.com/v1/chat/stream \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{"message": "What are your hours?"}'
SSE events
| Event | Data | Description |
|---|---|---|
session | { "sessionId": "..." } | Sent first. Contains the session ID for this conversation. |
done | { "response": "...", "slotPicker": ..., "cardCapture": ... } | Final event with the complete response and any widget data. |
GET /v1/chat/:sessionId
Returns the conversation transcript as an array of { role, content } objects. Image messages include hasImage: true.
curl https://agent.yourclinic.com/v1/chat/550e8400-e29b-41d4-a716-446655440000 \ -H "X-API-Key: YOUR_API_KEY"
DELETE /v1/chat/:sessionId
Ends the conversation, persists the session for admin review, and clears any patient verification state.
curl -X DELETE https://agent.yourclinic.com/v1/chat/550e8400-... \ -H "X-API-Key: YOUR_API_KEY"
POST /v1/chat/confirm-card
Called after a Stripe card form is completed to confirm the capture on the server side.
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | Required | Active session ID |
setupIntentId | string | One of | For card-on-file captures (Setup Intent) |
paymentIntentId | string | One of | For copay payment captures (Payment Intent) |
POST /v1/chat/fetch-slots
Used to load more appointment slots when the patient navigates to a different week in the schedule picker.
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | Required | Active session ID |
startDate | string | Required | Start date for the slot search (YYYY-MM-DD) |
endDate | string | Required | End date for the slot search (YYYY-MM-DD) |
Session Management
Sessions expire after a period of inactivity. Each message or PHI access refreshes the timer. Key behaviors:
- Session IDs are auto-generated if not provided in the first
POST /v1/chatcall. - Omit
sessionIdto start a new conversation; include it to continue an existing one. - Expired sessions are persisted and available for review in the admin dashboard.
- Patient verification state is tied to the session and cleared when it ends.
- Sessions include security protections against hijacking and replay.
Chat endpoints are rate limited per IP. Exceeding the limit returns a 429 response with the RATE_LIMITED error code.
Forms API — POST /v1/forms/start
Start a conversational form session. The form engine drives the AI to collect data field-by-field in natural language, with branching logic, validation, and dynamic data sources.
curl -X POST https://agent.yourclinic.com/v1/forms/start \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{"schemaId": "patient-intake"}'
You can either reference a pre-registered schema by schemaId, or provide an inline schema object. The response includes sessionId, formId, formName, and an initial greeting response.
POST /v1/forms/message
curl -X POST https://agent.yourclinic.com/v1/forms/message \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY" \ -d '{"sessionId": "abc-123...", "message": "John Smith"}'
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | Required | Form session ID from /forms/start |
message | string | Required | The user's response to the current form prompt |
Returns response (next question), completed flag, and result (action outcomes) when the form is done. Rate limited per session.
GET /v1/forms/:sessionId
Returns formId, completed, fieldsCollected (field IDs only, not values), and progress (filled/total/percent). Form values are never exposed via this endpoint to protect patient data.
GET /v1/forms/schemas
Returns an array of registered schemas with id, name, and description.
Form Schema Format
Forms use a declarative JSON schema with steps, fields, conditions, data sources, and submit actions.
{
"id": "patient-intake",
"name": "New Patient Intake",
"steps": [
{
"id": "demographics",
"title": "Personal Information",
"fields": [
{ "id": "first_name", "type": "text", "label": "First name", "required": true },
{ "id": "email", "type": "email", "label": "Email", "required": true,
"validations": [{ "rule": "email" }] }
]
},
{
"id": "insurance",
"title": "Insurance Details",
"condition": { "field": "has_insurance", "equals": true },
"fields": [ ... ]
}
],
"onSubmit": [
{ "type": "register_patient", "params": { "firstName": "{{ first_name }}" } }
]
}
Top-level fields
| Key | Type | Description |
|---|---|---|
id | string | Unique form identifier |
name | string | Display name |
steps | Step[] | Ordered list of form steps |
onSubmit | Action[] | Actions to run after the final step |
branding | object | Optional: accent, logo, clinicName for white-label |
Step properties
Each step has: id, title, description, fields[], condition (skip step if not met), dataSources[] (fetch dynamic data at step entry), goto (static next step), branches[] (conditional next step).
Conditions
// Simple equality { "field": "has_insurance", "equals": true } // Compound (AND) { "and": [ { "field": "age", "gt": 18 }, { "field": "reason", "equals": "physical" } ] } // Set membership { "field": "insurance.payer", "in": ["BCBS", "Aetna"] }
Supported condition operators: equals, not_equals, in, not_in, exists, gt, lt, gte, lte, contains, and, or, not.
Data sources
Steps can declare dataSources that fetch dynamic data at step entry. Results populate field options or are available in templates.
| Source Type | Description |
|---|---|
providers | Fetch available providers from the EHR |
slots | Fetch appointment slots (requires provider/type params) |
insurance_check | Run eligibility check |
clinic_hours | Load clinic operating hours |
locations | Fetch clinic locations/departments |
eligibility | Check insurance eligibility |
custom | Fetch from a custom URL |
Submit actions
| Action Type | Description |
|---|---|
register_patient | Register a new patient in the EHR |
book_appointment | Book an appointment |
check_eligibility | Run insurance eligibility check |
send_email | Send a confirmation email |
send_otp | Send a verification code |
verify_otp | Verify a submitted code |
webhook | POST form data to an external URL |
set_field | Set a field value programmatically |
Each action supports a condition (only run if met) and params with {{ template }} references to collected values.
Field Types
| Type | Description |
|---|---|
text | Free-form text input |
email | Email with format validation |
phone | Phone number with format validation |
date | Date in MM/DD/YYYY format |
number | Numeric input with optional min/max |
select | Single-select from options list |
multiselect | Multiple selection from options |
radio | Radio button group |
checkbox | Boolean checkbox |
provider_select | Provider picker (populated from EHR data source) |
slot_select | Appointment slot picker |
location_select | Clinic location picker |
info | Display-only text block (not collected) |
hidden | Hidden field with preset value |
Validation Rules
| Rule | Parameter | Description |
|---|---|---|
required | — | Field must have a non-empty value |
email | — | Must match email format |
phone | — | Must be a valid phone number (7+ digits) |
date | — | Must match MM/DD/YYYY format |
min_length | number | Minimum character count |
max_length | number | Maximum character count |
min | number | Minimum numeric value |
max | number | Maximum numeric value |
pattern | regex string | Value must match the regular expression |
Each validation can include a custom message string. If omitted, a sensible default is used (e.g., "Invalid email address").
Analytics Events
The agent emits 13 structured analytics events at key points in the conversation lifecycle. Events can be routed to multiple destinations. Configure analytics in the admin dashboard under Analytics.
Event Catalog
| Event | Trigger | Payload Fields |
|---|---|---|
chat_loaded | Widget loads on page | timestamp, visitorId, sessionId, pageUrl, referrer |
chat_opened | Visitor opens the widget | timestamp, visitorId, sessionId |
chat_closed | Visitor closes the widget | timestamp, visitorId, sessionId, messageCount |
message_sent | Patient sends a message | timestamp, visitorId, sessionId, messageLength, hasImage |
message_received | Agent responds | timestamp, visitorId, sessionId, responseLength |
slots_shown | Available slots displayed to patient | timestamp, visitorId, sessionId, slotCount, dateRangeStart, dateRangeEnd, timeToNextSlotMs |
slot_selected | Patient picks an appointment slot | timestamp, visitorId, sessionId, selectedTime, providerName, providerId |
slot_confirmed | Patient confirms slot selection | timestamp, visitorId, sessionId, selectedTime, providerName, appointmentDate |
date_range_changed | Patient navigates to different dates | timestamp, visitorId, sessionId, newStartDate, newEndDate |
card_saved | Stripe card saved successfully | timestamp, visitorId, sessionId, last4, brand |
booking_confirmed | Booking confirmed in EHR | timestamp, sessionId, appointmentId, patientId* |
escalated | Conversation escalated to staff | timestamp, sessionId |
verification_completed | Patient OTP verified successfully | timestamp, sessionId, patientId* |
* Fields marked with * are disabled by default. See PHI Warnings below.
Setting Up GTM dataLayer
Enable the GTM destination in your analytics config. The widget pushes events to window.dataLayer automatically — no additional JavaScript required.
{
"enabled": true,
"destinations": {
"gtmDataLayer": { "enabled": true }
}
}
Events appear in GTM as custom events with the event name matching the names listed above (e.g., chat_opened, booking_confirmed). All payload fields are included as event parameters.
GTM must already be installed on the page that hosts the widget. The widget pushes to window.dataLayer — it does not install GTM for you.
Setting Up postMessage
For iframe embeds or cross-origin setups, the widget can post events to the parent window via window.parent.postMessage().
{
"destinations": {
"postMessage": { "enabled": true, "targetOrigin": "https://www.yourclinic.com" }
}
}
Listen for events in your parent page:
window.addEventListener('message', (event) => { if (event.data && event.data.type === 'hca_event') { console.log(event.data.event, event.data.payload); } });
Set targetOrigin to your domain for security. Use "*" only during development.
Setting Up Webhooks
The server can POST analytics events to a webhook URL. Events are sent as JSON with an HMAC signature for verification.
{
"destinations": {
"webhook": {
"enabled": true,
"url": "https://hooks.yourclinic.com/analytics",
"secret": "YOUR_WEBHOOK_SECRET"
}
}
}
See HMAC Verification for how to verify the signature.
Per-Event Field Toggles
Each event has individually toggleable fields. This lets you control exactly which data points are sent to each destination.
{
"events": {
"booking_confirmed": {
"enabled": true,
"fields": {
"timestamp": true,
"sessionId": true,
"appointmentId": true,
"patientId": false // PHI — disabled by default
}
}
}
}
Set an event's enabled to false to stop emitting it entirely. Set individual fields to false to exclude them from the payload.
PHI Field Warnings
The following fields may contain Protected Health Information (PHI) and are disabled by default:
booking_confirmed.patientIdverification_completed.patientId
If you enable these fields, ensure your webhook endpoint and analytics destination are HIPAA-compliant. The audit log always retains the full event regardless of field settings.
Experiment Context in Events
When an experiment is running, all events for sessions in the experiment include two additional fields:
experimentId— The experiment's unique identifiervariantId— The variant assigned to this visitor
In sandbox mode, events also include sandbox: true. This enables per-variant analytics without post-hoc data joins.
EHR Integration — Supported Systems
| System | API Type | Description |
|---|---|---|
| Healthie | API integration | Full integration including patient registration, scheduling, insurance, and lab results. |
| athenahealth | API integration | Full integration covering patient search, scheduling, eligibility, and lab results. |
Getting API Credentials
Healthie
- Log in to your Healthie admin account.
- Navigate to Settings → API Keys.
- Generate a new API key. Copy the key value.
- Note your practice ID (visible in the URL or settings).
athenahealth
- Register at the athenahealth Developer Portal.
- Create a new application to get a Client ID and Client Secret.
- Note your Practice ID from your athenahealth account.
- The token URL and base URL default to athenahealth's production endpoints unless overridden.
Connecting via the Dashboard
Go to Integrations in the admin dashboard, select your EHR system, and enter your credentials. Use the Test Connection button to verify before saving.
Credentials are stored encrypted at rest using AES-256-GCM. Separate credentials can be configured for production and sandbox environments.
What Data Syncs Automatically
After connecting your EHR, use the Sync from EHR button to pull:
- Providers — Name, credentials, specialty, languages, schedule, accepted insurance panels
- Appointment types — Name, duration, patient type (new/established), contact types, pricing
- Insurance — Accepted payers with plan types
- Services — Service catalog with telehealth availability
- Locations — Department/office names, addresses, phone numbers
- Policies — Cancellation window, booking horizon, same-day rules
Patient Verification (OTP)
All patient-specific data access requires two-step OTP verification. This is enforced at the server level — the AI cannot bypass it.
Flow
- Patient provides full legal name and date of birth in conversation.
- Agent initiates verification — the system sends a verification code to the email address on file in the EHR.
- Patient provides the code in the chat.
- Agent verifies the code — verification state is set on the session.
The verification response is always identical regardless of whether the patient was found. This prevents patient enumeration attacks. Verification is time-limited (refreshed on each PHI access). Verification attempts are rate limited per session.
Available EHR Tools
These are the capabilities available to the AI agent when your EHR is connected. The tools are automatically enabled based on your EHR integration.
| Tool | Verification Required | Description |
|---|---|---|
| Get Available Slots | No | Search available appointment slots by date range, provider, appointment type, and insurance payer. |
| Initiate Verification | No | Start patient identity verification. Sends OTP to the email on file. |
| Verify OTP | No | Complete verification with the verification code. |
| New Patient Signup | No | Register a new patient with demographics, insurance, and optional appointment preference. |
| Confirm New Patient | No | Verify new patient code, create the record, and book the appointment in one step. |
| Search Patient | Yes | Search for a patient by name, date of birth, phone, or email. |
| Book Appointment | Yes | Book an appointment for a verified patient. |
| Cancel Appointment | Yes | Cancel an existing appointment (subject to policy rules). |
| Get Patient Appointments | Yes | Retrieve upcoming appointments for a verified patient. |
| Check Eligibility | Yes | Run a real-time insurance eligibility check. |
| Get Lab Results | Yes | Retrieve lab results (names and status only, not detailed values). |
| Request Card | No | Request payment card capture via Stripe (card-on-file or copay collection). |
Policy Engine — Hard Rules
Hard rules are deterministic checks that execute before or after a tool call. They are pure functions: a field, an operator, and a value. They run with zero latency, zero cost, and 100% determinism — no AI is involved.
Create and manage rules in the admin dashboard under Policy → Hard Rules.
Rule structure
| Field | Type | Description |
|---|---|---|
name | string | Human-readable rule name (max 200 chars) |
description | string | Optional description (max 1,000 chars) |
tool | string | The tool this rule applies to (e.g., Cancel Appointment, Book Appointment) |
check.phase | string | pre (before tool executes) or post (after tool returns) |
check.field | string | Dot-path to the value: input.fieldName for pre-check, result.fieldName for post-check |
check.operator | string | One of the 13 built-in operators (see below) |
check.value | any | Comparison value |
category | string | Optional grouping category (max 50 chars) |
severity | string | low, medium, high, or critical |
action | string | block or warn (see below) |
message | string | Message returned to the agent when the rule triggers (max 500 chars) |
Pre-check rules run before the tool executes. If a pre-check rule triggers with action: "block", the tool call is prevented entirely. Post-check rules run after the tool returns and can inspect the result.
All 13 Operators
| Operator | Value Type | Description | Example |
|---|---|---|---|
equals | any | Exact value match (strict equality) | status equals "cancelled" |
not_equals | any | Negated exact match | type not_equals "emergency" |
in_list | array | Value is in the given array | payer in_list ["Aetna","BCBS"] |
not_in_list | array | Value is not in the given array | typeId not_in_list ["AT07","AT09"] |
greater_than | number | Numeric greater than | amount greater_than 500 |
less_than | number | Numeric less than | amount less_than 100 |
contains | string | Case-insensitive substring search | reason contains "emergency" |
not_contains | string | Negated substring search | notes not_contains "urgent" |
regex_match | string (regex) | Regular expression test | phone regex_match "^\d{10}$" |
is_empty | — | Null, undefined, or empty string | referral is_empty |
is_present | — | Not null and not empty | insurancePayer is_present |
time_within_hours | number (hours) | Target datetime is within N hours from now (and in the future) | appointmentDate time_within_hours 48 |
time_beyond_days | number (days) | Target datetime is more than N days from now | appointmentDate time_beyond_days 90 |
Actions: Block vs Warn
| Action | Behavior |
|---|---|
block | Prevents the tool call from executing (pre-check) or overrides the result (post-check). The rule's message is returned to the agent, which relays a policy-compliant explanation to the patient. |
warn | Logs the violation but allows the tool call to proceed. Useful for monitoring potential issues without disrupting the patient experience. |
Workflows
Workflows enforce tool ordering without coding state machines. The engine tracks which tools have been called in each session and blocks actions whose prerequisites have not been met.
Workflow structure
| Field | Type | Description |
|---|---|---|
name | string | Human-readable name (max 200 chars) |
description | string | Optional description (max 1,000 chars) |
trigger | string | The tool that activates this workflow (max 100 chars) |
requires | string[] | Tools that must have succeeded first in this session |
then | string[] | Tools to call after the trigger succeeds (injected as guidance) |
onFailure | string | Action on failure: escalate_to_staff or null |
message | string | Message shown when prerequisites are not met (max 500 chars) |
Example Rules
48-hour cancellation window
{
"name": "48-hour cancellation window",
"tool": "Cancel Appointment",
"category": "scheduling",
"severity": "high",
"check": {
"phase": "pre",
"field": "input.appointmentDate",
"operator": "time_within_hours",
"value": 48
},
"action": "block",
"message": "Our policy requires 48 hours notice for cancellations."
}
90-day booking horizon
{
"name": "90-day booking horizon",
"tool": "Book Appointment",
"category": "scheduling",
"severity": "medium",
"check": {
"phase": "pre",
"field": "input.appointmentDate",
"operator": "time_beyond_days",
"value": 90
},
"action": "block",
"message": "Appointments can only be booked up to 90 days in advance."
}
Insurance check before booking (workflow)
{
"name": "Insurance check before booking",
"trigger": "Book Appointment",
"requires": ["Check Eligibility"],
"then": ["Request Card"],
"onFailure": "escalate_to_staff",
"message": "Insurance eligibility must be verified before booking."
}
Experiments
Experiments let you A/B test different agent configurations against live traffic and measure the impact on conversation outcomes like booking rate, escalation rate, and session duration.
Lifecycle
- Draft — Configure variants and traffic split. No traffic is routed yet.
- Running — Traffic is split between variants. Sessions are tagged with experiment context.
- Ended — Traffic routing stops. Results are frozen for analysis.
Creating an experiment
Create via the admin dashboard under Experiments.
{
"name": "Professional vs friendly tone",
"variants": [
{
"name": "control",
"trafficPercent": 50,
"isControl": true,
"config": {}
},
{
"name": "friendly-tone",
"trafficPercent": 50,
"config": {
"persona": { "tone": "friendly", "useEmojis": true }
}
}
]
}
Traffic Splitting
Traffic is split using a deterministic hash of the visitor ID, so the same visitor always sees the same variant. Each variant has a trafficPercent field. The percentages across all variants must total exactly 100% before the experiment can be started. You need at least 2 variants.
The visitor ID is stored in the browser and persists across sessions and page loads.
What Can Be Varied
Each variant's config object can override any combination of:
- Persona — tone, answer length, emoji usage, custom instructions
- Widget — accent color, greeting, layout, labels
- Guidance — additional or replacement guidance rules
- Hard rules — modified policy rules for the variant
- Workflows — different tool ordering requirements
- Snippets and knowledge docs — different reference material
The control variant should use "isControl": true and an empty config ({}) to capture your current production settings.
Reading Results
While running, each session is tagged with experimentId and variantId. These tags appear in:
- Analytics events (all destinations)
- Session metadata in the admin dashboard
- The admin analytics dashboard, which shows an experiment overlay comparing variants on:
- Booking rate and conversion funnel per variant
- Escalation rate and supervisor block rate
- Average conversation length and duration
- Outcome distribution (booked, resolved, escalated, dropped)
Promoting a Winner
After ending an experiment, promote the winning variant to apply its config to production. Use the Promote button in the admin dashboard.
Promoting applies the variant's persona, widget, guidance, snippets, knowledge docs, and (if present) policy rules and workflows to your main configuration. The experiment must be in ended status first.
Sandbox and Testing
Sandbox mode lets you test the full integration end-to-end using real EHR sandbox environments and Stripe test keys, without affecting production data or analytics.
Toggle sandbox on/off in the admin dashboard under Settings. The widget displays a "SANDBOX" badge in the header when active.
Sandbox EHR Credentials
Configure sandbox EHR keys separately from production in Integrations → Sandbox tab. In sandbox mode, the agent connects to your EHR's test/staging environment. If no sandbox EHR credentials are configured, it falls back to a mock adapter with realistic sample data.
Sandbox Stripe Keys
Set Stripe test keys (sk_test_* and pk_test_*) via Integrations → Sandbox. No real charges are processed. Use Stripe's test card numbers (e.g., 4242 4242 4242 4242) to test card capture flows.
Sandbox Analytics Destinations
Sandbox mode uses separate analytics destination config so test events do not pollute your production analytics. Configure sandbox-specific GTM, postMessage, and webhook destinations in the admin dashboard under Analytics → Sandbox.
Sandbox sessions are tagged with sandbox: true and excluded from production dashboard metrics.
Test Chat in the Dashboard
The admin dashboard includes a built-in test chat interface. Use it to test the agent's behavior with your current configuration without needing to embed the widget on a live page. Policy rules, workflows, guidance, and the supervisor all run normally in test chat.
Webhooks
Configure a webhook URL to receive analytics events as server-side HTTP POST requests. This is useful for feeding data into your own analytics pipeline, CRM, or data warehouse.
Set up via the admin dashboard under Analytics → Destinations → Webhook.
{
"destinations": {
"webhook": {
"enabled": true,
"url": "https://hooks.yourclinic.com/analytics",
"secret": "YOUR_WEBHOOK_SECRET"
}
}
}
HMAC Signature Verification
Every webhook request includes an X-HCA-Signature header containing an HMAC-SHA256 hex digest of the request body, computed using your webhook secret. Always verify this signature to confirm the request came from Healthcare Agent.
const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // In your webhook handler: const signature = req.headers['x-hca-signature']; const rawBody = req.body; // raw string, not parsed JSON if (!verifyWebhook(rawBody, signature, 'YOUR_WEBHOOK_SECRET')) { return res.status(401).send('Invalid signature'); }
Use the raw request body (before JSON parsing) when computing the HMAC. Use crypto.timingSafeEqual to prevent timing attacks.
Event Payload Format
Each webhook request sends a JSON body with this structure:
{
"event": "booking_confirmed",
"timestamp": "2026-03-14T09:41:52.771Z",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"appointmentId": "apt_12345",
// ... additional fields per event type
"experimentId": "exp_abc", // present if experiment is running
"variantId": "var_def" // present if experiment is running
}
Retry Behavior
If your webhook endpoint returns a non-2xx status code or times out, the event is logged but not retried. Ensure your endpoint is highly available. All events are always recorded in the audit log regardless of webhook delivery status.
Error Reference
All error responses use a consistent JSON format with structured error codes. Match on the code field for programmatic error handling.
| Code | HTTP Status | Description |
|---|---|---|
VALIDATION_ERROR | 400 | Invalid request body or parameters. The details array contains field-level errors. |
UNAUTHORIZED | 401 | Missing or invalid API key / session. |
FORBIDDEN | 403 | Insufficient permissions (e.g., viewer trying to edit config). |
NOT_FOUND | 404 | Resource not found (session, form schema, rule, etc.). |
CONFLICT | 409 | Resource already exists. |
RATE_LIMITED | 429 | Too many requests. Wait and retry. |
QUOTA_EXCEEDED | 429 | Monthly usage quota exceeded. Contact your account administrator. |
INTERNAL_ERROR | 500 | Unexpected server error. Include the X-Request-ID header value when reporting issues. |
Error Response Format
{
"code": "VALIDATION_ERROR",
"message": "Invalid request body",
"details": [
{ "path": "message", "message": "String must contain at most 10000 character(s)" }
]
}
The details array is present only for VALIDATION_ERROR responses and contains one entry per invalid field, each with a path (dot-notation field name) and a human-readable message.
Every response includes an X-Request-ID header. Include this value in any support requests to help trace the issue.