Two HTTP contracts matter in production. Both are JSON POST requests from the DotSIP machine to an endpoint you run. Both use User-Agent: DotSIP-Route/1, Accept: application/json, and Content-Type: application/json; charset=utf-8. The body is at most 65536 bytes. Status 400 or higher fails the exchange.
Route request
Sent to the route selected for this call. The default id is route-tools. An extension outbound route or a SIP channel inbound route is used when that row exists. The request also includes dialed, route, and caller.groups. See Routes.
{
"format": "dotsip.route",
"format_version": 1,
"direction": "inbound",
"call_id": "string",
"sip_account": { "id": "string", "name": "string" },
"sip_phone": null,
"dialed": "string or null",
"route": "route-tools",
"caller": {
"number": "string or null",
"name": "string or null",
"rights": null,
"groups": []
}
}
Route result
| action | Required fields | What happens |
|---|---|---|
| answer | optional agent | Picks up. An agent starts only when agent is set. Optional rights, user, prompt. |
| transfer | phone | Transfers to that extension on this machine. |
| hangup | none | Rejects the call. Do not send error on a deliberate hangup. |
{
"format": "dotsip.route-result",
"format_version": 1,
"action": "answer",
"agent": "agent-main",
"rights": "right-1"
}
Tool call
Sent after the agent’s tool router selects a tool that is on the agent and allowed by the call right.
{
"format": "dotsip.tool-call",
"format_version": 1,
"call_id": "string",
"tool": "lookup-order",
"arguments": {},
"caller": { "number": "string or null", "name": "string or null", "rights": "right-1" }
}
{
"ok": true,
"result": {},
"prompt": "optional instruction for the rest of this call"
}
ok true is success. Missing ok is not ok. rights on this body is ignored. prompt and agent are applied as on Tools.
Route endpoint
PHP, Python, and Node.js answer with the same agent.
<?php
header("Content-Type: application/json");
$body = json_decode(file_get_contents("php://input"), true);
if (!is_array($body) || (isset($body["format"]) ? $body["format"] : "") !== "dotsip.route") {
http_response_code(400);
echo json_encode(array("error" => "bad request"));
return;
}
echo json_encode(array(
"format" => "dotsip.route-result",
"format_version" => 1,
"action" => "answer",
"agent" => "agent-main",
"rights" => "right-1",
));
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/route")
def route():
body = request.get_json(silent=True) or {}
if body.get("format") != "dotsip.route":
return jsonify({"error": "bad request"}), 400
return jsonify({
"format": "dotsip.route-result",
"format_version": 1,
"action": "answer",
"agent": "agent-main",
"rights": "right-1",
})
const http = require("http");
http.createServer((req, res) => {
let raw = "";
req.on("data", (c) => { raw += c; });
req.on("end", () => {
let body = {};
try { body = JSON.parse(raw || "{}"); } catch (e) { body = {}; }
if (body.format !== "dotsip.route") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "bad request" }));
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
format: "dotsip.route-result",
format_version: 1,
action: "answer",
agent: "agent-main",
rights: "right-1"
}));
});
}).listen(9099);
Order of a call
- A SIP call arrives on an account.
- DotSIP POSTs dotsip.route to the selected route. The default is route-tools.
- Your result is one action, or a commands list. One action is answer, transfer, or hangup. A command list can also play a recording, wait, collect digits, or call a webhook. See Recordings and DTMF.
- On answer with an agent, OpenAI Live starts with that agent’s live model and the API key stored on that model.
- Tools are the intersection of the agent’s tools and the rights ID.
- Each model tool call is a POST of dotsip.tool-call.
- A tool result may include prompt and agent. A rights field on that result is ignored.