DocsConcepts
Calling tools
Every tool is one call over REST or MCP. Every reply comes in the same envelope, with what the call cost and what is left.
On this page
- Each tool's page in the tools directory shows its inputs, an example call, a real reply and its errors. Read it before calling a tool for the first time. Its Markdown is at the page's address plus
.md. GET https://api.conecto.chat/lists every tool, andv1/ tools ?area=crmone area.GET https://api.conecto.chat/gives one tool's input schema. Neither needs a key.v1/ tools/ <name> - Over MCP,
tools/liston an area's server lists its tools with their input schemas and prices.
POST https://api.conecto.chat/ with the arguments as a JSON object, and the key in the Authorization header. The examples on this page are the tools' documented calls, made for the docs' two example businesses: Halverson Cycle & Repair, a bike shop in Portland, and Hansen & Voss, a restaurant-equipment wholesaler in Hamburg.
curl
curl -X POST https://api.conecto.chat/v1/tools/crm_save_contact \
-H "Authorization: Bearer $CONECTO_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "jana@weber-kuechenplanung.example",
"name": "Jana Weber",
"title": "Kitchen planner",
"company": "Weber Küchenplanung",
"phone": "+49 40 66969 120",
"location": "Hamburg",
"lifecycle_stage": "lead",
"tags": ["trade", "test-kitchen-visit"]
}'Python
import os
import requests
reply = requests.post(
"https://api.conecto.chat/v1/tools/crm_save_contact",
headers={"Authorization": f"Bearer {os.environ['CONECTO_KEY']}"},
json={
"email": "jana@weber-kuechenplanung.example",
"name": "Jana Weber",
"title": "Kitchen planner",
"company": "Weber Küchenplanung",
"phone": "+49 40 66969 120",
"location": "Hamburg",
"lifecycle_stage": "lead",
"tags": ["trade", "test-kitchen-visit"],
},
)
print(reply.status_code, reply.json())JavaScript
const reply = await fetch("https://api.conecto.chat/v1/tools/crm_save_contact", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CONECTO_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: "jana@weber-kuechenplanung.example",
name: "Jana Weber",
title: "Kitchen planner",
company: "Weber Küchenplanung",
phone: "+49 40 66969 120",
location: "Hamburg",
lifecycle_stage: "lead",
tags: ["trade", "test-kitchen-visit"],
}),
});
console.log(reply.status, await reply.json());{
"call_id": "5f0c8a2e-3b7d-4c1e-9a64-2d8e1f7b0c93",
"tool": "crm_save_contact",
"status": "succeeded",
"replayed": false,
"cost_usd": "$0.01",
"balance_usd": "$19.75",
"result": {
"contact": {
"id": 4107,
"email": "jana@weber-kuechenplanung.example",
"name": "Jana Weber",
"title": "Kitchen planner",
"company": "Weber Küchenplanung",
"company_id": 4105,
"phone": "+49 40 66969 120",
"location": "Hamburg",
"lifecycle_stage": "lead",
"outreach_status": null,
"notes": "",
"tags": ["test-kitchen-visit", "trade"],
"custom_fields": {},
"archived": false,
"updated_at": "2026-10-14T08:00:00+00:00"
},
"created": true
}
}| Field | What it is |
|---|---|
call_id |
The call's id, for GET /v1/calls/{call_ and the call log. |
tool |
The tool that ran. |
status |
succeeded. A background call says running first. |
replayed |
true when the reply repeats an earlier call with the same Idempotency-Key. A replay costs nothing. |
cost_usd |
What this call cost. |
balance_usd |
What the account can spend after it. |
result |
The tool's own answer. Each tool's page shows it. |
bought |
Only on a read that bought the month's next block of reads: what it bought and what that cost. |
Amounts are strings in US dollars with as many decimals as they need, at least two, like "$0.01" or "$0.0005".
Connect https://api.conecto.chat/ with the key in the Authorization header, then call tools/call. The idempotency key goes in _meta.idempotencyKey, and an approval in _meta.approvalId.
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "crm_save_contact",
"arguments": {
"email": "jana@weber-kuechenplanung.example",
"name": "Jana Weber",
"title": "Kitchen planner",
"company": "Weber Küchenplanung",
"phone": "+49 40 66969 120",
"location": "Hamburg",
"lifecycle_stage": "lead",
"tags": ["trade", "test-kitchen-visit"]
},
"_meta": {
"idempotencyKey": "save-jana-weber"
}
}
}{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "{\n \"call_id\": \"5f0c8a2e-3b7d-4c1e-9a64-2d8e1f7b0c93\",\n \"tool\": \"crm_save_contact\",\n \"status\": \"succeeded\",\n \"replayed\": false,\n \"cost_usd\": \"$0.01\",\n \"balance_usd\": \"$19.75\",\n \"result\": {\n \"contact\": {\n \"id\": 4107,\n \"email\": \"jana@weber-kuechenplanung.example\",\n \"name\": \"Jana Weber\",\n \"title\": \"Kitchen planner\",\n \"company\": \"Weber Küchenplanung\",\n \"company_id\": 4105,\n \"phone\": \"+49 40 66969 120\",\n \"location\": \"Hamburg\",\n \"lifecycle_stage\": \"lead\",\n \"outreach_status\": null,\n \"notes\": \"\",\n \"tags\": [\n \"test-kitchen-visit\",\n \"trade\"\n ],\n \"custom_fields\": {},\n \"archived\": false,\n \"updated_at\": \"2026-10-14T08:00:00+00:00\"\n },\n \"created\": true\n }\n}"
}
],
"structuredContent": {
"call_id": "5f0c8a2e-3b7d-4c1e-9a64-2d8e1f7b0c93",
"tool": "crm_save_contact",
"status": "succeeded",
"replayed": false,
"cost_usd": "$0.01",
"balance_usd": "$19.75",
"result": {
"contact": {
"id": 4107,
"email": "jana@weber-kuechenplanung.example",
"name": "Jana Weber",
"title": "Kitchen planner",
"company": "Weber Küchenplanung",
"company_id": 4105,
"phone": "+49 40 66969 120",
"location": "Hamburg",
"lifecycle_stage": "lead",
"outreach_status": null,
"notes": "",
"tags": ["test-kitchen-visit", "trade"],
"custom_fields": {},
"archived": false,
"updated_at": "2026-10-14T08:00:00+00:00"
},
"created": true
}
},
"isError": false
}
}The reply's structuredContent holds the same envelope as REST, and content holds it as text. A refusal comes back with isError: true and the refusal as its text. Without a key, tools/call answers with a short text that says how to sign up. A batch of up to 50 messages counts each message toward the rate limits. JSON-RPC errors lists what the server itself answers.
Before a call runs, the most it can cost is held against the balance, the key's daily budget, its team's budget and the account's monthly ceiling. When it ends, the account pays what it really cost and the rest of the hold comes back. A call that fails costs nothing, and its whole hold comes back.
| Model | Tools | How it is charged | For example |
|---|---|---|---|
| Free | 313 | Costs nothing. A read counts toward the month's free reads. | crm_search: free up to the month's 1,000,000 reads |
| Per call | 219 | One price for each call that succeeds. | crm_: $0.01 |
| Per item | 40 | A price for each thing the call handles, like each record saved or each page read. The most it can cost is held first. | crm_: $0.01 per record saved |
| Metered | 8 | What the work cost, known once it is done, up to a stated most. AI work is charged at the model's cost plus 40% unless the tool says otherwise. | email_send: $0.001 per recipient, plus $0.001 per started MB of attachments for each recipient ($0.001 to one person without attachments), up to $0.52 |
| Quoted | 1 | The price in a quote the tool gives first, up to a stated most. | phone_: quoted, up to $25.00 |
Reads are free up to 1,000,000 a month for the whole account. Past that, the read that needs more buys the next 10,000 reads for $0.50 before it runs, and its reply says so in bought. Money has the details.
Send an Idempotency-Key header, any string up to 120 characters, to retry without paying twice. A longer one is kept as its SHA-256. Each idempotency key belongs to the API key that sent it.
- The same idempotency key with the same tool and arguments, after a success, answers with the first result,
replayed: trueand a cost of $0.00. - After a failure, it runs the call again.
- While the first call still runs, it answers
in_progresswith status 409. - With another tool or other arguments, it answers
idempotency_key_reusedwith status 409. Use a new idempotency key for a new request.
A tool that returns a secret runs again on a retry, since the call log never keeps the secret.
A few tools take a while, like transcribing audio or importing a CRM. They answer 202 at once:
curl
curl -X POST https://api.conecto.chat/v1/tools/speech_transcribe \
-H "Authorization: Bearer $CONECTO_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_id": "7c9e1b3d-5f7a-4c9e-8b1d-3f5a7c9e1b4d",
"speakers": true,
"language": "de"
}'Python
import os
import requests
reply = requests.post(
"https://api.conecto.chat/v1/tools/speech_transcribe",
headers={"Authorization": f"Bearer {os.environ['CONECTO_KEY']}"},
json={
"file_id": "7c9e1b3d-5f7a-4c9e-8b1d-3f5a7c9e1b4d",
"speakers": True,
"language": "de",
},
)
print(reply.status_code, reply.json())JavaScript
const reply = await fetch("https://api.conecto.chat/v1/tools/speech_transcribe", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CONECTO_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
file_id: "7c9e1b3d-5f7a-4c9e-8b1d-3f5a7c9e1b4d",
speakers: true,
language: "de",
}),
});
console.log(reply.status, await reply.json());{
"call_id": "9b7e3c51-2f4a-4d8e-a0c6-71e5d2b8f4a3",
"tool": "speech_transcribe",
"status": "running",
"reserved_usd": "$0.60",
"poll": "GET /v1/calls/9b7e3c51-2f4a-4d8e-a0c6-71e5d2b8f4a3 (or the account_get_call tool) until status is succeeded or failed. You are charged only when it succeeds."
}Poll GET /v1/calls/{call_id}, or call account_get_call, until status is succeeded or failed. The hold stays until then, and the call is charged only when it succeeds. These tools run in the background: account_export_all, files_ocr, import_from_hubspot, import_from_pipedrive, import_from_zendesk, speech_transcribe and visibility_check.
A refused call answers with a status of 400 or more and costs nothing. Its error is a code, its message says what to do in words written for an agent, and details, when there is something to add, holds what is needed to fix it.
{
"call_id": "5f0c8a2e-3b7d-4c1e-9a64-2d8e1f7b0c93",
"status": "failed",
"error": "insufficient_balance",
"message": "This call costs up to $0.10 and the balance is $0.04. Top up to continue; nothing was charged.",
"cost_usd": "$0.00",
"details": {
"price_usd": "$0.10",
"balance_usd": "$0.04",
"top_up": "POST /v1/topups {\"amount_usd\": 20} returns a Stripe checkout link; pay it once and turn on auto-reload so the balance never runs dry.",
"minimum_usd": 5,
"fee": "7% + $0.35 per top-up, Stripe's fees passed on",
"first_topup_offer": "Your first top-up is doubled, up to $20 extra."
}
}402means top up. The details say how.429means a limit was reached: a budget, the monthly ceiling or the rate. Limits says which is which.409withapproval_requiredmeans a person must approve this exact call first. Approvals shows the next step.
Errors lists every code with its fix, and each tool's page lists the codes that tool can give.
- A body is at most 16 MB. A bigger one answers
too_large. Send big files withfiles_upload_link. - Text with the NUL character (U+0000) is refused with
invalid_input. - Arguments that fail the tool's input schema answer
invalid_input, anddetails.input_schemaholds the schema. - A tool's secret arguments, like a password, are kept encrypted while the call runs and stored as
[redacted]after it.
An MCP server answers these JSON-RPC errors itself, before any tool runs.
| Code | When |
|---|---|
| -32601 | Method not found: ... |
| -32600 | Invalid JSON-RPC request. |
| -32602 | Unknown tool ... in this connector. Call tools/list. |
| -32002 | Too many requests. The message names the limit. |
| -32600 | The request is over 16 MB. Send big files with files_upload_link. |
| -32700 | Parse error: the body is not JSON. |
Updated . Every route, field and number here is read from the API's own code.