Agent books an appointment

Install DotSIP, open the web admin, connect real SIP, and decide each call from your own HTTP API.

The caller wants an appointment. The route starts agent-desk. The tool router selects find_patient, free_slots, or book_slot. Your server reads and writes your own tables. DotSIP does not store patients. The right on the call stays the right from the route. A rights field on a tool result is ignored.

1. Records in DotSIP

  1. Under Models, save the API key on the live model, then select that model on the agent.
  2. Under Rights, keep right-1.
  3. Under Tools, create three tools. Endpoint for each is http://127.0.0.1:9099/tool. Rights right-1. Ids find_patient, free_slots, book_slot.
  4. find_patient parameters: object, property name string, required name. free_slots: property day string, required day. book_slot: properties day, time, name, all required.
  5. Under Agents, id agent-desk, router tool-router-builtin, tools find_patient, free_slots, book_slot. Prompt: ask for the name, then the day, offer only times the tool returns, and book only after the caller agrees.
  6. Under Routes, route-tools URL http://127.0.0.1:9099/route.
  7. Under SIP accounts, the trunk that receives the call. Set its inbound route to route-tools, or leave that field empty so the call uses route-tools.

The built-in router gives those tool schemas and its prompt to the text model on the live session. The text model picks the tool and fills the arguments. You do not have to put the tool name in the caller’s words. An external https router can pick instead of that text model. See Agents and OpenAI.

2. Your database

CREATE TABLE patients (name TEXT PRIMARY KEY, patient_id TEXT NOT NULL);
CREATE TABLE slots (day TEXT NOT NULL, time TEXT NOT NULL, patient_id TEXT, PRIMARY KEY(day, time));
INSERT INTO patients(name, patient_id) VALUES ('Eva Kovac', 'p-17');
INSERT INTO slots(day, time, patient_id) VALUES
  ('2026-09-28', '09:00', NULL),
  ('2026-09-28', '09:20', NULL);

3. Route JSON your server returns

{
  "format": "dotsip.route-result",
  "format_version": 2,
  "commands": [
    { "do": "answer" },
    { "do": "agent", "agent": "agent-desk", "rights": "right-1",
      "prompt": "Book an appointment. Use find_patient, then free_slots, then book_slot only after the caller agrees." }
  ]
}

4. What DotSIP posts to /tool

{
  "format": "dotsip.tool-call",
  "format_version": 1,
  "call_id": "c-7f3a",
  "tool": "find_patient",
  "arguments": { "name": "Eva Kovac" },
  "caller": { "number": "+421901234567", "name": null, "rights": "right-1" }
}

The same shape is used for free_slots and book_slot. tool and arguments change. The server returns JSON. ok true is success. Missing ok is not ok.

5. The server

<?php
$db = new PDO("sqlite:" . __DIR__ . "/operator.sqlite");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS patients (name TEXT PRIMARY KEY, patient_id TEXT NOT NULL)");
$db->exec("CREATE TABLE IF NOT EXISTS slots (day TEXT NOT NULL, time TEXT NOT NULL, patient_id TEXT, PRIMARY KEY(day, time))");
$db->exec("INSERT OR IGNORE INTO patients(name, patient_id) VALUES ('Eva Kovac', 'p-17')");
$db->exec("INSERT OR IGNORE INTO slots(day, time, patient_id) VALUES ('2026-09-28', '09:00', NULL), ('2026-09-28', '09:20', NULL)");
$body = json_decode(file_get_contents("php://input"), true);
header("Content-Type: application/json");
$format = is_array($body) && isset($body["format"]) ? $body["format"] : "";
if ($format === "dotsip.route") {
    echo json_encode(array(
        "format" => "dotsip.route-result", "format_version" => 2,
        "commands" => array(array("do" => "answer"), array(
            "do" => "agent", "agent" => "agent-desk", "rights" => "right-1",
            "prompt" => "Book an appointment. Use find_patient, then free_slots, then book_slot only after the caller agrees.",
        )),
    ));
    return;
}
if ($format !== "dotsip.tool-call") {
    http_response_code(400);
    echo json_encode(array("ok" => false, "error" => "bad request"));
    return;
}
$args = isset($body["arguments"]) && is_array($body["arguments"]) ? $body["arguments"] : array();
$tool = isset($body["tool"]) ? $body["tool"] : "";
if ($tool === "find_patient") {
    $id = $db->query("SELECT patient_id FROM patients WHERE name = " . $db->quote(isset($args["name"]) ? $args["name"] : ""))->fetchColumn();
    echo json_encode($id
        ? array("ok" => true, "result" => array("patient_id" => $id, "name" => $args["name"]))
        : array("ok" => false, "error" => "unknown patient"));
    return;
}
if ($tool === "free_slots") {
    $day = isset($args["day"]) ? $args["day"] : "";
    $rows = $db->query("SELECT time FROM slots WHERE day = " . $db->quote($day) . " AND patient_id IS NULL")->fetchAll(PDO::FETCH_COLUMN);
    echo json_encode(array("ok" => true, "result" => array("day" => $day, "slots" => array_values($rows))));
    return;
}
if ($tool === "book_slot") {
    $stmt = $db->prepare("UPDATE slots SET patient_id = ? WHERE day = ? AND time = ? AND patient_id IS NULL");
    $stmt->execute(array("p-17", isset($args["day"]) ? $args["day"] : "", isset($args["time"]) ? $args["time"] : ""));
    if ($stmt->rowCount() !== 1) {
        echo json_encode(array("ok" => false, "error" => "that time is not free"));
        return;
    }
    echo json_encode(array("ok" => true, "result" => array(
        "reference" => "A-1042", "day" => $args["day"], "time" => $args["time"],
    )));
    return;
}
echo json_encode(array("ok" => false, "error" => "unknown tool"));

A slow lookup can set x-dotsip-wait to false on the tool parameters object, outside properties. The model then receives { "ok": true, "pending": true, "tool": "find_patient" } at once. When your server returns, DotSIP appends the outcome as instructions.