Core resources
Tools
Tools let your agent fetch live data from your own systems in the middle of a call, such as checking an order or reading a booking. You describe the function; Offhook calls your endpoint with the model's arguments and gives the answer back to the model.
How it works
- You define a tool: a
name, adescriptionthat tells the model when to use it, the arguments it takes, and an HTTP endpoint. - You give the tool to an agent through
tool_ids. - During a call, when the model decides it needs that data, it calls the tool. Offhook sends the request to your endpoint with the model's arguments and passes your JSON response back to the model, which carries on talking.
Your code, your servers
Offhook doesn't run your code; it calls it. To give the agent a database lookup, a calculation or another API, put that logic behind a small HTTP endpoint on your side and point a tool at it. Your code runs with your own dependencies and secrets, and Offhook only sends the arguments and reads the result.
# Your own server: FastAPI, Flask, a Lambda, a Cloud Function, anything.
from fastapi import FastAPI
app = FastAPI()
@app.get("/tools/orders/{order_id}")
def check_order_status(order_id: str):
order = db.query(Order).get(order_id) # your code, your DB, your deps
if order is None:
return {"found": False}
return {
"found": True,
"status": order.status,
"items": order.item_count,
"delivery_date": str(order.delivery_date),
}curl -X POST http://localhost:8000/v1/tools \
-H "Authorization: Bearer $OFFHOOK_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "check_order_status",
"description": "Look up an order by ID; returns status, items, and delivery date.",
"method": "GET",
"url": "https://api.yourapp.com/tools/orders/{order_id}",
"parameters": {
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
},
"headers": { "Authorization": "Bearer your-internal-key" }
}' Two rules for the endpoint:
- It must be public HTTPS. Requests to private, loopback and cloud-metadata addresses are blocked. During local development, expose your endpoint with a tunnel such as ngrok and use that URL.
- It must answer within
timeout_seconds. For slow work, start it in the background and return straight away, then report the result later, for example through a webhook.
Create a tool
/v1/tools Body parameters
| Field | Type | Description |
|---|---|---|
namerequired | string | The function name the model calls: letters, digits and underscores, e.g. check_order_status. Must be unique within your tenant. record_result and end_call are reserved. |
descriptionrequired | string | What the tool does and when to use it. This is what the model reads to decide whether to call it. |
parametersoptional | object | The arguments, in JSON Schema style: { "properties": { … }, "required": [ … ] }. |
methodoptional | string | GET (the default) or POST. |
urlrequired | string | Your HTTPS endpoint. Write {name} in the path to insert an argument there. |
headersoptional | object | Fixed headers, such as Authorization. Stored encrypted and never returned. |
timeout_secondsoptional | int | 1 to 20. The default is 10. |
curl -X POST http://localhost:8000/v1/tools \
-H "Authorization: Bearer $OFFHOOK_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "check_order_status",
"description": "Look up an order by its ID and return its status, items, and delivery date.",
"method": "GET",
"url": "https://api.yourapp.com/orders/{order_id}",
"parameters": {
"properties": {
"order_id": { "type": "string", "description": "The order ID, e.g. ORD-1042" }
},
"required": ["order_id"]
},
"headers": { "Authorization": "Bearer sk_live_…" },
"timeout_seconds": 8
}'{
"id": "c4e81b2f-9a3d-4c6e-8f1b-7d2a5e9c3b10",
"name": "check_order_status",
"description": "Look up an order by its ID and return its status, items, and delivery date.",
"method": "GET",
"url": "https://api.yourapp.com/orders/{order_id}",
"parameters": { "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] },
"has_auth": true,
"timeout_seconds": 8,
"created_at": "2026-10-15T10:00:00Z"
} How arguments become a request
The model's arguments only ever fill in the request. They can't change the host, scheme, method or headers.
- Path: each
{name}in the URL is replaced with that argument, URL-encoded. - GET: the other arguments become the query string.
- POST: the other arguments become the JSON body.
So check_order_status(order_id="ORD-1042") sends GET https://api.yourapp.com/orders/ORD-1042 with your Authorization header.
Give a tool to an agent
Pass tool_ids when you create the agent. It can then use any of those tools during a call.
curl -X POST http://localhost:8000/v1/agents \
-H "Authorization: Bearer $OFFHOOK_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Order support",
"instructions": "Help the caller with questions about their order. Use check_order_status to look up specifics.",
"stt_connection_id": "<stt-connection-id>",
"tts_connection_id": "<tts-connection-id>",
"llm_connection_id": "<llm-connection-id>",
"tool_ids": ["c4e81b2f-9a3d-4c6e-8f1b-7d2a5e9c3b10"]
}' List, retrieve, update and delete
/v1/tools /v1/tools/{id} /v1/tools/{id} /v1/tools/{id} Header values are never returned. has_auth tells you whether any are set.
What the model gets back
- Success:
{ "ok": true, "result": <your JSON> }. Very large responses are shortened. - Your endpoint returned an error status:
{ "error": "endpoint returned HTTP 404", "body": … }. - Timeout, blocked URL or missing argument:
{ "error": "…" }.
In every error case the call carries on, so the agent can apologise and move on.