Skip to content

Assistants ​

An assistant is a versioned bundle of behaviour: a prompt, a transcriber, a model, a voice, the tools it may call, and the plans that govern when it speaks, when it stops and what it records. It is the unit a call runs.

Create one per job, not per customer. Per-caller differences belong in variables, not in a second assistant.

Draft, version, call ​

Editing an assistant changes its draft. publish freezes the draft into an immutable version row and points the assistant at it. A call pins the version it started with, so republishing mid-call cannot reach a live session.

GET /assistants/{id} reports has_unpublished_changes, which is what the dashboard's badge reads. version is 0 until the first publish.

POST /api/v1/assistants/{id}/publish            → 201 assistant_version
POST /api/v1/assistants/{id}/unpublish
GET  /api/v1/assistants/{id}/versions
GET  /api/v1/assistants/{id}/versions/{version}
POST /api/v1/assistants/{id}/versions/{version}/restore

A published version carries a config_hash, tool_refs and knowledge_base_refs — which is how you tell whether two calls really ran the same configuration.

Create and edit ​

The assistants API is snake_case. A PATCH changes only the fields it names.

bash
curl -X PATCH "$CALLAGENT_API_URL/assistants/$ASSISTANT_ID" \
  -H "Authorization: Bearer $CALLAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.4 },
    "silence_timeout_seconds": 20
  }'

PUT and PATCH both route to the same handler. Only PUT is in the OpenAPI document; PATCH works and is undocumented there.

Top-level fields ​

FieldNotes
nameRequired, ≤255.
description≤255.
modepipeline (default) or realtime. See the warning under Speech to speech.
language≤16, e.g. en, ro. Plus additional_languages[].
first_message≤5000.
first_message_modeassistant_speaks_first · assistant_speaks_first_with_model_generated_message · assistant_waits_for_user.
system_prompt≤100000.
template_strictnesslenient · warn (default) · strict — what happens when a template names a variable nothing supplies.
silence_timeout_seconds1–600, default 30.
max_duration_seconds10 to the platform ceiling, default 1800.
transcriber{ provider, model, language, … }.
model{ provider, model, temperature, … }.
voice{ provider, voice_id, … }.
speech_to_speech_modelStored and ignored — see below.
end_call_phrases[], idle_messages[], background_messages[], pronunciations[], custom_keywords[]
server{ url, … } — the fallback server for function tools and hooks. server.url must be a valid URL.
hooks[]14 trigger points.
fallbacks{ model: [], transcriber: [], voice: [] }, max two each, index 0 first.
variables[], variable_values{}See Variables.
knowledge_only, knowledge_only_plan
metadata{}Your keys, passed through untouched.
tools[], tool_ids[], knowledge_base_ids[], structured_output_ids[]Attachments; every id is checked against your organization.

The plan blocks ​

Thirteen plan blocks are accepted as free-form objects, and each is flat snake_case. Sending a nested camelCase block from @callagent/contracts will store cleanly and never reach the gateway.

BlockKeys
start_speaking_planwait_seconds, smart_endpointing_provider, completion_threshold, smart_endpointing_ms, on_punctuation_seconds, on_no_punctuation_seconds, on_number_seconds, custom_rules[]
stop_speaking_planenabled, num_words, voice_seconds, backoff_seconds, acknowledgement_phrases[], interruption_phrases[]
artifact_planrecording_enabled, recording_format, stereo_recording, video_enabled, transcript_enabled, messages_enabled, events_enabled, logs_enabled, debug_json_enabled, retention_days, pii_redaction, post_call_transcription.{enabled,provider,model,language}
analysis_plansummary_enabled, summary_prompt, sentiment_enabled, topics_enabled, success_evaluation_enabled, success_evaluation_prompt, success_evaluation_rubric, model, timeout_seconds
message_planidle_timeout_seconds, idle_max_count, idle_message_reset_count_on_user_speech
keypad_input_planenabled, delimiters[], timeout_seconds, max_digits, mute_assistant_while_typing
voicemail_detection_planenabled, strategy, beep_max_await_seconds, message
recording_consent_planrequired, mode, prompt, audio_url, on_refusal, regions[]
observability_planprovider, endpoint, project, sample_rate, include_prompts, include_transcripts, tags, metadata
compliance_planhipaa, pci, zero_data_retention
transfer_plandestinations[]
knowledge_only_planaction, message, min_passages, min_score
background_speech_denoisingenabled, mode, aggressiveness

Plus background_sound ({ type, volume } or { type: "custom", url, volume }; types are off, office, call-center, cafe, street, nature) and voice_formatting (enabled, min_characters, max_wait_ms, expand_numbers, expand_currency, expand_dates, expand_abbreviations, read_emails_naturally, strip_markdown).

