Tools API
A tool is an HTTP endpoint your agent can call mid-conversation — look up an order,
check availability, write to your CRM. The agent decides when to call it from the
description and parametersSchema, so those two fields are what actually drive
behaviour. Write the description the way you would brief a new colleague.
Tools are called functions in the API paths.
| Method | Path | Scope |
|---|---|---|
GET |
/api/v1/functions |
functions:read |
POST |
/api/v1/functions |
functions:write |
GET |
/api/v1/functions/{id} |
functions:read |
PATCH |
/api/v1/functions/{id} |
functions:write |
DELETE |
/api/v1/functions/{id} |
functions:write |
Create a tool
POST /api/v1/functions → 201 Created
curl -X POST https://voice-agent.edesy.in/api/v1/functions \
-H "Authorization: Bearer $EDESY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agentId": 1042,
"name": "lookup_order",
"description": "Look up the status of a customer order. Call this as soon as the customer gives an order number.",
"parametersSchema": {
"order_id": { "type": "string", "description": "The order number, digits only" }
},
"requiredParams": ["order_id"],
"httpMethod": "POST",
"httpUrl": "https://api.example.com/orders/lookup",
"httpHeaders": { "Content-Type": "application/json" },
"httpBody": "{\"orderId\": \"{{order_id}}\"}",
"responseMapping": { "status": "data.status", "eta": "data.expected_delivery" },
"isActive": true
}'
import os
import requests
response = requests.post(
"https://voice-agent.edesy.in/api/v1/functions",
headers={"Authorization": f"Bearer {os.environ['EDESY_API_KEY']}"},
json={
"agentId": 1042,
"name": "lookup_order",
"description": "Look up the status of a customer order. Call this as soon as the customer gives an order number.",
"parametersSchema": {
"order_id": {"type": "string", "description": "The order number, digits only"}
},
"requiredParams": ["order_id"],
"httpMethod": "POST",
"httpUrl": "https://api.example.com/orders/lookup",
"httpHeaders": {"Content-Type": "application/json"},
# A JSON *string*, not an object — the placeholder is substituted at call time.
"httpBody": '{"orderId": "{{order_id}}"}',
"responseMapping": {"status": "data.status", "eta": "data.expected_delivery"},
"isActive": True,
},
timeout=30,
)
response.raise_for_status()
tool = response.json()["data"]
print(tool["id"])
const response = await fetch("https://voice-agent.edesy.in/api/v1/functions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EDESY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: 1042,
name: "lookup_order",
description:
"Look up the status of a customer order. Call this as soon as the customer gives an order number.",
parametersSchema: {
order_id: { type: "string", description: "The order number, digits only" },
},
requiredParams: ["order_id"],
httpMethod: "POST",
httpUrl: "https://api.example.com/orders/lookup",
httpHeaders: { "Content-Type": "application/json" },
// A JSON *string*, not an object.
httpBody: '{"orderId": "{{order_id}}"}',
responseMapping: { status: "data.status", eta: "data.expected_delivery" },
isActive: true,
}),
});
if (!response.ok) {
throw new Error(`Edesy API ${response.status}: ${await response.text()}`);
}
const { data: tool } = await response.json();
console.log(tool.id);
<?php
$ch = curl_init('https://voice-agent.edesy.in/api/v1/functions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('EDESY_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'agentId' => 1042,
'name' => 'lookup_order',
'description' => 'Look up the status of a customer order. Call this as soon as the customer gives an order number.',
'parametersSchema' => [
'order_id' => ['type' => 'string', 'description' => 'The order number, digits only'],
],
'requiredParams' => ['order_id'],
'httpMethod' => 'POST',
'httpUrl' => 'https://api.example.com/orders/lookup',
'httpHeaders' => ['Content-Type' => 'application/json'],
// A JSON *string*, not an array.
'httpBody' => '{"orderId": "{{order_id}}"}',
'responseMapping' => ['status' => 'data.status', 'eta' => 'data.expected_delivery'],
'isActive' => true,
]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 201) {
throw new RuntimeException("Edesy API {$status}: {$response}");
}
$tool = json_decode($response, true)['data'];
echo $tool['id'];
{
"success": true,
"data": {
"id": 318,
"name": "lookup_order",
"description": "Look up the status of a customer order. …",
"agentId": 1042,
"parametersSchema": { "order_id": { "type": "string", "description": "The order number, digits only" } },
"requiredParams": ["order_id"],
"httpMethod": "POST",
"httpUrl": "https://api.example.com/orders/lookup",
"httpHeaders": { "Content-Type": "application/json" },
"httpBody": "{\"orderId\": \"{{order_id}}\"}",
"responseMapping": { "status": "data.status" },
"credentialRefs": [],
"isActive": true,
"templateId": null,
"categoryId": null,
"createdAt": "2026-08-10T09:20:11.004Z",
"updatedAt": "2026-08-10T09:20:11.004Z"
}
}
A newly created tool is visible to live calls immediately — the agent's cached call context is invalidated as part of the write.
Errors: 400 VALIDATION_ERROR, 404 AGENT_NOT_FOUND (agent missing or in another
workspace), 409 DUPLICATE_FUNCTION_NAME (name already used on that agent),
409 NO_ATTRIBUTABLE_USER.
Field reference
| Field | Type | Notes |
|---|---|---|
agentId |
integer | Required on create. The agent this tool belongs to |
name |
string (≤64) | Required. Must match ^[a-zA-Z_][a-zA-Z0-9_]*$ — it is handed to the LLM as a tool name |
description |
string (≤1000) | Drives when the agent calls the tool. Be explicit |
parametersSchema |
object | A JSON-Schema properties map: { "<param>": { "type": …, "description": … } }. Do not wrap it in {"type":"object","properties":…} — that wrapper is added for you |
requiredParams |
string[] | Which parameters are mandatory |
httpMethod |
GET | POST | PUT | PATCH | DELETE |
|
httpUrl |
string | Your endpoint. Supports placeholders |
httpHeaders |
object | Header name → value. Supports placeholders |
httpBody |
string | Request body template, as a string. Supports placeholders |
responseMapping |
object | Reshapes your response before the agent sees it |
credentialRefs |
object[] | { "placeholder": "ORDER_API_TOKEN", "vaultSlug": "acme-orders" } — injects a secret from the credential vault at call time, so tokens never live in the tool definition. Reference it as {{vault.acme-orders}} |
isActive |
boolean | Default true. Inactive tools are not offered to the agent |
templateId / categoryId |
string | Optional grouping metadata |
Tool names are unique per agent (agentId + name).
Placeholders
httpUrl, httpHeaders and httpBody are templates. Both {{name}} and {name} are
accepted. Three kinds of value resolve:
| Placeholder | Source |
|---|---|
{{order_id}} |
A parameter the agent supplied, from your parametersSchema |
{{call.id}} |
Call context injected by the platform |
{{vault.acme-orders}} |
A secret from the credential vault, resolved just before the request |
Available call-context values:
| Placeholder | Value |
|---|---|
{{call.id}} / {{call.conversation_id}} |
Conversation id — the same id GET /api/v1/calls/{id} takes |
{{call.sid}} |
Carrier Call SID (Twilio CA…, etc.) |
{{call.phone_number}} / {{call.from}} |
The other party's number |
{{call.agent_id}} |
Agent id |
{{call.workspace_id}} |
Workspace id |
Flat spellings {{call_id}}, {{conversation_id}} and {{call_sid}} also resolve.
Two behaviours worth knowing: an agent-supplied parameter wins over a call-context
value of the same name, and an unknown placeholder is left in place verbatim rather
than blanked — so if you receive a literal {{call.sid}} in a request, that value was
unavailable for that call, not empty.
Response mapping
By default the agent receives your endpoint's response as-is. responseMapping
reshapes it first. The two runtimes accept different shapes, so pick based on where
your agent runs — or omit it entirely and let the agent read the raw response, which
behaves identically everywhere.
Flat form — {"targetKey": "dot.path"}, builds a new object from extracted paths:
{ "responseMapping": { "status": "data.status", "eta": "data.expected_delivery" } }
Wrapped forms — extract returns a single value, transform builds an object:
{ "responseMapping": { "extract": "data.status" } }
{ "responseMapping": { "transform": { "status": "data.status" } } }
| Runtime | Flat {key: path} |
{"extract": …} |
{"transform": {…}} |
|---|---|---|---|
| Go (default) | Yes | No — treated as a literal key | No |
| Python | No — silently ignored | Yes | Yes |
Paths use dot notation and support array indices: data.items.0.name. A path that
doesn't match is dropped from the result. If mapping fails for any reason, the raw
response is passed through rather than erroring.
Because an unsupported shape is ignored rather than rejected, a mismatch shows up as "the agent behaves as if I never configured mapping". If in doubt, omit
responseMappingand return exactly the JSON you want the agent to read.
Update a tool
PATCH /api/v1/functions/{id} → 200 OK
Partial update. Every field above is optional.
curl -X PATCH https://voice-agent.edesy.in/api/v1/functions/318 \
-H "Authorization: Bearer $EDESY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "httpUrl": "https://api.example.com/v2/orders/lookup" }'
HTTP fields merge into the existing implementation. Changing httpUrl alone does
not drop your headers or body template — only what you send is replaced.
A tool can be moved to another agent by sending a new agentId, as long as the target
agent is in the same workspace. Both agents' caches are refreshed.
Errors: 400 VALIDATION_ERROR, 404 NOT_FOUND, 404 AGENT_NOT_FOUND
(reassignment target outside your workspace), 409 DUPLICATE_FUNCTION_NAME (rename
collides on the target agent).
List, read and delete tools
GET /api/v1/functions
| Query | Type | Description |
|---|---|---|
agentId |
integer | Restrict to one agent |
active |
true |
Only active tools |
limit / offset |
integer | Pagination (max 100) |
{ "success": true, "data": { "functions": [], "total": 4, "limit": 50, "offset": 0 } }
GET /api/v1/functions/{id} returns a single tool.
DELETE /api/v1/functions/{id} returns { "success": true }. Both return
404 NOT_FOUND for an unknown id.