Tools API

Give a voice agent HTTP tools it can call mid-conversation — create, update and delete functions, with placeholder substitution and response mapping.

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/functions201 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 responseMapping and 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.

Next steps

  • Agents — configure the agent that owns these tools
  • Calls — place a call and watch the tool fire