Some of these do less than they look

Fields nothing reads at run time, verified by searching the gateway source:

  • analysis_plan configures a pipeline that never runs. Summary, sentiment, topics and success evaluation are not produced on a live call.
  • compliance_plan reaches the gateway's snapshot and is never read.
  • observability_plan applies tags and sampling; no spans are shipped.
  • model.num_fast_turns is converted onto the wire and the gateway never reads it. fast_model is not even converted — it exists only in the contract. Opening turns are not routed to a cheaper model.
  • model.emotion_recognition is converted and never read.
  • voice_formatting becomes the contract's chunkPlan, which the gateway never reads. Text normalisation before synthesis is not configurable today.
  • pronunciations becomes pronunciationDictionary, which the gateway never reads.
  • artifact_plan.storage_path_template is ignored; the recorder hardcodes its path.

What is read: max_tool_iterations, context_window_messages, background_sound, keypad_input_plan, voicemail_detection_plan, message_plan, end_call_phrases, idle_messages, background_messages, fallbacks, knowledge_base_ids, variable_values, and the two speaking plans.

See What is not built.

Speech in, thinking, speech out ​

json
{
  "transcriber": {
    "provider": "deepgram",
    "model": "nova-3",
    "language": "en",
    "smart_format": true,
    "numerals": true,
    "diarization": false,
    "profanity_filter": false,
    "auto_language_detection": false
  },
  "model": {
    "provider": "openai",
    "model": "gpt-4o-mini",
    "temperature": 0.4,
    "max_tokens": 400,
    "tool_choice": "auto",
    "max_tool_iterations": 5,
    "context_window_messages": 60,
    "num_fast_turns": 2,
    "fast_model": "gpt-4o-mini",
    "timeout_seconds": 30
  },
  "voice": {
    "provider": "elevenlabs",
    "voice_id": "rachel",
    "speed": 1.0
  }
}

Defaults worth knowing: temperature 0.4, max_tokens 400, max_tool_iterations 5, context_window_messages 60, model timeout_seconds 30, voice speed 1.0.

The API prunes these blocks, and one of the casualties breaks every call

POST and PATCH /assistants use Laravel's $request->validated(), which returns only the keys that had a validation rule. Inside transcriber, model, voice and server the request class declares rules for exactly five sub-keys:

transcriber.provider · model.provider · model.model · voice.provider · server.url

Every other sub-key of those four blocks is silently discarded. Sent, accepted with a 201, and gone:

bash
# sent
"voice": { "provider": "elevenlabs", "voice_id": "rachel", "speed": 1.1 }
"model": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.9 }
# stored
"voice": { "provider": "elevenlabs" }
"model": { "provider": "openai", "model": "gpt-4o-mini" }

So voice_id, speed, stability, temperature, max_tokens, tool_choice, num_fast_turns, context_window_messages, transcriber.model, transcriber.language, smart_format and the rest cannot be set over the API at all.

That would be an annoyance if it stopped there. It does not: voice.voice_id is one of the five fields the gateway's call config cannot be built without. Without it the converter returns nothing, the call has no configuration, and the caller hears a dead line. validate checks the other four and not this one, so the assistant validates and publishes cleanly.

An assistant whose voice block was last written through the REST API cannot start a call. The CLI posts to the same endpoint and inherits the problem.

The plan blocks are unaffected — they have no sub-rules, so start_speaking_plan, artifact_plan, metadata, variable_values and background_sound pass through whole. And the dashboard writes the model directly rather than through this request class, so an assistant configured there is complete.

Set the voice in the dashboard until this is fixed.

Transcriber keywords are not transcriber.keywords. They come from the assistant's top-level custom_keywords, which accepts plain strings or { "term": …, "boost": … } objects. This is the single highest-value transcriber setting for brand and product names the transcriber would otherwise mishear.

num_fast_turns and fast_model are intended to route the opening turns of a call to a smaller model, on the theory that "Hi, how can I help?" does not need the expensive one. Neither is read by the gateway. Every turn uses model.model.

The transcriber must be one the gateway has a live adapter for. validate enforces this at publish, because a transcriber the gateway cannot name invalidates the whole call config, not just that field, and the caller hears a dead line.

Provider confidence

Nineteen voice providers are declared; two are flagged as reviewed against the vendor's documentation, and none has been run against a live vendor in CI. See Providers and What is not built.

Fallbacks ​

json
{ "fallbacks": { "voice": [ { "provider": "azure", "voice_id": "en-GB-SoniaNeural" } ] } }

Ordered, maximum two each, index 0 tried first. See Providers.

Speech to speech ​

speech_to_speech_model accepts a provider and model, stores them, versions them and publishes them.

