Tools
Custom tools
Define function-calling tools that let your assistant call your backend during a call.
A tool gives your assistant a structured action it can take mid-conversation: look up a menu, submit a reservation, transfer the caller. Each tool is an OpenAI-style function definition plus step-by-step instructions, and a serverUrl on your backend that receives the final payload when the assistant fires the tool.
Tools are tenant-scoped: define each one for the specific use case of your assistant, then attach it via the assistant's toolIds field.
| Method | Path | Purpose |
|---|---|---|
| GET | /tools | List tools |
| GET | /tools/{id} | Get one tool |
| POST | /tools | Create a tool |
| PUT | /tools/{id} | Replace a tool |
| PUT | /tools/{id}/instruction | Set this tenant's Knowledge Base instructions |
| DELETE | /tools/{id}/instruction | Restore the OnCore Knowledge Base default |
| DELETE | /tools/{id} | Delete a tool |
Anatomy of a tool#
| Field | Required | Description |
|---|---|---|
name | No | Friendly display name (e.g. "GetMenu", "SubmitReservation"). |
serverUrl | Yes, except sendDtmf | Your endpoint that receives the tool payload when it executes (e.g. https://api.myapp.com/v1/tools/get-menu). sendDtmf tools don't call a server, so it may be omitted. |
body | Yes | The function definition (see below). |
instruction | Yes | Step-by-step guidance controlling how the assistant uses the tool (see below). |
textInstructions | No | A second set of instructions, same shape as instruction, used instead of it in text conversations: SMS, WhatsApp and web chat. Voice calls always use instruction (see below). |
body: the function definition#
body.type is "function" (a regular tool), "dynamicTransfer" (a directory-driven transfer; see Dynamic transfer), or "sendDtmf" (presses dial-pad digits into the live call; see Keypad tones below).
kb_lookup is reserved. POST /tools with that function name is a 400. Use the platform Knowledge Base tool instead. See Control when the assistant looks up.
For a function tool:
| Field | Required | Description |
|---|---|---|
body.function.name | Yes | Function name, max 64 characters (e.g. getMenu). |
body.function.description | Yes | What the function does. The model uses this to decide when to call the tool; be explicit. |
body.function.parameters | Yes | A JSON Schema object (type must be "object") describing the expected parameters, with properties and optional required. |
body.async | No | Accepted and stored, but has no effect (see below). |
body.messages | No | Tool messages, e.g. a request-start filler spoken while the tool runs. Each message has a type, a mode ("static" uses content, "generated" lets the AI phrase it), and optional content. |
instruction: controlling the flow#
The instruction is required and tightly controls the assistant's behavior around the tool:
| Field | Required | Description |
|---|---|---|
title | Yes | Short title for the instruction. |
description | Yes | What the instruction covers. |
steps | Yes | At least one step. Each step has a title, an action (what the assistant should do), a required wait_for_response boolean, optional additional_rules (strings), and an optional step_id (stable identifier, max 100 chars, used by per-assistant step overrides). |
textInstructions: a separate flow for text conversations#
Instructions are usually written for a phone call: "wait for the caller to answer", "read the options back". A texter needs different steps, such as asking for everything in one message or sending a summary to confirm. textInstructions lets one tool carry both.
| The request is | The tool has textInstructions | The assistant follows |
|---|---|---|
| A voice call | Either way | instruction |
| A text conversation | No | instruction, exactly as on a call |
| A text conversation | Yes | textInstructions |
It takes the same fields and the same validation as instruction, including step_id on steps. It is optional: leave it out and text conversations keep using instruction, so nothing changes for a tool until you add it.
Per-assistant overrides follow the same split. The step and description overrides in Instruction overrides patch instruction; their text-override counterparts patch textInstructions. The two sets are independent, and a text override only takes effect while the tool has textInstructions.
Create a tool#
POST /tools
curl -X POST https://core-api.heysadie.ai/tools \
-H "Authorization: ApiKey YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "GetMenu",
"serverUrl": "https://api.myapp.com/v1/tools/get-menu",
"body": {
"type": "function",
"function": {
"name": "getMenu",
"description": "Fetches the current menu for a location",
"parameters": {
"type": "object",
"properties": {
"locationId": {
"type": "string",
"description": "Unique ID of the restaurant location"
}
},
"required": ["locationId"]
}
}
},
"instruction": {
"title": "Fetch the menu",
"description": "How to answer menu questions using getMenu.",
"steps": [
{
"title": "Check requirements",
"action": "Ensure the caller has specified which location they are asking about.",
"wait_for_response": true
},
{
"title": "Call the tool and respond",
"action": "Invoke getMenu with the locationId, then summarize the results back to the caller.",
"wait_for_response": false
}
]
}
}'Then attach it to an assistant by including its ID in the assistant's toolIds (see Assistants).
async does nothing: every tool call blocks the turn
The field is accepted and stored, so a request setting it succeeds. Nothing reads it: there is no fire-and-forget path, and the assistant waits for your response before it can say anything else.
Design for that. Your endpoint has 30 seconds before the call is abandoned, and the caller cannot interrupt while a tool is in flight. They hear a filler line and then, if it runs long, a keyboard-typing sound. If the real work takes longer than a few seconds, acknowledge immediately and do the work afterwards rather than holding the turn open.
Keypad tones (sendDtmf)#
A sendDtmf tool presses dial-pad digits into the live call: opening a door intercom,
navigating another system's phone menu, entering an access code. It calls no server, so
serverUrl is not required.
Configure it one of two ways:
| Field | Use it when |
|---|---|
digits | There is one code. The assistant presses it with no arguments. |
codes | There are several. A map of { "name": "digits" }; the assistant chooses by name. |
Digits may contain 0 through 9, * and # only, up to 32 characters. Whitespace is allowed and
stripped before dialling; any other character is rejected. At least one of digits or
codes is required, and if you supply both, digits is the no-argument default.
Dial codes are secrets: keep them out of every field the model reads
This is the one place in the API where a configuration field holds something a caller must never hear, and the protection is a convention rather than a mechanism.
With codes, the assistant is given the names as a fixed list of choices and never sees
the digits behind them. That separation is the whole point of the feature. It is defeated the
moment a code appears somewhere the model does read: the tool's description, its step
instructions, the assistant's prompt, or a custom instruction.
Name the code for what it opens, never for what it dials: front_door, not door_4417.
An unrecognised name dials nothing and the assistant is told so, rather than guessing.
{
"type": "sendDtmf",
"function": { "name": "openDoor", "description": "Let a verified visitor in." },
"codes": { "front_door": "1234#", "loading_bay": "9*55" }
}serverUrl-based execution#
When the model decides to call the tool, the collected arguments are sent to your serverUrl. Your server does the real work and responds; the assistant carries on with the result.
serverUrl values are validated for safety at write time: unresolvable or disallowed hosts are rejected.
What your server receives#
A function tool and a dynamicTransfer tool send different envelopes. Do not write
one handler that assumes the other's shape. A function tool receives:
{
"message": {
"toolCalls": [
{
"id": "toolCall_9f2c1ab47e0d3b58",
"type": "toolCall",
"function": {
"name": "lookupBooking",
"arguments": { "bookingRef": "AB1234" }
}
}
],
"call": { "id": "0195f1e2-0000-0000-0000-000000000000" },
"phoneNumber": { "number": "+15551234567" },
"metadata": { "language": "en" }
}
}| Field | Notes |
|---|---|
message.toolCalls | An array, but one call per request today. Echo id back with your result. |
function.name | The tool's function name, as you registered it. |
function.arguments | The arguments the assistant filled in, camelCase. |
call.id | The call this fired on; use it to correlate with the end-of-call report. |
phoneNumber.number | The number the assistant is answering on. |
metadata.language | The call's current language, e.g. en. |
The request carries Content-Type: application/json and the shared secret in an
x-sadie-core-secret header. Verify it exactly as described in
Verifying webhooks before trusting the body.
phoneNumber carries two reserved fields
Alongside number, the object includes id and orgId set to a fixed placeholder rather
than real values. They are reserved and on their way out, so do not read them.
Dynamic transfer documents the other envelope: a flat
call_id, arguments and metadata, with the same secret header.
List and search tools#
GET /tools supports:
| Query param | Type | Required | Description |
|---|---|---|---|
limit | number | No | Page size. Default 50, maximum 500. |
offset | number | No | Items to skip. Default 0. |
search | string | No | Filter by name |
includeGlobal | "true" | "false" | No | Include platform-provided global tools. Default false. |
curl "https://core-api.heysadie.ai/tools?search=menu&includeGlobal=true" \
-H "Authorization: ApiKey YOUR_API_KEY"Update and delete#
PUT /tools/{id} replaces the tool with the same body as create. DELETE /tools/{id} removes it.
textInstructions is the one field PUT does not replace by omission: leave it out and the stored text instructions stay as they are, so a client written before the field existed does not wipe them. Send "textInstructions": null to remove them, after which text conversations use instruction again.
Both are ownership-checked: attempting to modify or delete a tool owned by another tenant (including global tools visible via includeGlobal) returns 403 Forbidden.
The Knowledge Base tool is one of those platform-provided tools. You cannot create, rename, or delete it. PUT and DELETE on /tools/{id}/instruction are the exception: they set or restore this tenant's Knowledge Base instructions against that same tool id. PUT takes an optional textInstructions next to the required instruction. See Control when the assistant looks up.
Next steps#
- Dynamic transfer: the
dynamicTransfertool type and your directory-server contract. - Control when the assistant looks up: tenant and assistant instructions for the Knowledge Base tool.
- Task groups: reference tools by name from structured task flows.
New to this? Custom tools & integrations explains what it does and when to use it, without the API detail.
For a complete worked example (endpoint, registration, attachment and a live call), follow Give an assistant a tool.