Skip to content

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.

MethodPathPurpose
GET/toolsList tools
GET/tools/{id}Get one tool
POST/toolsCreate a tool
PUT/tools/{id}Replace a tool
PUT/tools/{id}/instructionSet this tenant's Knowledge Base instructions
DELETE/tools/{id}/instructionRestore the OnCore Knowledge Base default
DELETE/tools/{id}Delete a tool

Anatomy of a tool#

FieldRequiredDescription
nameNoFriendly display name (e.g. "GetMenu", "SubmitReservation").
serverUrlYes, except sendDtmfYour 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.
bodyYesThe function definition (see below).
instructionYesStep-by-step guidance controlling how the assistant uses the tool (see below).
textInstructionsNoA 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:

FieldRequiredDescription
body.function.nameYesFunction name, max 64 characters (e.g. getMenu).
body.function.descriptionYesWhat the function does. The model uses this to decide when to call the tool; be explicit.
body.function.parametersYesA JSON Schema object (type must be "object") describing the expected parameters, with properties and optional required.
body.asyncNoAccepted and stored, but has no effect (see below).
body.messagesNoTool 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:

FieldRequiredDescription
titleYesShort title for the instruction.
descriptionYesWhat the instruction covers.
stepsYesAt 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 isThe tool has textInstructionsThe assistant follows
A voice callEither wayinstruction
A text conversationNoinstruction, exactly as on a call
A text conversationYestextInstructions

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

bash
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:

FieldUse it when
digitsThere is one code. The assistant presses it with no arguments.
codesThere 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.

A sendDtmf tool with named codes
{
  "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:

POST to your serverUrl
{
  "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" }
  }
}
FieldNotes
message.toolCallsAn array, but one call per request today. Echo id back with your result.
function.nameThe tool's function name, as you registered it.
function.argumentsThe arguments the assistant filled in, camelCase.
call.idThe call this fired on; use it to correlate with the end-of-call report.
phoneNumber.numberThe number the assistant is answering on.
metadata.languageThe 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 paramTypeRequiredDescription
limitnumberNoPage size. Default 50, maximum 500.
offsetnumberNoItems to skip. Default 0.
searchstringNoFilter by name
includeGlobal"true" | "false"NoInclude platform-provided global tools. Default false.
Terminal
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#


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.