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.

your-website.html
<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.

Setup required

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.

AttributeDefaultDescription
data-api-urlRequired. Healthcare Agent platform URL.
data-clinicRequired. Your clinic identifier. Found in the admin dashboard under Widget.
data-accent#2563ebPrimary accent color (hex). Applied to the bubble, header, buttons, and focus rings.
data-positionrightright or left. Which corner the floating bubble appears in.
data-greetingHi! 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.

OptionTypeDefaultDescription
accentColorstring#2563ebPrimary color (hex, max 20 chars)
positionstringrightleft or right
greetingstringHi! How can I help you today?Initial greeting message (max 500 chars)
placeholderstringType a message...Input field placeholder (max 200 chars)
disclaimerstringThis is an AI assistant and does not provide medical advice.Disclaimer text shown at the bottom (max 500 chars)
showDisclaimerbooleantrueWhether to show the disclaimer footer
showPhotoUploadbooleantrueWhether to show the photo upload button (for insurance cards)
headerTitlestringPatient AssistantTitle displayed in the chat header (max 100 chars)
bubbleSizeinteger56Floating bubble diameter in pixels (40–80)
panelWidthinteger380Chat panel width in pixels (300–500)
borderRadiusinteger16Panel corner radius in pixels (0–32)
userBubbleColorstringOverride color for the user's message bubbles (hex)
selectedColorstringColor for selected states (defaults to accent if empty)
confirmedColorstringColor for confirmed states (defaults to accent if empty)
successColorstringColor 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:

OptionTypeDefaultDescription
slotPickerDaysVisibleinteger7Number of days shown in the date strip (3–14)
slotPickerTimeFormatstring12h12h or 24h time display
slotPickerConfirmLabelstringConfirm appointmentText on the confirm button (max 100 chars)
slotPickerEmptyLabelstringNo availability this dayText 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:

OptionTypeDefaultDescription
cardButtonColorstringOverride color for the card submit button (hex)
cardSaveLabelstringSave cardButton text for card-on-file captures (max 50 chars)
cardPayLabelstringPay nowButton text for copay payment captures (max 50 chars)
cardSuccessMessagestringCard savedMessage 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:

X-API-Key: YOUR_API_KEY

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.

POST /v1/chat Send message, get response

Request

curl
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

FieldTypeRequiredDescription
messagestringRequired*The patient's message (max 10,000 chars). Required unless image is provided.
sessionIdstringOptionalSession ID for conversation continuity (max 100 chars). Auto-generated if omitted.
imagestringOptionalBase64 data URL (data:image/jpeg;base64,...). JPEG and PNG only, max 4MB. Used for insurance card photos.

Response — 200 OK

