Skip to main content

Agent Tools

Create, test and manage the custom REST tools your WhatsApp and voice agents call at reply time, using the same definition the console writes.

A custom tool is one of your own read-only HTTP endpoints, described so a model can call it mid-conversation. These routes are the API behind the console's Tools screen: create the definition, test it against your real endpoint, and attach it to one agent or to all of them.

Custom tools vs built-in tools

Two different mechanisms, and this page covers only the first.

Custom REST toolsBuilt-in tools
What it isYour endpoint, described by youOurs: WhatsApp sends, CRM reads, storefront connectors
Managed byThese endpointsconfig.tools on the bot, listed by GET /api/v1/bots/tool-catalog
Attached bybot_id on the tool rowTool name in the bot's config

A tool with bot_id: null is available to every agent in the workspace. Set bot_id to scope it to one.

Credential class: dashboard JWT or a cm_ key

Authorization: Bearer cm_your_api_key
CallerRequirement
cm_ API keybots:read to list and read. bots:write to create, update, delete and test
Dashboard JWTRead for any member. Create, update, delete and test require owner or admin (403 Only owners/admins can manage agent tools otherwise)

Base URL for every example: https://api.callmissed.com. Errors are {"detail": "..."}.

A custom tool makes real outbound requests with credentials you store, exactly like a webhook. bots:write is a powerful scope: give it only to keys that need to author tools, and use a bots:read key for anything that only inspects them.

The tool object

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "tenant_id": "a0b1c2d3-4455-6677-8899-aabbccddeeff",
  "bot_id": null,
  "name": "search_products",
  "description": "Search the catalogue by keyword. Returns price, stock, the product link and an image URL.",
  "method": "GET",
  "url": "https://api.yourstore.com/agent/products/search",
  "header_params": [
    { "name": "X-API-Key", "type": "string", "description": "", "required": false, "source": "static", "secret": true, "value": null }
  ],
  "path_params": [],
  "query_params": [
    { "name": "q", "type": "string", "description": "what the customer is looking for", "required": true, "source": "llm", "secret": false, "value": null }
  ],
  "body_params": [],
  "response_mapping": {},
  "timeout_seconds": 10,
  "phase": "on_call",
  "enabled": true,
  "has_secrets": true,
  "created_at": "2026-09-08T12:00:00Z",
  "updated_at": "2026-09-08T12:00:00Z"
}
FieldTypeNotes
namestring1-64 chars. Must start with a letter; letters, digits and underscores only. This is the name the model sees, so write it as a verb: search_products, get_order_status. Unique per workspace
descriptionstringMax 2000 chars. The only thing telling the model when to reach for this tool. Write it as an instruction, not a label
methodstringGET, POST, PUT, PATCH or DELETE. Default GET
urlstringMax 2048 chars, http or https, with a hostname. May contain {placeholders} in the path or query only
header_paramsarrayUp to 20 per location. See Parameters
path_paramsarrayEvery declared name must appear as a {placeholder} in the URL, and vice versa
query_paramsarray
body_paramsarray
response_mappingobject{output_key: "dotted.path"}, up to 20 entries. Empty means the raw body is returned
timeout_secondsinteger1-30, default 10
phasestringpre_call, on_call or post_call. Only on_call is resolved into the live tool list today, so leave it at the default
enabledbooleanA disabled tool is never offered to the model
has_secretsbooleanWhether an encrypted value is stored. The values themselves are never returned

A secret parameter always serialises as "secret": true, "value": null. There is no endpoint that reads a stored secret back, including for the account that wrote it.

Parameters

Each parameter says where it goes on the wire (its location) and where its value comes from (its source).

Locations: header, path, query, body. At most 20 each.

Sources:

sourceWho supplies the valueUse it for
llmThe model, per callThe search term, the order number, anything the customer states
staticYou, once, in valueYour API key, a fixed store_id, a version pin
contextThe server, per turn, from the live conversationThe identity of the person actually in the chat

A context parameter's value is a key, not a literal, and must be one of contact_phone, contact_name, contact_email, conversation_id. The model cannot see or set it, and that is what makes an order lookup safe. If the conversation has no such value (a voice call with no linked contact), the parameter is omitted unless you mark it required, in which case the call fails cleanly instead of running an unscoped query.

FieldTypeNotes
namestring1-64 chars, starting with a letter or digit; letters, digits, _, . and -. Unique within its location (headers case-insensitively). Two llm parameters may not share a name across locations
typestringstring, integer, number or boolean. Header and path parameters must be string
descriptionstringTruncated at 500 chars. Shown to the model for an llm parameter
requiredbooleanDefault false
sourcestringllm, static or context. Default llm
valuestringRequired for static (max 2048 chars, no control characters) and for context (a context key). Ignored otherwise
secretbooleanOnly meaningful with static. Defaults to true for a static header, because that is where API keys live. Elsewhere it defaults to false

These headers cannot be set, because the transport computes them or they would let a definition address one origin while connecting to another: Host, Content-Length, Transfer-Encoding, Connection, Upgrade, Keep-Alive, TE, Trailer, Expect, Proxy-Authorization, Proxy-Connection.

Endpoint requirements

Your URL must be reachable on the public internet. Requests to private, loopback and cloud-metadata addresses are refused at save time and again on every request, including after each redirect, so a hostname re-pointed later does not get through either. At most 3 redirects are followed.

