Tool calling lets the LLM driving your voice session take action: query a database, schedule a visit, transfer to a human. The model decides when to invoke a tool from your prompt; Speko POSTs a Standard Webhooks-signed request to your endpoint, folds the JSON response back into the model’s next turn, and the agent verbalizes the result. This guide walks through registering a tool, hooking it into a LiveKit-powered voice session, and confirming it fires.Documentation Index
Fetch the complete documentation index at: https://docs.speko.dev/llms.txt
Use this file to discover all available pages before exploring further.
Architecture
- Your endpoint — a public HTTPS URL that receives the tool call and returns JSON.
- The Speko dashboard — where you register the tool (name, description, JSON Schema parameters, your endpoint URL). Speko stores an HMAC signing secret you save once.
- A LiveKit voice session — the worker fetches your registered tools at session start, exposes them to the LLM, and routes invocations through the executor.
1. Build your endpoint
The executor POSTs the LLM-generated arguments as JSON. Whatever you return becomes the model’s next observation, so keep responses small and specific.src/server.ts
Verifying the signature
Production endpoints MUST verify the Standard Webhooks signature on every request. Speko sends three headers:| Header | Meaning |
|---|---|
webhook-id | Idempotency key for this delivery. Skip duplicates. |
webhook-timestamp | Unix seconds when Speko signed the body. Reject anything older than ~5 minutes to prevent replay. |
webhook-signature | v1,<base64(HMAC-SHA256("{webhook-id}.{webhook-timestamp}.{body}", secret))>. Multiple comma-separated signatures may appear during rotation; accept if any one matches. |
standardwebhooks package — constant-time comparison and clock-skew tolerance are tricky to roll yourself.
2. Register the tool
Via the dashboard
Open/tools in the dashboard, click Add tool, fill in:
- Name —
snake_case, ≤ 64 chars (e.g.lookup_pet). The model sees this; pick something it’ll match against the user’s intent. - Description — tell the model when to call this. Be explicit (“ALWAYS call this when the user asks about a specific pet by name”).
- Parameters — a JSON Schema. Strict typing works; vague typing leads to the model passing garbage args.
- Webhook URL — your public HTTPS endpoint from step 1. Speko rejects HTTP, private/loopback hosts, and known cloud-metadata IPs at registration time.
Via the API
secret you POST is what Speko uses to sign webhook deliveries. The server stores an encrypted copy and never echoes it back, so keep your local copy.
The _default agentId
Until per-agent worker management ships, every tool you register lives under the _default agentId. That’s the worker’s default lookup key as well. When the per-agent agents table arrives, the sentinel migrates and existing tools keep working — no migration required on your side.
3. Wire the worker
If you run a LiveKit Agents worker (the typical Speko-hosted setup), the adapter loads your registered tools at session start and merges them with anything the framework provides at runtime. UsecreateSpekoComponents with the registered-tools options:
agent.ts
agentId, the loader stays disabled and the agent only sees runtime tools — useful when you want to opt in selectively.
4. Run a call
The simplest client is a browser using@spekoai/client:
/api/session server route mints the LiveKit token via Speko:
What gets sent over the wire
When the model invokes a registered tool, Speko’s executor signs the request with your secret and POSTs to your URL:Debugging
Common failure modes:- Tool never invoked. The model didn’t decide to call it. Tighten the description (be explicit about when to call), or set
toolChoice: "required"in your call options to force one. - Webhook never lands. Check the worker logs for the executor span. Common: 403 from your endpoint (signature mismatch), 5xx (your code threw), or timeout (your endpoint is too slow — budget under 4 seconds).
- Agent says “couldn’t find” instead of the real result. Your endpoint returned 4xx. Either the query genuinely missed, or the model passed empty/wrong args. During development, have your endpoint echo back the body it received so you can spot the latter.
- Two voices overlap in the room. A second agent dispatched into the same room without ending the previous session. Always call
endSession()on yourVoiceConversation(or disconnect the participant) before opening a new conversation.
What’s next
- Per-agent worker config so you can scope tools to specific agents instead of the
_defaultsentinel. - Built-in tools — Speko-provided helpers (calendar, current time, transfer-to-human) you opt into without running your own webhook.
- Streaming tool results for long-running queries.