Tools & Events
Let your Human Agent call functions during a conversation, and react to the agent from your own page.
Tool Calling
Tools (function calling) let the agent's LLM trigger actions while it talks: look something up, submit a form, update your UI, or notify a backend system. You define each tool's name, description, and JSON-schema parameters; the LLM decides when to call it and with what arguments.
Tools work only on Ojin's default speech pipeline. An agent configured with a speech-to-speech provider (OpenAI Realtime, ElevenLabs Agents, Hume) runs a different worker that never dispatches tool calls. You can still save tools on such an agent, but they will never fire.
Tools are configured per agent. You can manage them two ways:
In the Ojin dashboard when configuring an agent, the agent's tools editor.
Programmatically via the REST API.
Each tool has an enabled flag. A disabled tool keeps its configuration but is hidden from the LLM, so you can turn a tool off without losing its setup.
Limits worth knowing before you design a tool:
The agent executes at most 3 tool calls per model response. Further calls in the same response are dropped, and the model is told to answer with speech instead.
Back-to-back identical calls (same tool, same arguments) inside one turn are collapsed to a single delivery. A tool meant to be called repeatedly with the same arguments should vary an argument.
end_callis reserved for the built-in hang-up tool. Do not use it as a custom tool name.Only
propertiesandrequiredare read from yourparametersobject. Every other top-level JSON Schema keyword is accepted by the API and then dropped, includingtype(eventype: object),additionalProperties,$defs,oneOf,anyOf,allOfand a top-leveldescription. Express constraints inside each property rather than at the top level.
Delivery Modes
Every tool declares how its calls are delivered. There is no agent-wide default. Each tool picks its own mode.
Webhook
Ojin POSTs the call to a URL you configure, server-side
Stand up the endpoint; no browser code
Client
The widget emits a DOM event in the user's browser
Listen for the event on the embedding page
Use webhook delivery for backend actions (write to a database, call an API, send a notification). Use client delivery for things that happen in the user's browser (navigate the page, open a modal, update on-page state).
Webhook Delivery
When a webhook tool fires, Ojin sends an HTTP request to the tool's configured URL:
Method
POST, headerContent-Type: application/json.A 30-second timeout.
Redirects are not followed. Point the tool at the final URL: a
301or302reaches the model as an error.The URL must be
httporhttpsand must resolve to a public address. Loopback and private addresses (localhost,127.0.0.1,10.x,192.168.x,169.254.x,::1) are rejected, and a tool with a rejected URL is dropped for that session. You cannot point a webhook at your laptop; use a tunnel with a public hostname.
The request body carries the call under a tool_call_message object. The fields you'll use:
name
The tool that was called
parameters
The call arguments, as a JSON string (parse it before use)
tool_call_id
Unique id for this call
response_required
true when the agent is waiting for your response (see below)
The body also includes additional session metadata fields alongside tool_call_message; you can ignore them for most integrations.
The webhook request is not signed. There is no secret header. Treat the webhook URL itself as a secret, serve it over HTTPS, and validate the payload before acting on it.
Waiting for a response
Each webhook tool has a wait for response setting:
On (
response_required: true), the agent waits for your endpoint, then feeds the result back into the conversation so the LLM can use it. The result is always added to the conversation, but the agent only speaks about it in the same turn if it had not already started speaking. Otherwise the model uses it from the next turn onward. Return HTTP2xxwith a JSON body:If you omit the
resultkey, the entire JSON body is used as the result. An empty body (or204) is treated as an empty result. A non-2xxstatus, a timeout, or an unparseable body surfaces an error result to the LLM instead.Off (
response_required: false), fire-and-forget. Ojin sends the request and does not wait; the LLM immediately continues with a neutral acknowledgement. Anything your endpoint returns is ignored.
Client Delivery
When a client tool fires, the widget dispatches a DOM CustomEvent named ojinToolCall. The event fires on window (not on the <ojin-agent> element), so add the listener to window:
The arguments field is renamed to args above because arguments is a reserved word inside regular functions.
event.detail carries:
function_name
string
The tool that was called
tool_call_id
string
Unique id for this call
arguments
object
The call arguments, already parsed
Managing Tools via the REST API
Agent endpoints accept your API key, so you can manage tools programmatically. Authenticate with the X-API-Key header (see Get your API key).
Tools live on the agent itself. Set them with the agent update endpoint, PATCH /v1/agents/{agent_id}, using the tools array:
The tools array is full-replace. Tools are matched by function.name: a name already on the agent is updated in place, a new name is created, and any existing tool whose name is not in the array is deleted. To change one tool, send the complete set. To leave tools untouched, omit tools from the request entirely.
To read an agent's current tools (each returned with its server-assigned tool_id), use GET /v1/agents/{agent_id}. For the full schema, see the REST API reference.
The API enforces these limits on each tool:
function.name
Max 64 characters, ^[a-zA-Z0-9_-]+$, unique per agent
function.description
Max 1024 characters
delivery.url
Max 2048 characters, must be a valid URL
Widget Events
Beyond ojinToolCall, the <ojin-agent> widget emits DOM events you can listen for to build custom UI, transcripts, or telemetry around the conversation.
All of them are observable on window, but they get there two different ways. ojinToolCall, LatencyReport, ConnectionTimingReport and OjinSessionOutcome are dispatched on window directly. The rest are dispatched on the <ojin-agent> element and bubble up to window.
Conversation events
Event
detail
Fires when
ojinToolCall
{ function_name, tool_call_id, arguments }
A client-delivered tool is called (see above)
botIsSpeaking
{ isSpeaking: boolean }
The agent starts or stops speaking
ReceivedTranscriptTurn
{ owner: "user" | "bot", text, final: true }
One finished conversational turn. This is the event to use for a transcript: one clean, de-duplicated line per turn
ReceivedTranscriptUser
{ text, timestamp, user, final }
A user speech transcript arrives (final: false for interim)
ReceivedTranscriptBot
{ text }
A chunk of the agent's response text arrives. Raw deltas, not a finished line
ReceivedTranscriptBotPartial
{ owner: "bot", text, final: false }
The cumulative text produced by the speech service for the agent's current turn updates
BotTtsStopped
no payload (detail is null)
The agent's current spoken turn ends or is interrupted
UserStartedSpeaking
{ instanceId }
The user starts speaking
UserStoppedSpeaking
{ instanceId }
The user stops speaking
ConnectionTimeout
{ duration_s, bot_state, connection_state }
Connection setup exceeds its timeout; the widget resets and shows an error
UpdateBotState
{ botState, instanceId }
The agent's lifecycle state changes
UpdateConnectionState
{ connectionState }
The transport connection state changes
UpdateQueuing
{ queuing: boolean }
The session enters or leaves the waiting queue
Where a payload includes instanceId, it identifies the <ojin-agent> element the event came from, so a page hosting several widgets can tell them apart.
Advanced / telemetry events
These carry fine-grained audio and timing signals, useful for meters, latency dashboards, or debugging. They are more granular than the conversation events above and may change. Don't depend on them for core logic.
Event
detail
Fires when
userSpeakingChanged
{ isSpeaking: boolean }
Browser voice-activity detection toggles
userVolume
{ volume: number }
The user's microphone level is sampled
botVolume
{ volume: number }
The agent's playback level is sampled
LatencyReport
per-utterance latency breakdown
The agent reports timing for a turn
ConnectionTimingReport
connection timing breakdown
The session finishes connecting
OjinSessionOutcome
{ outcome, phase, faultDomain, sessionId, agentId, instanceId, httpStatus, elapsedMs, widgetVersion }
A connection attempt or a live session ends. See below
OjinSessionOutcome
Fires once when a connection attempt finishes and again when a live session ends, so you can measure how often visitors reach a conversation and why they don't. phase is connect or session; outcome is a value such as success, mic_permission_denied, agent_unavailable, transport_lost or ended_by_visitor. faultDomain coarsely attributes it, client_env, ojin, backend, or unknown when the outcome carries no attribution (every normal ending does).
instanceId identifies which <ojin-agent> element it came from, since the event fires on window and a page may host several. sessionId is present only once a session exists, a microphone refused before the session starts has none.
Treat the outcome vocabulary as open: new values are added as new failure modes are identified, so match on the ones you care about rather than assuming the list is closed.
Next Steps
Create & Configure: set up the agent these tools belong to
Widget Integration: embed the agent on your page
REST API: full schema for programmatic management
Last updated
Was this helpful?