Quickstart - IECC Direct API Call Integration Guide
Use the Evergrove agent API directly to create scheduler entities, schedule outbound calls, trigger them, and poll for outcomes when building an IECC integration.
Prerequisites
Before you start, obtain an API key from Evergrove Labs. You can set key rotation to quarterly, yearly, or another agreed cadence.
Base URL and authentication
Send every request to https://agent-api.evergrovelabs.com. Trim any trailing slash from the base URL before appending paths in application code.
Every JSON request must include these headers:
Authorization: Bearer {API_KEY}
Accept: application/json
Content-Type: application/jsonIn curl examples and generated client code, define the following variables:
export API_URL="https://agent-api.evergrovelabs.com"
export API_KEY="<provided key>"Endpoints
Endpoint | Method | Purpose |
|---|---|---|
| POST | Create or update one callable scheduler entity. |
| POST | Create queued scheduled requests. |
| POST | Start one queued scheduled request. |
| GET | Poll request status and outcomes. |
Shared schemas
Normalized call row
Prepare one normalized row per outbound call before using the API:
interface NormalizedCallRow {
external_reference_id: string;
organization_name: string;
patient_name: string;
patient_ssn?: string | null;
patient_dob: string; // YYYY-MM-DD
patient_phone_number?: string | null; // optional NANP phone; API normalizes to E.164
date_of_injury: string; // YYYY-MM-DD
expected_appointment_date_time: string; // YYYY-MM-DDTHH:mm:ss
local_timezone: string; // e.g. America/New_York
service_modality: string;
claim_number: string;
insurer_group_name: string;
body_part: string;
provider_office: string;
provider_office_address: string;
provider_state?: string | null;
outbound_phone_number: string; // callable NANP E.164 provider phone
eoc_number: string;
notes?: string | null;
}Recommended validation rules:
external_reference_idmust be stable and unique per source row.outbound_phone_numberis required and must be a callable NANP E.164 number, for example+12125550199.patient_phone_numberis optional; omit it, sendnull, or sendNONEwhen blank. The API accepts valid NANP input and normalizes it to+1....patient_ssnis optional; omit it, sendnull, sendNONE, send 9 digits, or send last 4 digits.patient_dobanddate_of_injurymust be ISO dates inYYYY-MM-DDformat.expected_appointment_date_timemust be a timezone-naive ISO datetime. Do not includeZ, offsets, dates without times, or fractional seconds.local_timezonemust be a canonical region IANA timezone, for exampleAmerica/New_York.provider_stateis optional and may be a valid US state abbreviation or full state name.Use
claim_number: "Not Available"if no claim number exists.organization_namemust be enabled for the tenant associated with the provided API key; otherwise scheduling returns422.
Working hours policy
interface WorkingHoursWindow {
start: string; // HH:mm
end: string; // HH:mm
}
interface WorkingHoursDayPolicy {
default_call_time?: string | null; // HH:mm
windows?: WorkingHoursWindow[];
}
interface WorkingHoursPolicy {
monday?: WorkingHoursDayPolicy;
tuesday?: WorkingHoursDayPolicy;
wednesday?: WorkingHoursDayPolicy;
thursday?: WorkingHoursDayPolicy;
friday?: WorkingHoursDayPolicy;
saturday?: WorkingHoursDayPolicy;
sunday?: WorkingHoursDayPolicy;
}Working-hours objects reject extra fields. A day with windows must include default_call_time, and that default must fall inside one of the windows. A day with no windows is closed and must not set default_call_time.
Default policy used by the existing flow:
{
"monday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"tuesday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"wednesday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"thursday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"friday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"saturday": {},
"sunday": {}
}Step 1: Create scheduler entity
POST /scheduler/entities upserts by name within the authenticated tenant. If an entity with the same name exists, the endpoint updates its schedule and location fields and returns the existing entity. Persist the returned id as entity_id for the row.
Request schema
interface CreateSchedulerEntityRequest {
name: string;
location_name?: string | null;
location_timezone: string; // IANA timezone, e.g. America/New_York
working_hours_policy: WorkingHoursPolicy;
ivr_answer_key_guide?: string | null;
}Request field | Source |
|---|---|
|
|
|
|
|
|
| Caller-defined calling window; default Monday–Friday 09:00–17:00 |
| Optional IVR instructions for the agent. |
Example request
curl -X POST "$API_URL/scheduler/entities" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "Select Physical Therapy",
"location_name": "Select Physical Therapy",
"location_timezone": "America/New_York",
"working_hours_policy": {
"monday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"tuesday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"wednesday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"thursday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"friday": { "default_call_time": "09:00", "windows": [{ "start": "09:00", "end": "17:00" }] },
"saturday": {},
"sunday": {}
}
}'Response schema
interface SchedulerEntity {
id: string;
tenant_id: string;
name: string;
location_name: string | null;
location_timezone: string;
can_call: boolean;
ivr_answer_key_guide?: string | null;
working_hours_policy: WorkingHoursPolicy;
working_hours_source?: string | null;
working_hours_updated_at?: string | null;
created_at: string;
}Persist the mapping:
entityIdByExternalReference[row.external_reference_id] = entity.id;Step 2: Schedule requests
After each call row has an entity_id, create queued scheduled requests. This step does not start calls.
Request schema
interface ScheduleRequestsRequest {
calls: ScheduleRequestCallInput[];
}
type CallObjective = 'CONFIRM_IE_ATTENDANCE' | 'RESCHEDULE';
interface ScheduleRequestCallInput {
entity_id: string; // UUID
external_reference_id?: string; // if you create scheduled request in bulk, this is how you can keep track of it
outbound_phone_number: string; // callable NANP E.164 provider phone
objective: CallObjective;
notes?: string | null;
best_to_make_call_by?: string | null; // ISO datetime
details: ScheduleRequestDetails;
}
interface ScheduleRequestDetails {
organization_name: string;
patient_name: string;
patient_ssn?: string | null;
patient_dob: string; // YYYY-MM-DD
patient_phone_number?: string | null; // optional NANP phone; API normalizes to E.164
date_of_injury: string; // YYYY-MM-DD
expected_appointment_date_time: string; // YYYY-MM-DDTHH:mm:ss
local_timezone: string;
service_modality: string;
claim_number?: string; // defaults to "Not Available" when blank or omitted
insurer_group_name: string;
body_part: string;
provider_office: string;
provider_office_address: string;
provider_state?: string | null;
eoc_number: string;
notes?: string | null;
}Use objective: "CONFIRM_IE_ATTENDANCE" for IECC attendance-confirmation calls. If any call in the request has external_reference_id, every call in that request must have one.
Field notes:
Omit
details.patient_phone_numberwhen unavailable.Omit
details.patient_ssn, set it tonull, or sendNONEwhen unavailable.Put caller-facing notes in the top-level
notesfield.details.notesis accepted by the request model but is not part of the persisted initial-eval contract metadata.
Example request
curl -X POST "$API_URL/scheduler/requests" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"calls": [
{
"entity_id": "11111111-1111-4111-8111-111111111111",
"external_reference_id": "row-0001",
"outbound_phone_number": "+12125550199",
"objective": "CONFIRM_IE_ATTENDANCE",
"details": {
"organization_name": "SPNet",
"patient_name": "Jane Patient",
"patient_ssn": "999-88-7777",
"patient_dob": "1967-04-13",
"patient_phone_number": "+12125550100",
"date_of_injury": "2026-01-15",
"expected_appointment_date_time": "2026-04-09T11:00:00",
"local_timezone": "America/New_York",
"service_modality": "physical therapy",
"claim_number": "CLAIM-1",
"insurer_group_name": "Acme Insurer Group",
"body_part": "left shoulder",
"provider_office": "Select Physical Therapy",
"provider_office_address": "456 Provider Rd, New York, NY 10002",
"provider_state": "NY",
"eoc_number": "EOC-1"
}
}
]
}'Response schema
interface ScheduleRequestsResponse {
scheduled_requests: ScheduledRequest[];
}
interface ScheduledRequest {
id: string; // UUID
call_input_id: string; // UUID
entity_id: string; // UUID
external_reference_id?: string | null;
tenant_id: string;
workflow_status: OutboundCallStatus;
best_to_callback_after: string | null; // datetime or null
generated_outcomes?: GeneratedOutcomes | null;
created_at: string;
updated_at: string;
}
type OutboundCallStatus =
| 'ENQUEUED'
| 'ACTIVE'
| 'COMPLETE'
| 'ERROR'
| 'CANCELLED'
| 'DO_NOT_CALL_BACK';Map scheduled request IDs back to source rows:
const requestIdByExternalReference = Object.fromEntries(
response.scheduled_requests.flatMap((request) =>
request.external_reference_id
? [[request.external_reference_id, request.id]]
: []
)
);Fail or retry if any expected external reference is missing:
const missing = rows.filter((row) => !requestIdByExternalReference[row.external_reference_id]);
if (missing.length) throw new Error('Missing scheduled request IDs');Step 3: Trigger calls
Trigger a scheduled request ID to start the queued call workflow.
Request schema
interface TriggerRequestsRequest {
scheduled_request_ids: [string]; // exactly one scheduled request UUID
latency_mode?: 'low' | 'high'; // defaults to low
}latency_mode defaults to low. Use low for a conversation experience; use high only when explicitly needed.
Example request
Low latency (default):
curl -X POST "$API_URL/scheduler/trigger/v2" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"scheduled_request_ids": ["22222222-2222-4222-8222-222222222222"],
"latency_mode": "low"
}'High latency:
curl -X POST "$API_URL/scheduler/trigger/v2" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"scheduled_request_ids": ["22222222-2222-4222-8222-222222222222"],
"latency_mode": "high"
}'Response schema
interface TriggerRequestsResponse {
triggered: string[];
workflow_id?: string;
latency_mode?: 'low' | 'high';
errors?: string[];
message?: string;
}Persist which request ID was triggered. If triggering multiple scheduled requests, call this endpoint once per scheduled request ID.
Step 4: Poll status and outcomes
Poll by scheduled request ID until the request reaches a terminal status. The default response is slim and focuses on request status plus generated_outcomes. Detailed attempt history is omitted unless explicitly requested.
Request schema
This endpoint has no JSON request body.
interface PollScheduledRequestPathParams {
requestId: string;
}
interface PollScheduledRequestQueryParams {
include_call_attempts?: boolean; // defaults to false
}Example request
REQUEST_ID="22222222-2222-4222-8222-222222222222"
curl -X GET "$API_URL/scheduler/requests/$REQUEST_ID" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json"Include call attempts only when debugging or auditing a request:
curl -X GET "$API_URL/scheduler/requests/$REQUEST_ID?include_call_attempts=true" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: application/json"Response schema
interface ScheduledRequestStatus {
id: string; // UUID
workflow_status: OutboundCallStatus;
best_to_callback_after: string | null; // datetime or null
generated_outcomes: GeneratedOutcomes | null;
call_attempts?: CallAttempt[]; // present only when include_call_attempts=true
created_at: string;
updated_at: string;
}
interface CallAttempt {
id: string; // UUID
scheduled_request_id: string; // UUID
tenant_id: string;
attempt_number: number;
status: 'PLACED' | 'ANSWERED' | 'FAILED' | 'COMPLETE' | 'LEGACY_COMPLETED';
disposition?: 'ANSWERED' | 'RETRYABLE_ERROR' | 'NON_RETRYABLE_ERROR' | null;
error_detail?: string | null;
room_name?: string | null;
workflow_id?: string | null;
call_reference_time_utc?: string | null;
transcript_bucket?: string | null;
transcript_object_key?: string | null;
generated_outcomes?: Record | null;
agent_behavior_evaluation?: Record | null;
recording_id?: string | null;
created_at: string;
updated_at: string;
}
interface GeneratedOutcomes {
called: boolean;
time_of_call?: string | null;
voicemail_left?: boolean | null;
person_spoke_with?: string | null;
positive_or_negative_contact: 'positive' | 'negative' | 'unknown';
original_appointment?: { date: string; time: string | null } | null;
rescheduled_appointment?: { date: string; time: string | null } | null;
additional_dates_of_service: Array;
appointment_attendance_status:
| 'attended'
| 'canceled'
| 'canceled_rescheduled'
| 'no_show'
| 'no_show_rescheduled'
| 'unable_to_confirm'
| 'unknown';
reason_why?: string | null;
bill_sending_confirmation: 'confirmed' | 'not_confirmed' | 'not_discussed';
report_sending_confirmation: 'confirmed' | 'not_confirmed' | 'not_discussed';
}Before completion, generated_outcomes is usually null. Once transcript processing finishes, generated_outcomes contains the best representative outcome selected across attempts. The call_attempts array is hidden by default and is returned only with include_call_attempts=true.
Example responses
Default active poll response:
{
"id": "22222222-2222-4222-8222-222222222222",
"workflow_status": "ACTIVE",
"best_to_callback_after": null,
"generated_outcomes": null,
"created_at": "2026-04-09T14:59:30Z",
"updated_at": "2026-04-09T15:00:05Z"
}Default completed poll response:
{
"id": "22222222-2222-4222-8222-222222222222",
"workflow_status": "COMPLETE",
"best_to_callback_after": null,
"generated_outcomes": {
"called": true,
"time_of_call": "2026-04-09T15:00:00Z",
"voicemail_left": false,
"person_spoke_with": "Alex",
"positive_or_negative_contact": "positive",
"original_appointment": { "date": "2026-04-09", "time": "11:00" },
"rescheduled_appointment": null,
"additional_dates_of_service": [],
"appointment_attendance_status": "attended",
"reason_why": null,
"bill_sending_confirmation": "confirmed",
"report_sending_confirmation": "not_discussed"
},
"created_at": "2026-04-09T14:59:30Z",
"updated_at": "2026-04-09T15:08:30Z"
}Expanded response with include_call_attempts=true:
{
"id": "22222222-2222-4222-8222-222222222222",
"workflow_status": "COMPLETE",
"best_to_callback_after": null,
"generated_outcomes": {
"called": true,
"time_of_call": "2026-04-09T15:00:00Z",
"voicemail_left": false,
"person_spoke_with": "Alex",
"positive_or_negative_contact": "positive",
"original_appointment": { "date": "2026-04-09", "time": "11:00" },
"rescheduled_appointment": null,
"additional_dates_of_service": [],
"appointment_attendance_status": "attended",
"reason_why": null,
"bill_sending_confirmation": "confirmed",
"report_sending_confirmation": "not_discussed"
},
"call_attempts": [
{
"id": "44444444-4444-4444-8444-444444444444",
"scheduled_request_id": "22222222-2222-4222-8222-222222222222",
"tenant_id": "tenant_123",
"attempt_number": 1,
"status": "COMPLETE",
"disposition": "ANSWERED",
"error_detail": null,
"room_name": "scheduler-tenant_123-22222222",
"workflow_id": "scheduled-request-tenant_123-22222222-2222-4222-8222-222222222222",
"call_reference_time_utc": "2026-04-09T15:00:00Z",
"transcript_bucket": "transcripts",
"transcript_object_key": "tenant_123/22222222/transcript.json",
"generated_outcomes": {
"called": true,
"time_of_call": "2026-04-09T15:00:00Z",
"voicemail_left": false,
"person_spoke_with": "Alex",
"positive_or_negative_contact": "positive",
"original_appointment": { "date": "2026-04-09", "time": "11:00" },
"rescheduled_appointment": null,
"additional_dates_of_service": [],
"appointment_attendance_status": "attended",
"reason_why": null,
"bill_sending_confirmation": "confirmed",
"report_sending_confirmation": "not_discussed"
},
"agent_behavior_evaluation": null,
"recording_id": "55555555-5555-4555-8555-555555555555",
"created_at": "2026-04-09T15:00:00Z",
"updated_at": "2026-04-09T15:08:30Z"
}
],
"created_at": "2026-04-09T14:59:30Z",
"updated_at": "2026-04-09T15:08:30Z"
}Status mapping
Upstream | Local status |
|---|---|
|
|
|
|
|
|
|
|
Terminal local statuses are completed and failed. Continue polling until each triggered request is terminal or until the caller's timeout policy is reached.
State to persist
Persist enough state to resume safely:
interface IntegrationCallState {
external_reference_id: string;
entity_id?: string;
request_id?: string;
request_status: 'not_scheduled' | 'queued' | 'calling' | 'completed' | 'failed';
last_workflow_status?: string;
generated_outcomes?: GeneratedOutcomes | null;
}Idempotency and retry guidance
Do not create a second entity for a source row that already has
entity_id.Do not schedule a second request for a source row that already has
request_id.Do not trigger request IDs that are already
calling,completed, orfailed.If scheduling partially succeeds, persist successful
external_reference_idtoidmappings before retrying missing rows.If a trigger call fails or returns
errors, retry that single request ID only after confirming it is stillENQUEUEDand has no pending attempt.Treat the scheduler request ID as the source of truth after scheduling.
TypeScript direct API example
This example assumes rows are already normalized.
const apiUrl = (process.env.API_URL ?? 'https://agent-api.evergrovelabs.com/').replace(/\/$/, '');
const apiKey = process.env.API_KEY!;
const DEFAULT_WORKING_HOURS_POLICY: WorkingHoursPolicy = {
monday: { default_call_time: '09:00', windows: [{ start: '09:00', end: '17:00' }] },
tuesday: { default_call_time: '09:00', windows: [{ start: '09:00', end: '17:00' }] },
wednesday: { default_call_time: '09:00', windows: [{ start: '09:00', end: '17:00' }] },
thursday: { default_call_time: '09:00', windows: [{ start: '09:00', end: '17:00' }] },
friday: { default_call_time: '09:00', windows: [{ start: '09:00', end: '17:00' }] },
saturday: {},
sunday: {},
};
async function apiFetch<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${apiUrl}${path}`, {
...init,
headers: {
Accept: 'application/json',
Authorization: `Bearer ${apiKey}`,
...(init.body ? { 'Content-Type': 'application/json' } : {}),
...init.headers,
},
});
if (!response.ok) {
throw new Error(`${path} failed: ${response.status} ${await response.text()}`);
}
return response.json() as Promise<T>;
}
function toScheduleRequestCallInput(
row: NormalizedCallRow,
entityId: string
): ScheduleRequestCallInput {
return {
entity_id: entityId,
external_reference_id: row.external_reference_id,
outbound_phone_number: row.outbound_phone_number,
objective: 'CONFIRM_IE_ATTENDANCE',
details: {
organization_name: row.organization_name,
patient_name: row.patient_name,
patient_ssn: row.patient_ssn,
patient_dob: row.patient_dob,
...(row.patient_phone_number ? { patient_phone_number: row.patient_phone_number } : {}),
date_of_injury: row.date_of_injury,
expected_appointment_date_time: row.expected_appointment_date_time,
local_timezone: row.local_timezone,
service_modality: row.service_modality,
claim_number: row.claim_number,
insurer_group_name: row.insurer_group_name,
body_part: row.body_part,
provider_office: row.provider_office,
provider_office_address: row.provider_office_address,
provider_state: row.provider_state,
eoc_number: row.eoc_number,
},
};
}
async function runDirectApiCallFlow(rows: NormalizedCallRow[]) {
const entityIdByExternalReference: Record<string, string> = {};
for (const row of rows) {
const entity = await apiFetch<SchedulerEntity>('/scheduler/entities', {
method: 'POST',
body: JSON.stringify({
name: row.provider_office,
location_name: row.provider_office,
location_timezone: row.local_timezone,
working_hours_policy: DEFAULT_WORKING_HOURS_POLICY,
} satisfies CreateSchedulerEntityRequest),
});
entityIdByExternalReference[row.external_reference_id] = entity.id;
}
const scheduleResponse = await apiFetch<ScheduleRequestsResponse>('/scheduler/requests', {
method: 'POST',
body: JSON.stringify({
calls: rows.map((row) =>
toScheduleRequestCallInput(row, entityIdByExternalReference[row.external_reference_id])
),
} satisfies ScheduleRequestsRequest),
});
const requestIdByExternalReference = Object.fromEntries(
scheduleResponse.scheduled_requests.flatMap((request) =>
request.external_reference_id ? [[request.external_reference_id, request.id]] : []
)
);
const missingRows = rows.filter((row) => !requestIdByExternalReference[row.external_reference_id]);
if (missingRows.length) {
throw new Error(
`Missing scheduler request IDs for ${
missingRows.map((row) => row.external_reference_id).join(', ')
}`
);
}
const requestIds = Object.values(requestIdByExternalReference);
const triggerResponses: TriggerRequestsResponse[] = [];
for (const requestId of requestIds) {
triggerResponses.push(
await apiFetch<TriggerRequestsResponse>('/scheduler/trigger/v2', {
method: 'POST',
body: JSON.stringify({
scheduled_request_ids: [requestId] as [string],
latency_mode: 'low',
} satisfies TriggerRequestsRequest),
})
);
}
return {
entityIdByExternalReference,
requestIdByExternalReference,
triggerResponses,
};
}
async function pollScheduledRequest(
requestId: string,
options: { includeCallAttempts?: boolean } = {}
) {
const query = options.includeCallAttempts ? '?include_call_attempts=true' : '';
return apiFetch<ScheduledRequestStatus>(
`/scheduler/requests/${encodeURIComponent(requestId)}${query}`
);
}Integration checklist
Build all URLs from
API_URLdirectly.Send
Authorization: Bearer {API_KEY}on every API call.Keep one stable
external_reference_idper source row.Resolve and persist one
entity_idper source row.Schedule requests.
Verify the schedule response includes every expected
external_reference_id.Trigger scheduled request IDs through
/scheduler/trigger/v2, one request ID at a time.Poll request IDs through
/scheduler/requests/{requestId}until terminal.Request
include_call_attempts=trueonly when debugging or auditing attempt-level history.Persist entity IDs, request IDs, trigger state, workflow status, and generated outcomes.