Extension 101 dials 102, or a trunk call arrives with a dialed number. DotSIP does not connect extensions by itself. Your server reads dialed and looks up your table. A group in the admin is only a named list. It does not place this call.
1. Records in DotSIP
- Under SIP phones, users 101 and 102, both enabled, each with a password.
- On phone 101, set the outbound route to route-tools if that field is on the phone. A dialed call from 101 then uses that route.
- Under SIP accounts, trunk id trunk. Leave inbound route empty if this example should fall through to route-tools.
- Under Routes, route-tools URL http://127.0.0.1:9099/route.
2. Your database
CREATE TABLE destinations (
dialed TEXT PRIMARY KEY,
kind TEXT NOT NULL,
target TEXT NOT NULL
);
INSERT INTO destinations(dialed, kind, target) VALUES
('102', 'transfer', '102'),
('9000', 'dial', '+421900000000');
kind transfer uses target as the SIP phone user. kind dial uses target as the outside number. mode sequence tries one destination. parallel is the group-ring example.
3. What DotSIP posts
{
"format": "dotsip.route",
"format_version": 1,
"direction": "inbound",
"call_id": "c-7f3a",
"sip_account": { "id": "trunk", "name": "trunk" },
"sip_phone": null,
"dialed": "102",
"route": "route-tools",
"caller": { "number": "101", "name": null, "rights": null, "groups": [] }
}
4. 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 destinations (dialed TEXT PRIMARY KEY, kind TEXT NOT NULL, target TEXT NOT NULL)");
$db->exec("INSERT OR IGNORE INTO destinations(dialed, kind, target) VALUES ('102', 'transfer', '102'), ('9000', 'dial', '+421900000000')");
$body = json_decode(file_get_contents("php://input"), true);
header("Content-Type: application/json");
if (!is_array($body) || (isset($body["format"]) ? $body["format"] : "") !== "dotsip.route") {
http_response_code(400);
echo json_encode(array("error" => "bad request"));
return;
}
$dialed = isset($body["dialed"]) ? (string) $body["dialed"] : "";
$row = $db->query("SELECT kind, target FROM destinations WHERE dialed = " . $db->quote($dialed))->fetch(PDO::FETCH_ASSOC);
if (!$row) {
echo json_encode(array("format" => "dotsip.route-result", "format_version" => 2, "commands" => array(array("do" => "hangup"))));
return;
}
$step = $row["kind"] === "dial"
? array("do" => "dial", "number" => $row["target"], "mode" => "sequence", "timeout_ms" => 30000, "on_fail" => array(array("do" => "hangup")))
: array("do" => "transfer", "phone" => $row["target"]);
echo json_encode(array(
"format" => "dotsip.route-result",
"format_version" => 2,
"commands" => array(array("do" => "answer"), $step),
));
import sqlite3
from flask import Flask, request, jsonify
app = Flask(__name__)
con = sqlite3.connect("operator.sqlite", check_same_thread=False)
con.execute("CREATE TABLE IF NOT EXISTS destinations (dialed TEXT PRIMARY KEY, kind TEXT NOT NULL, target TEXT NOT NULL)")
con.execute("INSERT OR IGNORE INTO destinations(dialed, kind, target) VALUES ('102', 'transfer', '102'), ('9000', 'dial', '+421900000000')")
con.commit()
@app.post("/route")
def route():
body = request.get_json(silent=True) or {}
if body.get("format") != "dotsip.route":
return jsonify({"error": "bad request"}), 400
row = con.execute("SELECT kind, target FROM destinations WHERE dialed = ?", (str(body.get("dialed") or ""),)).fetchone()
if not row:
return jsonify({"format": "dotsip.route-result", "format_version": 2, "commands": [{"do": "hangup"}]})
if row[0] == "dial":
step = {"do": "dial", "number": row[1], "mode": "sequence", "timeout_ms": 30000, "on_fail": [{"do": "hangup"}]}
else:
step = {"do": "transfer", "phone": row[1]}
return jsonify({"format": "dotsip.route-result", "format_version": 2, "commands": [{"do": "answer"}, step]})
const http = require("http");
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync("operator.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS destinations (dialed TEXT PRIMARY KEY, kind TEXT NOT NULL, target TEXT NOT NULL)");
db.exec("INSERT OR IGNORE INTO destinations(dialed, kind, target) VALUES ('102', 'transfer', '102'), ('9000', 'dial', '+421900000000')");
const find = db.prepare("SELECT kind, target FROM destinations WHERE dialed = ?");
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 = {}; }
const send = (payload, code) => {
res.writeHead(code, { "Content-Type": "application/json" });
res.end(JSON.stringify(payload));
};
if (body.format !== "dotsip.route") { send({ error: "bad request" }, 400); return; }
const row = find.get(String(body.dialed || ""));
if (!row) { send({ format: "dotsip.route-result", format_version: 2, commands: [{ do: "hangup" }] }, 200); return; }
const step = row.kind === "dial"
? { do: "dial", number: row.target, mode: "sequence", timeout_ms: 30000, on_fail: [{ do: "hangup" }] }
: { do: "transfer", phone: row.target };
send({ format: "dotsip.route-result", format_version: 2, commands: [{ do: "answer" }, step] }, 200);
});
}).listen(9099);
From phone 101, dial 102. The table says transfer to user 102, so that phone rings. Dial 9000 and the table says dial +421900000000 through the trunk, one number after another because mode is sequence. A dialed value with no row hangs up. Nothing in this server rings a group by itself.