API Endpoints¶
All Webhook Endpoints (via Vapi tool webhooks) — Updated for v4.0.0
Base URL¶
| Environment | URL |
|---|---|
| Production | https://api.vitaravox.ca:9443 |
| Development | https://api-dev.vitaravox.ca:9443 |
Authentication¶
Vapi webhook endpoints support multi-auth (any one method is sufficient):
| Method | Header | Description |
|---|---|---|
| Bearer Token | Authorization: Bearer <secret> |
Primary method. Token matches VAPI_WEBHOOK_SECRET env var |
| HMAC-SHA256 | X-Vapi-Signature: <hmac-sha256-signature> |
Vapi's built-in webhook signing |
| API Key | X-API-Key: <api-key> |
Legacy/admin API key |
All comparisons use constant-time (crypto.timingSafeEqual) to prevent timing attacks. In development mode (non-production), auth is skipped if no secret is configured.
Admin API endpoints use JWT authentication:
Health & Status¶
GET /health¶
Health check endpoint. No authentication required.
Response:
{
"status": "healthy",
"timestamp": "2026-01-12T15:30:00.000Z",
"version": "1.1.0",
"components": {
"database": { "healthy": true, "latency_ms": 2 },
"oscar": { "healthy": true, "latency_ms": 45 }
}
}
Patient Lookup¶
POST /api/vapi/search-patient-by-phone¶
Search patient by phone number (caller ID).
Request:
Response (Found):
{
"success": true,
"patient": {
"demographicNo": 12345,
"firstName": "John",
"lastName": "Smith",
"phone": "604-555-1234"
}
}
Response (Not Found):
POST /api/vapi/search-patient¶
Search patient by name.
Request:
Response:
{
"success": true,
"patients": [
{
"demographicNo": 12345,
"firstName": "John",
"lastName": "Smith",
"dateOfBirth": "1985-03-15"
}
]
}
POST /api/vapi/get-patient¶
Get full patient details by demographic number.
Request:
Response:
{
"success": true,
"patient": {
"demographicNo": 12345,
"firstName": "John",
"lastName": "Smith",
"dateOfBirth": "1985-03-15",
"sex": "M",
"hin": "9876543210",
"phone": "604-555-1234",
"email": "john.smith@email.com",
"address": {
"street": "123 Main St",
"city": "Vancouver",
"province": "BC",
"postalCode": "V6A 1A1"
}
}
}
Clinic Information¶
POST /api/vapi/get-clinic-info¶
Get clinic configuration including registration status.
Request:
Response:
{
"success": true,
"clinic": {
"id": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"name": "Downtown Medical Clinic",
"phone": "604-555-0100",
"address": "456 Health Ave, Vancouver, BC V6B 2B2",
"accepting_new_patients": true,
"waitlist_enabled": true,
"hours": {
"monday": { "open": "09:00", "close": "17:00" },
"tuesday": { "open": "09:00", "close": "17:00" },
"wednesday": { "open": "09:00", "close": "17:00" },
"thursday": { "open": "09:00", "close": "17:00" },
"friday": { "open": "09:00", "close": "17:00" },
"saturday": { "closed": true },
"sunday": { "closed": true }
}
}
}
POST /api/vapi/get-providers¶
Get list of providers for a clinic.
Request:
Response:
{
"success": true,
"providers": [
{
"providerNo": "100001",
"displayName": "Dr. Sarah Chen",
"specialty": "Family Medicine",
"acceptingNewPatients": true
},
{
"providerNo": "100002",
"displayName": "Dr. Michael Wong",
"specialty": "Family Medicine",
"acceptingNewPatients": false
}
]
}
Appointment Management¶
POST /api/vapi/find-earliest-appointment¶
Find the earliest available appointment slot. Returns exactly one slot to prevent decision paralysis. Supports smart filtering and server-side guardrails.
Request:
{
"providerId": "100001",
"providerName": "Dr. Chen",
"startDate": "2026-02-06",
"endDate": "2026-02-20",
"timeOfDay": "morning",
"excludeDates": ["2026-02-07", "2026-02-10"]
}
| Parameter | Type | Required | Description |
|---|---|---|---|
providerId |
string | No | Provider ID or "any" for any provider |
providerName |
string | No | Human name (e.g. "Dr. Chen"). Server fuzzy-matches against provider list, strips "Dr." prefix |
startDate |
string | No | ISO date. Server clamps past dates to today (GPT-4o doesn't know current date) |
endDate |
string | No | ISO date. Limits search window |
timeOfDay |
string | No | "morning", "afternoon", or "evening" — filters by time range |
excludeDates |
string[] | No | Dates to skip (used when patient rejects a slot, then searches again) |
Server-side behaviors:
- If startDate is in the past, clamps to today with a warning log
- If providerName is provided but providerId is not, resolves name via fuzzy matching
- Non-numeric providerId values ("any", "任何") treated as "search all providers"
- Returns exactly 1 slot (the earliest match)
Response:
{
"success": true,
"appointment": {
"date": "2026-02-07",
"time": "09:30",
"duration": 15,
"provider": {
"providerNo": "100001",
"displayName": "Dr. Sarah Chen"
}
}
}
POST /api/vapi/check-appointments¶
Check available appointments or patient's existing appointments.
Request (Find Available):
{
"providerId": "100001",
"startDate": "2026-01-13",
"endDate": "2026-01-20",
"findAvailable": true
}
Request (Patient's Appointments — v3.0 uses demographicId):
{
"demographicId": 12345,
"startDate": "2026-02-10",
"endDate": "2026-08-10",
"findAvailable": false
}
Dual parameter naming
Both demographicId and demographicNo/patientId are accepted. The server checks for demographicId first, then falls back to patientId and demographicNo.
Response (Available):
{
"success": true,
"appointments": [
{
"date": "2026-01-14",
"time": "09:30",
"duration": 15,
"provider": { "providerNo": "100001", "displayName": "Dr. Sarah Chen" }
},
{
"date": "2026-01-14",
"time": "10:00",
"duration": 15,
"provider": { "providerNo": "100001", "displayName": "Dr. Sarah Chen" }
}
]
}
POST /api/vapi/create-appointment¶
Book a new appointment. Accepts both legacy and v3.0 parameter names.
Request (v3.0 — preferred):
{
"demographicId": 12345,
"providerId": "100001",
"startTime": "2026-02-14T09:30:00",
"appointmentType": "B",
"reason": "Prescription refill",
"language": "en"
}
Request (legacy — still accepted):
{
"demographicNo": 12345,
"providerNo": "100001",
"date": "2026-02-14",
"startTime": "09:30",
"duration": 15,
"appointmentType": "B",
"reason": "Prescription refill",
"notes": "Booked via VitaraVox"
}
| Parameter | Type | Required | v3.0 Name | Legacy Name | Description |
|---|---|---|---|---|---|
demographicId |
number | Yes | demographicId |
demographicNo, patientId |
Patient demographic number |
providerId |
string | Yes | providerId |
providerNo |
Provider number |
startTime |
string | Yes | startTime |
appointmentTime, date+startTime |
ISO 8601 datetime (YYYY-MM-DDTHH:MM:00) or bare time |
appointmentType |
string | No | appointmentType |
same | B (general), 2 (follow-up), 3 (concern), P (prescription). Server validates against ['B','2','3','P'] — invalid values default to 'B' |
reason |
string | No | reason |
same | Visit reason from patient |
language |
string | No | language |
— | en or zh. Cached for call log via callMetadataCache |
Server-side behaviors (v3.0):
- Accepts both
demographicIdandpatientId/demographicNo(first non-null wins) - Robust time parsing: ISO 8601, date+time, bare time formats
appointmentTypevalidated against['B', '2', '3', 'P']— invalid defaults to'B'languageanddemographicIdcached viasetCallMetadatafor end-of-call log
Response:
{
"success": true,
"appointment": {
"appointmentNo": 99001,
"demographicNo": 12345,
"providerNo": "100001",
"date": "2026-02-14",
"startTime": "09:30",
"duration": 15,
"status": "confirmed"
}
}
POST /api/vapi/update-appointment¶
Reschedule an existing appointment. v3.0 uses appointmentId + newStartTime + newProviderId.
Request (v3.0 — preferred):
Request (legacy — still accepted):
| Parameter | Type | Required | v3.0 Name | Legacy Name | Description |
|---|---|---|---|---|---|
appointmentId |
number | Yes | appointmentId |
appointmentNo |
Existing appointment ID to reschedule |
newStartTime |
string | Yes | newStartTime |
newDate+newStartTime |
ISO 8601 datetime for new slot |
newProviderId |
string | No | newProviderId |
— | New provider ID if changing doctor |
Server-side behavior: Books the new appointment first, then cancels the old one (safer than cancel-first).
Response:
{
"success": true,
"appointment": {
"appointmentNo": 99001,
"date": "2026-02-15",
"startTime": "14:00",
"status": "rescheduled"
}
}
POST /api/vapi/cancel-appointment¶
Cancel an appointment.
Request (v3.0):
Request (legacy):
| Parameter | Type | Required | v3.0 Name | Legacy Name | Description |
|---|---|---|---|---|---|
appointmentId |
number | Yes | appointmentId |
appointmentNo |
Appointment to cancel |
reason |
string | No | reason |
same | Cancellation reason |
Response:
Registration & Waitlist¶
POST /api/vapi/register-new-patient¶
Register a new patient in OSCAR EMR. v3.0 adds healthCardType, language, and flattened address fields.
Request (v3.0 — preferred):
{
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-05-20",
"gender": "F",
"phone": "6045559876",
"address": "789 Oak St",
"city": "Vancouver",
"postalCode": "V6C 3C3",
"province": "BC",
"healthCardNumber": "9123456789",
"healthCardType": "BC",
"language": "en",
"email": "jane.doe@email.com"
}
Request (legacy — still accepted):
{
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-05-20",
"sex": "F",
"hin": "9123456789",
"phone": "604-555-9876",
"email": "jane.doe@email.com",
"address": {
"street": "789 Oak St",
"city": "Vancouver",
"province": "BC",
"postalCode": "V6C 3C3"
}
}
| Parameter | Type | Required | Description |
|---|---|---|---|
firstName |
string | Yes | Patient first name |
lastName |
string | Yes | Patient last name |
dateOfBirth |
string | Yes | YYYY-MM-DD format |
gender / sex |
string | Yes | M, F, or O (both field names accepted) |
phone |
string | Yes | 10-digit phone number |
address |
string | No | Street address (v3.0 flat) or object (legacy) |
city |
string | No | City |
postalCode |
string | No | Postal code |
province |
string | No | Province (default BC) |
healthCardNumber / hin |
string | No | Health card number |
healthCardType |
string | No | BC, OUT_OF_PROVINCE, or PRIVATE. Maps to OSCAR hcType field |
language |
string | No | en or zh. Cached for call log |
email |
string | No | Email address |
Server-side behaviors (v3.0):
healthCardTypemapped to OSCARhcTypevalues:BC->BC,OUT_OF_PROVINCE->OT,PRIVATE->PRlanguageanddemographicIdcached viasetCallMetadataas fallback for call log- Gender normalized:
male/M->M,female/F->F,other/O->O
Response:
{
"success": true,
"patient": {
"demographicNo": 12346,
"firstName": "Jane",
"lastName": "Doe"
},
"message": "Patient registered successfully"
}
POST /api/vapi/add-to-waitlist¶
Add patient to new patient waitlist.
Request:
{
"clinicId": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"firstName": "Jane",
"lastName": "Doe",
"phone": "604-555-9876",
"email": "jane.doe@email.com",
"notes": "Interested in family medicine"
}
Response:
Call Management¶
POST /api/vapi/transfer-call¶
Transfer call to clinic staff.
Request:
Response:
POST /api/vapi/log-call-metadata¶
Log call metadata for analytics. In v3.0, this is called by every role agent (Booking, Modification, Registration) instead of a separate Confirmation agent.
Request (v3.0):
| Parameter | Type | Required | Description |
|---|---|---|---|
callOutcome |
string | Yes | booked, rescheduled, cancelled, registered, waitlisted, no_action, clinic_info, transferred |
language |
string | No | en or zh |
demographicId |
number | No | Patient demographic number |
appointmentId |
number | No | Related appointment ID |
Server-side behavior (v3.0 — callMetadataCache):
The log_call_metadata tool caches its data in an in-memory Map keyed by callId:
When the end-of-call-report webhook fires, saveCallLog calls getCallMetadata(callId) and merges the cached data into the database record. This bridges the gap because Vapi's end-of-call-report does not include tool call results.
Fallback caching: create_appointment and register_new_patient also call setCallMetadata with language and demographicId — ensuring the call log has this data even if the LLM never calls log_call_metadata.
Response:
Error Responses¶
All endpoints return consistent error format:
{
"success": false,
"error": {
"code": "PATIENT_NOT_FOUND",
"message": "No patient found with the provided criteria"
}
}
Error Codes¶
| Code | HTTP Status | Description |
|---|---|---|
PATIENT_NOT_FOUND |
404 | Patient not found in EMR |
APPOINTMENT_NOT_FOUND |
404 | Appointment not found |
NO_AVAILABILITY |
404 | No appointments available |
INVALID_REQUEST |
400 | Invalid request parameters |
OSCAR_UNAVAILABLE |
503 | OSCAR EMR unreachable |
UNAUTHORIZED |
401 | Invalid or missing API key |
RATE_LIMITED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Internal server error |
Rate Limits¶
| Endpoint | Limit | Burst |
|---|---|---|
| General API | 100/min | 20 |
Booking (create-appointment) |
20/min | 5 |
| Health check | Unlimited | - |
Admin API (v1.5.0+)¶
The Admin API provides endpoints for clinic and user management. All admin endpoints require JWT authentication.
Authentication¶
Response Format¶
All admin endpoints return a consistent format:
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"pageSize": 10,
"total": 25,
"totalPages": 3
}
}
Auth Endpoints¶
| Endpoint | Method | Description |
|---|---|---|
/api/auth/login |
POST | User login, returns JWT tokens |
/api/auth/logout |
POST | User logout |
/api/auth/refresh |
POST | Refresh access token |
/api/auth/change-password |
POST | Change own password |
/api/auth/me |
GET | Get current user info |
Admin - Clinics (Vitara Admin only)¶
| Endpoint | Method | Description |
|---|---|---|
/api/admin/clinics |
GET | List clinics (paginated) |
/api/admin/clinics/stats |
GET | Clinic statistics |
/api/admin/clinics |
POST | Create clinic |
/api/admin/clinics/:id |
GET | Get clinic details |
/api/admin/clinics/:id |
PUT | Update clinic |
/api/admin/clinics/:id |
DELETE | Delete clinic |
/api/admin/clinics/:id/emr-config |
PUT | Update EMR config |
/api/admin/clinics/:id/go-live |
POST | Set EMR live status |
Admin - Users (Vitara Admin only)¶
| Endpoint | Method | Description |
|---|---|---|
/api/admin/users |
GET | List users (paginated) |
/api/admin/users |
POST | Create user |
/api/admin/users/:id |
GET | Get user details |
/api/admin/users/:id |
PUT | Update user |
/api/admin/users/:id |
DELETE | Delete user |
/api/admin/users/:id/reset-password |
POST | Reset password |
/api/admin/users/:id/unlock |
POST | Unlock account |
Admin - Audit Logs (Vitara Admin only)¶
| Endpoint | Method | Description |
|---|---|---|
/api/admin/audit-logs |
GET | List audit logs (paginated) |
/api/admin/audit-logs/summary |
GET | Activity summary |
Clinic Manager - Self-Service¶
| Endpoint | Method | Description |
|---|---|---|
/api/clinic/me |
GET | Get own clinic |
/api/clinic/settings |
GET | Get clinic settings (getClinicSettings) |
/api/clinic/stats |
GET | Get clinic statistics |
/api/clinic/calls |
GET | Get recent calls |
/api/clinic/providers |
GET | Get clinic providers |
/api/clinic/schedule |
GET | Get schedule settings |
/api/clinic/emr |
GET | Get EMR configuration |
/api/clinic/emr/test |
POST | Test OSCAR connection |
/api/clinic/emr/sync |
POST | Sync from OSCAR |
/api/clinic/providers/sync |
POST | Sync providers from OSCAR |
/api/clinic/analytics |
GET | Get call analytics |
/api/clinic/call-history |
GET | Get paginated call history |
/api/clinic/waitlist |
GET | Get waitlist entries |
/api/clinic/waitlist/stats |
GET | Get waitlist statistics |
Clinic Manager - Onboarding (v4.0.0+)¶
| Endpoint | Method | Description |
|---|---|---|
/api/clinic/onboarding |
GET | Get onboarding progress |
/api/clinic/onboarding |
PUT | Update onboarding step flags |
/api/clinic/onboarding/validate |
GET | Run pre-launch validation checks |
/api/clinic/onboarding/go-live |
POST | Activate clinic (blocked if checks fail) |
/api/clinic/onboarding/complete |
POST | Complete onboarding (notifies admin) |
/api/clinic/onboarding/test-slots |
POST | Test schedule slot retrieval (validation step) |
/api/clinic/onboarding/test-patient-search |
POST | Test patient search (validation step) |
/api/clinic/oscar-config |
GET | Get OSCAR config (schedule codes, appointment types) |
/api/clinic/oscar-config/pull |
POST | Pull config from OSCAR |
Admin - Provisioning (v4.0.0+)¶
| Endpoint | Method | Description |
|---|---|---|
/api/admin/clinics/pending-activation |
GET | List clinics awaiting activation |
/api/admin/clinics/:id/provisioning |
GET | Get provisioning status for a clinic |
/api/admin/clinics/:id/activate |
PUT | Activate clinic (requires phone assigned) |
Admin - Vapi Management (v2.1.0+)¶
| Endpoint | Method | Description |
|---|---|---|
/api/admin/vapi/mappings |
GET | List all clinic-assistant mappings |
/api/admin/clinics/:id/vapi |
PUT | Update Vapi config for a clinic |
/api/admin/clinics/:id/vapi/test |
POST | Test Vapi connection |
/api/admin/clinics/:id/vapi |
DELETE | Remove Vapi config from clinic |
Example: Get Vapi Mappings Response
{
"success": true,
"data": [
{
"clinicId": "vitaravox-demo",
"clinicName": "VitaraVox Demo Clinic",
"vapiPhone": "+1 (604) 555-8888",
"vapiAssistantId": "45f98810-2ff3-4c25-9cf0-54a2d2822d37",
"vapiSquadId": null,
"vapiConnected": true,
"oscarConnected": true,
"status": "active"
}
]
}
Example: Update Vapi Config
PUT /api/admin/clinics/vitaravox-demo/vapi
{
"vapiAssistantId": "45f98810-2ff3-4c25-9cf0-54a2d2822d37",
"vapiPhone": "+1 (604) 555-8888"
}
v3.0 Server-Side Changes Summary¶
Parameter Naming (Dual Support)¶
v3.0 Vapi tools use demographicId, appointmentId, providerId, startTime, etc. The server accepts both v3.0 and legacy parameter names for backward compatibility:
| v3.0 Name | Legacy Names Also Accepted |
|---|---|
demographicId |
patientId, demographicNo |
appointmentId |
appointmentNo |
providerId |
providerNo |
startTime (ISO 8601) |
date + startTime, appointmentTime |
newStartTime (ISO 8601) |
newDate + newStartTime |
newProviderId |
— (new in v3.0) |
healthCardType |
— (new in v3.0) |
language |
— (new in v3.0) |
callOutcome |
outcome |
Call Metadata Cache¶
The callMetadataCache is an in-memory Map<string, CallMetadata> that bridges tool-call webhooks and end-of-call-report webhooks:
| Function | Purpose |
|---|---|
setCallMetadata(callId, data) |
Store metadata during tool calls |
getCallMetadata(callId) |
Retrieve in saveCallLog for merging into DB |
Set by: log_call_metadata, create_appointment, register_new_patient
Read by: saveCallLog (on end-of-call-report webhook)
Appointment Type Validation¶
Server validates appointmentType against ['B', '2', '3', 'P']. Invalid values silently default to 'B'.
Clinic Resolution (v3.0 Squad Support)¶
Webhook requests resolve the clinic through:
The clinic_config table now has both vapi_squad_id (v2.3.0) and vapi_squad_id_v3 (v3.0) columns. The resolver checks both: