sphoroVOICEdocs
Markdown

Tools and functions

A knowledge base lets the agent know something. A function lets it do something — look up this caller's order, book the slot, hand the call to a person. The model decides when; you decide what is possible.

What is available

GET/v1/functions
Shell
curl -s https://voice.sphoro.com/v1/functions \
  -H "Authorization: Bearer $SPHORO_API_KEY"

This lists what your account can actually use, which is the only list worth trusting — naming a function on an agent that this endpoint does not return is refused.

The three built in

ToolDoesGiven to the agent by
search_knowledge_baseSearches the documents attached to this agent, mid-turn, before it answers.Attaching at least one base in knowledge_base_ids. Naming it in tools does nothing.
transfer_callHands the caller to a person.Setting transfer_number on the agent. That is the only number it can ever dial — the model chooses when, never where.
end_callEnds the call, with a reason in the model's own words that is written onto the call record.Available to an agentic agent; switchable under autonomy: "custom".
Two of the three are enabled by configuring the thing they act on, not by listing them. This trips people up constantly: an agent with "tools": ["transfer_call"] and no transfer_number has no transfer tool, and the model never mentions transferring because it cannot see one.

Functions that call your own API

The registry also runs tools backed by your REST endpoints: the model calls get_order_status, we validate the arguments, call your URL, and hand the response back to the model to speak from.

A tool like that is described by four things:

JSON
{
  "name": "get_order_status",
  "description": "Look up the status of an order by its reference. Ask the caller to read the reference back before calling this.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "pattern": "^AC-[0-9]{5}$" }
    },
    "required": ["order_id"],
    "additionalProperties": false
  },
  "url": "https://api.acme.example/orders/{{order_id}}",
  "method": "GET"
}
You do not have to send us that. An agent carries its own tools in tool_configs, and the endpoint above lists what the deployment registered — the shared, code-defined ones. Most agents have both.

Tools an agent carries itself

tool_configs on an agent is a list of tools defined by whoever built the agent, each carrying the configuration that makes it mean something: which calendar, which person, which endpoint. It replaces the whole list on every write, so a tool left out of it is removed.

There are four kinds.

kindDoesConfigured by
calendar_availabilityReads the open slots, so the agent offers times that are actually free.calendar: a Cal.com api_key, an event_type_id and a timezone.
book_appointmentWrites the agreed time in, and reads the reference back.The same calendar block. Give the agent both: one to offer times, one to take one.
transfer_callHands the call to a person, at its own destination.transfer: a destination in E.164 or a sip: URI, and an optional webhook_url told before the call moves.
custom_functionCalls any endpoint of yours.request: a method, a url, headers, and a table of parameters.
JSON
{
  "tool_configs": [
    {
      "kind": "custom_function",
      "name": "get_order_status",
      "description": "Look up an order by its reference. Ask the caller to read the reference back before calling this — a wrong reference returns somebody else's order.",
      "pre_tool_message": { "en-IN": "Let me pull that order up." },
      "request": {
        "method": "GET",
        "url": "https://api.acme.example/orders/{{order_id}}",
        "headers": { "Accept": "application/json" },
        "secret_header_values": { "Authorization": "Bearer sk_live_…" },
        "parameters": [
          { "name": "order_id", "type": "string", "in": "path", "required": true,
            "description": "The order reference the caller read out, like AC-10293." }
        ],
        "timeout_seconds": 8
      }
    },
    {
      "kind": "transfer_call",
      "name": "escalate_to_billing",
      "description": "Put the caller through to billing when they dispute a charge.",
      "transfer": { "destination": "+15559998888" }
    }
  ]
}

Credentials go one way

A calendar key travels as {"api_key": {"value": "cal_live_…"}} and comes back as {"api_key": {"hint": "••••1234"}}; sealed headers go out under secret_header_values and come back as secret_header_names alone. Leaving either out on an update keeps what is stored — which is what lets you change a description without resending the key.