A response is read up to 32 KB and truncated past it, with truncated: true on the result. Return the handful of fields a customer actually asks about rather than a full catalogue document. /docs/connect-your-store covers how to shape a response for a model.

GET /api/v1/agent-tools

Lists the workspace's tools, newest first, up to 500. Requires bots:read.

ParameterTypeNotes
bot_iduuidNarrows to that agent's effective set: its own tools plus the workspace-wide ones
curl "https://api.callmissed.com/api/v1/agent-tools?bot_id=b1f2c3d4-5678-90ab-cdef-1234567890ab" \
  -H "Authorization: Bearer cm_your_api_key"

Returns an array of tool objects.

POST /api/v1/agent-tools

Creates a tool. Returns 201 and the tool object. Requires bots:write.

Takes every field of the tool object except the server-set ones (id, tenant_id, has_secrets, created_at, updated_at), plus bot_id, phase and enabled.

curl -X POST https://api.callmissed.com/api/v1/agent-tools \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "get_order_status",
    "description": "Look up the caller'\''s most recent orders and their delivery status. Use when a customer asks where their order is.",
    "method": "GET",
    "url": "https://api.yourstore.com/agent/orders",
    "header_params": [
      { "name": "X-API-Key", "source": "static", "value": "your_endpoint_key" }
    ],
    "query_params": [
      { "name": "phone", "source": "context", "value": "contact_phone", "required": true }
    ],
    "timeout_seconds": 10
  }'
StatusMeaning
409A tool with that name already exists in this workspace
422The definition is invalid. The detail names the problem: an undeclared path placeholder, a duplicate model-facing name, a static parameter with no value, a forbidden header, a context key outside the allowlist, a timeout outside 1-30
422Tool limit reached. A workspace holds up to 100 tools
404bot_id is not an agent in this workspace

GET /api/v1/agent-tools/{tool_id}

One tool object. 404 if it belongs to another workspace. Requires bots:read.

PATCH /api/v1/agent-tools/{tool_id}

Every field is optional. Returns the updated tool object. Requires bots:write.

The whole definition is re-validated on each update, because the URL and the path parameters constrain each other. Omitting a parameter list keeps the stored one; sending a list replaces it.

curl -X PATCH https://api.callmissed.com/api/v1/agent-tools/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

To keep a stored secret, send the parameter with "secret": true and no value. To rotate it, send the new value. There is no way to read the old one first.

DELETE /api/v1/agent-tools/{tool_id}

Returns 204. Requires bots:write.

POST /api/v1/agent-tools/test

Runs a definition that has not been saved, so you can check an endpoint before committing to it. Requires bots:write.

Takes the same body as POST /api/v1/agent-tools, plus arguments: the values a model would have supplied for the llm parameters.

curl -X POST https://api.callmissed.com/api/v1/agent-tools/test \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "search_products",
    "description": "Search the catalogue by keyword.",
    "method": "GET",
    "url": "https://api.yourstore.com/agent/products/search",
    "query_params": [ { "name": "q", "source": "llm", "required": true } ],
    "arguments": { "q": "line follower kit" }
  }'

POST /api/v1/agent-tools/{tool_id}/test

The same run for a saved tool, using its stored secrets. Requires bots:write.

curl -X POST https://api.callmissed.com/api/v1/agent-tools/7c9e6679-7425-40de-944b-e07fc1f90ae7/test \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "arguments": { "q": "line follower kit" } }'

Test results

Both test endpoints return 200 with a result object, whether or not your endpoint answered. A failure of yours is reported in the body, not as an HTTP error on ours.

{
  "ok": true,
  "status": 200,
  "result": { "count": 1, "products": [{ "id": "kit-104", "price": 1180 }] },
  "truncated": false
}
FieldNotes
oktrue when your endpoint answered 2xx
statusYour endpoint's status code, or null when no response was received
resultThe body, or the mapped subset when response_mapping is set
truncatedtrue when the response exceeded 32 KB
errorPresent in place of result only when no usable response came back. See the vocabulary below
detailPresent only with invalid_arguments, naming the offending argument

A 4xx or 5xx from your endpoint is not an error. It comes back as ok: false with status set and result holding what your endpoint said, so you can read the reason. Branching on error alone will miss it: check ok, then look at status and result.

error comes from a fixed vocabulary so that nothing about the failure leaks back through the model. status is null with it unless the failure happened part-way through a redirect chain, in which case it carries the status of the last hop:

errorMeaning
invalid_argumentsAn argument was missing, the wrong type, or too long
url_not_allowedThe URL resolved somewhere a tool may not reach
timeoutYour endpoint did not answer within timeout_seconds
request_failedThe request could not be completed
too_many_redirectsMore than 3 redirects

A test runs outside any conversation, so there is no real customer for a context parameter to bind to. Obvious placeholders are sent instead: +10000000000, Test Contact, test@example.com and a zero UUID. Expect a correctly-built order lookup to answer "not found". Check the real behaviour from a live chat.

At reply time

A tool is offered to the model when all of these hold: enabled is true, phase is on_call, and the tool is either scoped to the agent handling the conversation or workspace-wide (bot_id: null).

Connecting the tool is half the job. The agent's instructions decide whether it uses it well, and Connect Your Store has a prompt you can adapt.