Response
{
  "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.

POST /v1/chat/stream Streaming SSE response
curl
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

EventDataDescription
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

GET /v1/chat/:sessionId Retrieve message history

Returns the conversation transcript as an array of { role, content } objects. Image messages include hasImage: true.

curl
curl https://agent.yourclinic.com/v1/chat/550e8400-e29b-41d4-a716-446655440000 \
  -H "X-API-Key: YOUR_API_KEY"

DELETE /v1/chat/:sessionId

DELETE /v1/chat/:sessionId End and persist a session

Ends the conversation, persists the session for admin review, and clears any patient verification state.

curl
curl -X DELETE https://agent.yourclinic.com/v1/chat/550e8400-... \
  -H "X-API-Key: YOUR_API_KEY"

POST /v1/chat/confirm-card

POST /v1/chat/confirm-card Confirm Stripe card capture

Called after a Stripe card form is completed to confirm the capture on the server side.

FieldTypeRequiredDescription
sessionIdstringRequiredActive session ID
setupIntentIdstringOne ofFor card-on-file captures (Setup Intent)
paymentIntentIdstringOne ofFor copay payment captures (Payment Intent)

POST /v1/chat/fetch-slots

POST /v1/chat/fetch-slots Fetch additional appointment slots

Used to load more appointment slots when the patient navigates to a different week in the schedule picker.

FieldTypeRequiredDescription
sessionIdstringRequiredActive session ID
startDatestringRequiredStart date for the slot search (YYYY-MM-DD)
endDatestringRequiredEnd 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/chat call.
  • Omit sessionId to 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.
Rate limits

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.

POST /v1/forms/start Start a new form session
curl
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

POST /v1/forms/message Continue form conversation
curl
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"}'
FieldTypeRequiredDescription
sessionIdstringRequiredForm session ID from /forms/start
messagestringRequiredThe 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

GET /v1/forms/:sessionId Get form session progress

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

GET /v1/forms/schemas List available form 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.

Example schema
{
  "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

KeyTypeDescription
idstringUnique form identifier
namestringDisplay name
stepsStep[]Ordered list of form steps
onSubmitAction[]Actions to run after the final step
brandingobjectOptional: 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

Condition examples
// 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 TypeDescription
providersFetch available providers from the EHR
slotsFetch appointment slots (requires provider/type params)
insurance_checkRun eligibility check
clinic_hoursLoad clinic operating hours
locationsFetch clinic locations/departments
eligibilityCheck insurance eligibility
customFetch from a custom URL

Submit actions

Action TypeDescription
register_patientRegister a new patient in the EHR
book_appointmentBook an appointment
check_eligibilityRun insurance eligibility check
send_emailSend a confirmation email
send_otpSend a verification code
verify_otpVerify a submitted code
webhookPOST form data to an external URL
set_fieldSet a field value programmatically

Each action supports a condition (only run if met) and params with {{ template }} references to collected values.

Field Types

TypeDescription
textFree-form text input
emailEmail with format validation
phonePhone number with format validation
dateDate in MM/DD/YYYY format
numberNumeric input with optional min/max
selectSingle-select from options list
multiselectMultiple selection from options
radioRadio button group
checkboxBoolean checkbox
provider_selectProvider picker (populated from EHR data source)
slot_selectAppointment slot picker
location_selectClinic location picker
infoDisplay-only text block (not collected)
hiddenHidden field with preset value

Validation Rules

RuleParameterDescription
requiredField must have a non-empty value
emailMust match email format
phoneMust be a valid phone number (7+ digits)
dateMust match MM/DD/YYYY format
min_lengthnumberMinimum character count
max_lengthnumberMaximum character count
minnumberMinimum numeric value
maxnumberMaximum numeric value
patternregex stringValue 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

EventTriggerPayload Fields
chat_loadedWidget loads on pagetimestamp, visitorId, sessionId, pageUrl, referrer
chat_openedVisitor opens the widgettimestamp, visitorId, sessionId
chat_closedVisitor closes the widgettimestamp, visitorId, sessionId, messageCount
message_sentPatient sends a messagetimestamp, visitorId, sessionId, messageLength, hasImage
message_receivedAgent respondstimestamp, visitorId, sessionId, responseLength
slots_shownAvailable slots displayed to patienttimestamp, visitorId, sessionId, slotCount, dateRangeStart, dateRangeEnd, timeToNextSlotMs
slot_selectedPatient picks an appointment slottimestamp, visitorId, sessionId, selectedTime, providerName, providerId
slot_confirmedPatient confirms slot selectiontimestamp, visitorId, sessionId, selectedTime, providerName, appointmentDate
date_range_changedPatient navigates to different datestimestamp, visitorId, sessionId, newStartDate, newEndDate
card_savedStripe card saved successfullytimestamp, visitorId, sessionId, last4, brand
booking_confirmedBooking confirmed in EHRtimestamp, sessionId, appointmentId, patientId*
escalatedConversation escalated to stafftimestamp, sessionId
verification_completedPatient OTP verified successfullytimestamp, 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.

Analytics config (admin dashboard or API)
{
  "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.

Prerequisite

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().

Analytics config
{
  "destinations": {
    "postMessage": { "enabled": true, "targetOrigin": "https://www.yourclinic.com" }
  }
}

Listen for events in your parent page:

JavaScript
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.

Analytics config
{
  "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.

Toggling fields for a specific event
{
  "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

HIPAA warning

The following fields may contain Protected Health Information (PHI) and are disabled by default:

  • booking_confirmed.patientId
  • verification_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 identifier
  • variantId — 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

SystemAPI TypeDescription
HealthieAPI integrationFull integration including patient registration, scheduling, insurance, and lab results.
athenahealthAPI integrationFull integration covering patient search, scheduling, eligibility, and lab results.

Getting API Credentials

Healthie

  1. Log in to your Healthie admin account.
  2. Navigate to Settings → API Keys.
  3. Generate a new API key. Copy the key value.
  4. Note your practice ID (visible in the URL or settings).

athenahealth

  1. Register at the athenahealth Developer Portal.
  2. Create a new application to get a Client ID and Client Secret.
  3. Note your Practice ID from your athenahealth account.
  4. 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

  1. Patient provides full legal name and date of birth in conversation.
  2. Agent initiates verification — the system sends a verification code to the email address on file in the EHR.
  3. Patient provides the code in the chat.
  4. Agent verifies the code — verification state is set on the session.
Security design

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.

ToolVerification RequiredDescription
Get Available SlotsNoSearch available appointment slots by date range, provider, appointment type, and insurance payer.
Initiate VerificationNoStart patient identity verification. Sends OTP to the email on file.
Verify OTPNoComplete verification with the verification code.
New Patient SignupNoRegister a new patient with demographics, insurance, and optional appointment preference.
Confirm New PatientNoVerify new patient code, create the record, and book the appointment in one step.
Search PatientYesSearch for a patient by name, date of birth, phone, or email.
Book AppointmentYesBook an appointment for a verified patient.
Cancel AppointmentYesCancel an existing appointment (subject to policy rules).
Get Patient AppointmentsYesRetrieve upcoming appointments for a verified patient.
Check EligibilityYesRun a real-time insurance eligibility check.
Get Lab ResultsYesRetrieve lab results (names and status only, not detailed values).
Request CardNoRequest 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 PolicyHard Rules.

Rule structure

FieldTypeDescription
namestringHuman-readable rule name (max 200 chars)
descriptionstringOptional description (max 1,000 chars)
toolstringThe tool this rule applies to (e.g., Cancel Appointment, Book Appointment)
check.phasestringpre (before tool executes) or post (after tool returns)
check.fieldstringDot-path to the value: input.fieldName for pre-check, result.fieldName for post-check
check.operatorstringOne of the 13 built-in operators (see below)
check.valueanyComparison value
categorystringOptional grouping category (max 50 chars)
severitystringlow, medium, high, or critical
actionstringblock or warn (see below)
messagestringMessage 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

OperatorValue TypeDescriptionExample
equalsanyExact value match (strict equality)status equals "cancelled"
not_equalsanyNegated exact matchtype not_equals "emergency"
in_listarrayValue is in the given arraypayer in_list ["Aetna","BCBS"]
not_in_listarrayValue is not in the given arraytypeId not_in_list ["AT07","AT09"]
greater_thannumberNumeric greater thanamount greater_than 500
less_thannumberNumeric less thanamount less_than 100
containsstringCase-insensitive substring searchreason contains "emergency"
not_containsstringNegated substring searchnotes not_contains "urgent"
regex_matchstring (regex)Regular expression testphone regex_match "^\d{10}$"
is_emptyNull, undefined, or empty stringreferral is_empty
is_presentNot null and not emptyinsurancePayer is_present
time_within_hoursnumber (hours)Target datetime is within N hours from now (and in the future)appointmentDate time_within_hours 48
time_beyond_daysnumber (days)Target datetime is more than N days from nowappointmentDate time_beyond_days 90

Actions: Block vs Warn

ActionBehavior
blockPrevents 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.
warnLogs 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

FieldTypeDescription
namestringHuman-readable name (max 200 chars)
descriptionstringOptional description (max 1,000 chars)
triggerstringThe tool that activates this workflow (max 100 chars)
requiresstring[]Tools that must have succeeded first in this session
thenstring[]Tools to call after the trigger succeeds (injected as guidance)
onFailurestringAction on failure: escalate_to_staff or null
messagestringMessage shown when prerequisites are not met (max 500 chars)

Example Rules

48-hour cancellation window

Hard Rule
{
  "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

Hard Rule
{
  "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)

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

  1. Draft — Configure variants and traffic split. No traffic is routed yet.
  2. Running — Traffic is split between variants. Sessions are tagged with experiment context.
  3. Ended — Traffic routing stops. Results are frozen for analysis.

Creating an experiment

Create via the admin dashboard under Experiments.

Experiment configuration
{
  "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.

Webhook configuration
{
  "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.

Node.js verification example
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');
}
Important

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:

Webhook payload
{
  "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.

CodeHTTP StatusDescription
VALIDATION_ERROR400Invalid request body or parameters. The details array contains field-level errors.
UNAUTHORIZED401Missing or invalid API key / session.
FORBIDDEN403Insufficient permissions (e.g., viewer trying to edit config).
NOT_FOUND404Resource not found (session, form schema, rule, etc.).
CONFLICT409Resource already exists.
RATE_LIMITED429Too many requests. Wait and retry.
QUOTA_EXCEEDED429Monthly usage quota exceeded. Contact your account administrator.
INTERNAL_ERROR500Unexpected server error. Include the X-Request-ID header value when reporting issues.

Error Response Format

400 Validation Error
{
  "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.