The pre-tool message

A tool can take seconds, and silence on a phone reads as a dropped line. pre_tool_message is what the agent says while it runs, keyed by language code, and it is per tool because the wait is not the same wait: "let me check the diary" and "let me put you through" describe different things happening. An agent's own filler_phrase covers any tool that has none.

Where else the work could go

Instead of a live toolUseDifference
Work that can happen after the callA workflow's trigger_webhook stepRuns once the conversation is over. The caller never waits for it.
Work between calls in a sequenceA journey's api nodeCalls your endpoint between calls and branches on the answer.

Most things people reach for a live tool to do turn out not to need to be live. "Write the outcome to our CRM" does not have to happen while the caller is on the line; "tell the caller their balance" does.

Writing the description

The description is a prompt, not documentation. It is the only thing the model reads when deciding whether to call this tool, and rewriting it is the highest-leverage change you can make to a tool that fires at the wrong times.

Instead ofWrite
"Books an appointment.""Reserve a slot on the clinic calendar. Confirm the date and time with the caller first, and never call this twice for one caller."
"Gets order status.""Look up an order by its reference. Ask the caller to read the reference back before calling this — a wrong reference returns somebody else's order."

Say when to call it, what to do first, and when not to. Those three sentences do more than any schema change.

Arguments are validated before your endpoint sees them

A model will, reliably and forever, invent argument names, omit required fields and pass strings where numbers belong. Every call is checked against the schema first, and every problem is reported at once.

Supported
Typesobject, array, string, number, integer, boolean, null
Presencerequired, additionalProperties
StringsminLength, maxLength, pattern, enum
Numbersminimum, maximum, exclusiveMinimum, exclusiveMaximum
Arraysitems, minItems, maxItems
Objectsproperties, nested to any depth
CombinatorsallOf, anyOf, oneOf
Formatsemail, date-time, uri, phone
Always set "additionalProperties": false. Without it, a model that invents an extra argument has it silently accepted and passed to your handler. With it, the invention is caught and the model is told, on the same turn, so it corrects itself.

Coercion is deliberately narrow

Models routinely emit "3" where a number belongs. Only the unambiguous cases are converted; anything else is left alone so validation still catches it.

"3"      ──▶ 3        (integer)
"0.7"    ──▶ 0.7      (number)
"true"   ──▶ true     (boolean)
"maybe"  ──▶ rejected

A validation failure is text, not an error

When arguments do not validate, the model is handed the failure as the tool result rather than the call being aborted. It then corrects itself on the next turn, usually within the same breath. That feedback loop is most of what makes tool calling reliable on a live call, and it is why a badly-specified schema shows up as a slow call rather than a broken one.

What the caller hears while it runs

A tool call is a real network round-trip inside a live conversation. Two things to set:

SettingDoes
filler_phrase on the agentSomething said while the tool runs — "let me check that for you". Makes a three-second lookup sound like thinking rather than a dropped line.
max_tool_rounds in capabilitiesHow many times one turn may call a tool and generate again. 1–10, default 3. Every round is a full model call plus your API, so a bigger number is a longer silence rather than a more thorough answer.

Watching tool calls

call.tool.called fires on every invocation, carrying the tool name — on webhooks and on the per-call event stream. It is the first thing to look at when a call has an unexplained pause in the middle: if the pause lines up with a tool call, your endpoint is the latency, not the model.

GET/v1/calls/{id}/events
Shell
curl -sN https://voice.sphoro.com/v1/calls/$CALL_ID/events \
  -H "Authorization: Bearer $SPHORO_API_KEY"

Restricting tools to part of a conversation

On a flow, each node can narrow or force the tools available while the call is on it — tools restricts what the model can see, and tool forces one to be called. A transfer node that must transfer, a booking node that must book. That is usually a better answer than writing "only book once you have confirmed" into a prompt and hoping.