It is ignored at run time

The live call session never reads the field. An assistant configured for speech-to-speech runs the ordinary transcribe → model → synthesise pipeline. Setting mode: "realtime" does not change that; it only changes which fields validate insists on.

Variables ​

Two halves. variables[] declares them; variable_values{} supplies defaults.

json
{
  "variables": [
    { "name": "company_name", "source": "static", "default_value": "Example Ltd", "required": true },
    { "name": "plan_tier",    "source": "call" }
  ],
  "variable_values": { "company_name": "Example Ltd" },
  "system_prompt": "You are a support agent for {{company_name}}. The caller is on the {{plan_tier}} plan."
}

name must match ^[A-Za-z_][A-Za-z0-9_]*$. source is one of static, call, contact, webhook, tool, handoff, environment.

Syntax and what is available ​

The syntax is {{dotted.path}}. It is not Liquid; there are no filters, no conditionals, no loops. An unresolved path renders as an empty string.

Substitution runs on the system prompt, the first message, background messages, idle messages, the recording-consent announcement and the voicemail message.

Built-ins available at run time:

PathValue
{{call.id}}The call's ULID.
{{call.from}}The caller's number.
{{customer.name}}, {{customer.number}}, {{customer.email}}, {{customer.language}}, {{customer.id}}, {{customer.external_id}}, {{customer.attributes.*}}The customer context, when one was supplied.

Precedence: assistant variable_values, then per-call variables passed to POST /calls, which win.

There is no {{now}} or {{date}} at call time. The dashboard preview resolver does expose an env namespace with now, date, time, timezone, region and environment, and it is a curated allow-list rather than the process environment — but that resolver renders previews, not live calls.

POST /assistants/{id}/render-prompt renders the prompt against a context you supply and returns missing, which is the fastest way to find a typo in a path.

Hooks ​

hooks[] is meant to fire an action at a named point. Fourteen triggers:

call.start · call.end · call.ending · speech.update · transcript.final · tool.calls · tool.completed · customer.speech.timeout · customer.speech.interrupted · assistant.speech.interrupted · phone.call.control · silence.timeout · handoff · transfer.failed

Each hook has optional filters (oneOf, regex, equals) and at least one action: say, tool, transfer, handoff, endCall, setVariable, webhook.

Only handoff hooks actually fire

Hooks are converted onto the wire and shipped to the gateway. The gateway source contains no reference to them. The control plane runs hooks for exactly one event — handoff, through its own runner. The other thirteen triggers are stored, published, transmitted and ignored.

Use a webhook or a tool for anything you were planning to do with the rest.

A hook with an unrecognised action is dropped during conversion, not rejected. Only say, transfer, tool and end_call survive it; handoff, setVariable and webhook do not reach the wire at all.

Inspecting ​

GET  /api/v1/assistants/{id}/validate       publish-time checks, without publishing
GET  /api/v1/assistants/{id}/preview        the resolved snapshot
POST /api/v1/assistants/{id}/render-prompt  render templates against a context

validate checks structure and adapter availability. It does not check that a credential exists or works, or that attached tools resolve. See Quickstart step 4 for the exact list.

preview returns the resolved snapshot in the API's snake_case shape. It is not the gateway's wire format — that is a second conversion, which also drops tools it cannot map.

As a file ​

yaml
name: Support Line
firstMessageMode: assistant-speaks-first
firstMessage: Hi, you have reached support. How can I help?
systemPrompt: |
  You are a support agent for {{company_name}}.
  Be brief. Ask one question at a time.
transcriber: { provider: deepgram, model: nova-3, language: en }
model:       { provider: openai, model: gpt-4o-mini, temperature: 0.4 }
voice:       { provider: elevenlabs, voiceId: rachel }
tools:
  - lookupOrder
knowledgeBases:
  - Product manual

The CLI speaks the contracts vocabulary — camelCase, hyphenated enum values — and converts to the API's spelling itself. tools and knowledgeBases name resources and the CLI resolves them. See Config as code.

API ​

GET/POST         /api/v1/assistants
GET/PATCH/DELETE /api/v1/assistants/{id}
POST             /api/v1/assistants/{id}/duplicate
POST             /api/v1/assistants/{id}/publish
POST             /api/v1/assistants/{id}/unpublish
GET              /api/v1/assistants/{id}/versions
GET              /api/v1/assistants/{id}/versions/{version}
POST             /api/v1/assistants/{id}/versions/{version}/restore
GET              /api/v1/assistants/{id}/preview
POST             /api/v1/assistants/{id}/render-prompt
GET              /api/v1/assistants/{id}/validate

Full parameters, request bodies and responses: assistant reference.

Documented against the code. Anything not yet implemented is labelled, never described as working.