Outbound call to a patient

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

Your program asks DotSIP to call a patient. When the patient answers, DotSIP posts the route. Your server finds that patient in your database and returns a recording plus gather. Digit 1 confirms the row. DotSIP does not store the patient.

1. Records in DotSIP

  1. Open API. Create a key, listen on 127.0.0.1, port 9090, turn it on, and copy the secret before you hide it.
  2. Under SIP accounts, turn on Outgoing (OUT) for the trunk that places the call. Its transfer URI template must contain {extension}. With no sip_account in the JSON, DotSIP uses the first enabled outgoing account.
  3. Under SIP phones, user 101. Set its outbound route to a route whose URL is http://127.0.0.1:9099/route. If that route is missing, an outbound originate is not placed.
  4. Put confirm.wav in data/media. Mono PCM16, 8000, 16000, or 24000 Hz. The words are “Press 1 to confirm the appointment.”

2. Your database

CREATE TABLE patients (
  phone TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  confirmed INTEGER NOT NULL
);
INSERT INTO patients(phone, name, confirmed) VALUES ('+421901234567', 'Eva Kovac', 0);
CREATE TABLE pending (call_id TEXT PRIMARY KEY, phone TEXT NOT NULL);

3. Place the call

This request is from your program to the API-key port, not to port 9080.

POST /v1/calls/originate HTTP/1.1
Host: 127.0.0.1:9090
Authorization: Bearer dsk_the-copied-secret
Content-Type: application/json

{"to":"+421901234567","extension":"101","behavior":"outbound"}

HTTP 201 returns { "ok": true, "call_id": "..." }. After the patient answers, DotSIP POSTs the extension’s outbound route. direction is outbound. caller.number is 101. dialed is to.

4. The route server

It loads the patient by dialed and stores that phone under call_id in pending. The plan plays the file, then waits for 1. The digit is not in the local map, so DotSIP posts dotsip.dtmf to /confirmed and waits. The server loads the phone for that call_id, sets confirmed to 1, and returns hangup. On that digit callback, caller.number is the number that was rung, not the extension. Keep the patient on call_id.

<?php
$db = new PDO("sqlite:" . __DIR__ . "/operator.sqlite");
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS patients (phone TEXT PRIMARY KEY, name TEXT NOT NULL, confirmed INTEGER NOT NULL)");
$db->exec("CREATE TABLE IF NOT EXISTS pending (call_id TEXT PRIMARY KEY, phone TEXT NOT NULL)");
$db->exec("INSERT OR IGNORE INTO patients(phone, name, confirmed) VALUES ('+421901234567', 'Eva Kovac', 0)");
$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") {
    $dialed = isset($body["dialed"]) ? (string) $body["dialed"] : "";
    $name = $db->query("SELECT name FROM patients WHERE phone = " . $db->quote($dialed))->fetchColumn();
    if (!$name) {
        echo json_encode(array("commands" => array(array("do" => "hangup"))));
        return;
    }
    $save = $db->prepare("INSERT OR REPLACE INTO pending(call_id, phone) VALUES (?, ?)");
    $save->execute(array(isset($body["call_id"]) ? $body["call_id"] : "", $dialed));
    echo json_encode(array(
        "format" => "dotsip.route-result",
        "format_version" => 2,
        "commands" => array(
            array("do" => "play", "media" => "confirm.wav", "barge_in" => false),
            array("do" => "gather", "max_digits" => 1, "timeout_ms" => 10000,
                "callback_url" => "http://127.0.0.1:9099/confirmed", "wait" => true,
                "digits" => array("timeout" => array(array("do" => "hangup")))),
        ),
    ));
    return;
}
if ($format === "dotsip.dtmf") {
    if ((isset($body["digits"]) ? $body["digits"] : "") === "1") {
        $call = isset($body["call_id"]) ? $body["call_id"] : "";
        $phone = $db->query("SELECT phone FROM pending WHERE call_id = " . $db->quote($call))->fetchColumn();
        if ($phone) {
            $stmt = $db->prepare("UPDATE patients SET confirmed = 1 WHERE phone = ?");
            $stmt->execute(array($phone));
        }
    }
    echo json_encode(array("commands" => array(array("do" => "hangup"))));
    return;
}
http_response_code(400);
echo json_encode(array("error" => "bad request"));

After the patient presses 1, patients.confirmed is 1 for the phone stored in pending for that call_id. DotSIP does not write that column. behavior none with no route does not send the route POST.