Full Examples Python · Swift · Go

Three complete, runnable programmable-voice apps — each written the same way in Python, Swift and Go. They're deliberately small and dependency-light (just a standard-library HTTP server, HMAC and JSON), so you can read one in a sitting and build your own service over it.

How they work

On a call, the PBX POSTs an event to your endpoint and runs the list of verbs you return (speak, collect digits, transfer, record, hang up), then POSTs the result of each step back for the next. Your service owns all the logic and data; the PBX is a thin voice runtime. Every request is HMAC-signed — each sample includes a verify() you can lift verbatim.

Running a sample

Set two environment variables and run the single file. Point a Programmable Voice endpoint's control URL at http://<this-machine>:<port>/<path> and use the same secret.

export VOICER_SECRET='the-endpoint-shared-secret'    # must match the PBX endpoint
export VOICER_BASE_URL='http://192.168.10.50:8080'   # how the PBX reaches THIS app (LAN IP, not localhost)
export PORT=8080

python3 auto-attendant/python/app.py     # Python 3.7+, stdlib only
go run    auto-attendant/go/main.go       # Go 1.16+, stdlib only
swift     auto-attendant/swift/main.swift # macOS, stdlib only

You can also drive a running sample without a phone by signing a request the way the PBX would — see the README.md bundled with the download links below.

1. Auto Attendant

The classic phone menu: greet the caller, collect one keypad digit, and branch to a department.

control URL /ivr verbs: say · gather (DTMF) · route · forward · redirect

#!/usr/bin/env python3
"""
VoicerOnePBX Programmable Voice — Example 1: Auto Attendant
===========================================================

The classic phone menu: greet the caller, collect a single digit, and branch to a department. This is the
"hello world" of the platform — one prompt, one gather, a switch statement.

The control flow, in the platform's request→response loop:

    caller dials the DID
        │
        ▼  POST /ivr        (event = call.inbound.pre_route)
    reply: [ say greeting, gather 1 digit → /ivr/main ]
        │
        ▼  POST /ivr/main   (event = gather.completed, digits = "2")
    reply: [ say "...", route/forward/redirect ]   ← ends or loops the call

Everything below the "HTTP plumbing" divider is generic scaffolding you can lift unchanged into the other
examples; the interesting part is `handle_ivr` and `handle_main`.

Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 python3 app.py
Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/ivr
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

# ── Configuration (env) ──────────────────────────────────────────────────────
SECRET = os.environ.get("VOICER_SECRET", "")                       # HMAC shared secret; must match the PBX endpoint
BASE   = os.environ.get("VOICER_BASE_URL", "http://localhost:8080")  # how the PBX reaches THIS app (for actionUrls)
PORT   = int(os.environ.get("PORT", "8080"))

# Where each menu choice sends the caller. `route.to` values are IDs the PBX admin configured — an extension,
# a queue, a ring group, or a voicemail box. Ask the admin for the real ones; these are illustrative.
DEPARTMENTS = {
    "1": ("Connecting you to sales.",   {"route": {"to": "sales"}}),      # a queue named "sales"
    "2": ("Connecting you to support.", {"route": {"to": "support"}}),    # a queue named "support"
    "0": ("Connecting you to reception.", {"route": {"to": "100"}}),      # extension 100
    "9": ("Connecting your call.",      {"forward": {"to": "+19055550134"}}),  # an external number, via a trunk
}


# ── The handlers — this is the app ───────────────────────────────────────────

def handle_ivr(event: dict) -> list:
    """Inbound call arrived (event = call.inbound.pre_route). Greet and open the menu."""
    # `event` has callId, from, to, callerName, trunkId — branch on `to` here if one app serves several DIDs.
    return [
        {"say": {"text": "Thank you for calling Acme. "
                         "For sales press 1, for support press 2, "
                         "for reception press 0, or press 9 to reach our after-hours line."}},
        # A gather ENDS this document. The PBX plays the prompt, collects one digit (or times out after 6 s),
        # then POSTs gather.completed to actionUrl. actionUrl must be absolute and reachable by the PBX.
        {"gather": {"numDigits": 1, "timeout": 6, "actionUrl": f"{BASE}/ivr/main"}},
    ]


def handle_main(event: dict) -> list:
    """A menu digit came back (event = gather.completed). Branch to the chosen department."""
    reason = event.get("reason")
    digits = event.get("digits", "")

    # The caller hung up mid-gather: the call is already gone, so acknowledge with an empty document.
    if reason == "hangup":
        return []

    # No input in time, or a key we don't recognise: re-greet by JUMPING back to /ivr with `redirect`
    # (fetches that CCD with no further input — a clean state-machine loop).
    if reason == "timeout" or digits not in DEPARTMENTS:
        return [
            {"say": {"text": "Sorry, I did not get that."}},
            {"redirect": {"url": f"{BASE}/ivr"}},
        ]

    # A valid choice: say the confirmation and hand the call to the department. `route`/`forward` end the flow.
    phrase, transfer = DEPARTMENTS[digits]
    return [{"say": {"text": phrase}}, transfer]


# Map request path → handler. Add a route here for each actionUrl you hand out.
ROUTES = {
    "/ivr":      handle_ivr,
    "/ivr/main": handle_main,
}


# ── HTTP plumbing (generic; identical across the three examples) ──────────────

def verify(raw_body: bytes, headers) -> bool:
    """Validate the PBX's HMAC signature. See the contract §7.

    signature = HMAC-SHA256(key=SECRET, msg=timestamp + "." + rawBody), hex, sent as "sha256=<hex>".
    """
    if not SECRET:
        return True  # dev mode — verification skipped (warned at startup)
    ts = headers.get("X-Voicer-Timestamp", "")
    sent = headers.get("X-Voicer-Signature", "").replace("sha256=", "")
    expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    # Optionally also reject a stale `ts` here (e.g. abs(now - ts) > 300) to blunt replay.
    return hmac.compare_digest(expected, sent)


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def log_message(self, *_):  # quiet the default access log; we print our own line
        pass

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length)

        if not verify(raw, self.headers):
            self._send(403, b'{"error":"bad signature"}', "application/json")
            print(f"  ✗ {self.path}  REJECTED (bad HMAC signature)")
            return

        handler = ROUTES.get(self.path.split("?")[0])
        if handler is None:
            self._send(404, b'{"error":"no such route"}', "application/json")
            return

        event = json.loads(raw or b"{}")
        print(f"  → {self.path}  event={event.get('event')} "
              f"digits={event.get('digits','')!r} callId={event.get('callId','')[:8]}")

        ccd = handler(event)                                  # ← run the app logic
        self._send(200, json.dumps(ccd).encode(), "application/json")

    def _send(self, code: int, body: bytes, ctype: str):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def main():
    print(f"Auto-attendant sample on :{PORT}  (control URL → {BASE}/ivr)")
    if not SECRET:
        print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()


if __name__ == "__main__":
    main()
// VoicerOnePBX Programmable Voice — Example 1: Auto Attendant (Swift, stdlib only)
//
// The classic phone menu: greet, collect one digit, branch to a department.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
// Uses only system frameworks (Foundation, Network, CryptoKit) — no packages.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 swift main.swift
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/ivr

import Foundation
import Network
import CryptoKit

let SECRET = ProcessInfo.processInfo.environment["VOICER_SECRET"] ?? ""
let BASE   = ProcessInfo.processInfo.environment["VOICER_BASE_URL"] ?? "http://localhost:8080"
let PORT   = UInt16(ProcessInfo.processInfo.environment["PORT"] ?? "8080") ?? 8080

typealias Verb = [String: Any]
typealias CCD = [Verb]

// Where each menu choice sends the caller. `route.to` values are IDs the PBX admin configured.
let department: [String: (phrase: String, transfer: Verb)] = [
    "1": ("Connecting you to sales.",     ["route": ["to": "sales"]]),
    "2": ("Connecting you to support.",   ["route": ["to": "support"]]),
    "0": ("Connecting you to reception.", ["route": ["to": "100"]]),
    "9": ("Connecting your call.",        ["forward": ["to": "+19055550134"]]),
]

// ── The handlers — this is the app ───────────────────────────────────────────

func handleIVR(_ event: [String: Any]) -> CCD {
    [
        ["say": ["text": "Thank you for calling Acme. For sales press 1, for support press 2, "
                       + "for reception press 0, or press 9 to reach our after-hours line."]],
        ["gather": ["numDigits": 1, "timeout": 6, "actionUrl": "\(BASE)/ivr/main"]],
    ]
}

func handleMain(_ event: [String: Any]) -> CCD {
    let reason = event["reason"] as? String ?? ""
    let digits = event["digits"] as? String ?? ""
    if reason == "hangup" { return [] }                          // caller gone
    guard reason != "timeout", let dep = department[digits] else {
        return [["say": ["text": "Sorry, I did not get that."]],
                ["redirect": ["url": "\(BASE)/ivr"]]]
    }
    return [["say": ["text": dep.phrase]], dep.transfer]
}

func route(path: String, event: [String: Any]) -> CCD? {
    switch path {
    case "/ivr":      return handleIVR(event)
    case "/ivr/main": return handleMain(event)
    default:          return nil
    }
}

// ── HTTP plumbing (generic; identical across the three examples) ──────────────

/// Validate the PBX's HMAC signature: sha256=hex(HMAC-SHA256(secret, ts + "." + body)). See contract §7.
func verify(body: Data, headers: [String: String]) -> Bool {
    if SECRET.isEmpty { return true }                            // dev mode — skipped (warned at startup)
    let ts = headers["x-voicer-timestamp"] ?? ""
    let sent = (headers["x-voicer-signature"] ?? "").replacingOccurrences(of: "sha256=", with: "")
    var msg = Data("\(ts).".utf8); msg.append(body)
    let mac = HMAC<SHA256>.authenticationCode(for: msg, using: SymmetricKey(data: Data(SECRET.utf8)))
    let hexStr = mac.map { String(format: "%02x", $0) }.joined()
    // constant-time compare
    guard hexStr.utf8.count == sent.utf8.count else { return false }
    var diff: UInt8 = 0
    for (a, b) in zip(hexStr.utf8, sent.utf8) { diff |= a ^ b }
    return diff == 0
}

/// Verify → parse event → run the route → serialize the CCD. Returns (status, contentType, body).
func appHandler(path: String, headers: [String: String], body: Data) -> (Int, String, Data) {
    if !verify(body: body, headers: headers) {
        return (403, "application/json", Data(#"{"error":"bad signature"}"#.utf8))
    }
    let event = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] ?? [:]
    print("  → \(path)  event=\(event["event"] ?? "") digits=\(event["digits"] ?? "")")
    guard let ccd = route(path: path, event: event) else {
        return (404, "application/json", Data(#"{"error":"no such route"}"#.utf8))
    }
    let data = (try? JSONSerialization.data(withJSONObject: ccd)) ?? Data("[]".utf8)
    return (200, "application/json", data)
}

// A minimal HTTP/1.1 server over the Network framework. Reads one request, replies, closes. In production use
// Vapor/Hummingbird and keep only `verify` + the handlers above.
func startServer() {
    let listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: PORT)!)
    listener.newConnectionHandler = { conn in
        conn.start(queue: .global())
        var buf = Data()
        func read() {
            conn.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, done, err in
                if let data = data { buf.append(data) }
                if let hdrEnd = buf.range(of: Data("\r\n\r\n".utf8)) {
                    let head = String(data: buf.subdata(in: 0..<hdrEnd.lowerBound), encoding: .utf8) ?? ""
                    let lines = head.components(separatedBy: "\r\n")
                    let reqParts = lines.first?.components(separatedBy: " ") ?? []
                    let path = reqParts.count > 1 ? reqParts[1] : "/"
                    var headers = [String: String]()
                    for l in lines.dropFirst() {
                        if let c = l.firstIndex(of: ":") {
                            headers[l[..<c].trimmingCharacters(in: .whitespaces).lowercased()] =
                                l[l.index(after: c)...].trimmingCharacters(in: .whitespaces)
                        }
                    }
                    let clen = Int(headers["content-length"] ?? "0") ?? 0
                    let bodyStart = hdrEnd.upperBound
                    if buf.count - bodyStart >= clen {
                        let body = buf.subdata(in: bodyStart..<(bodyStart + clen))
                        let (code, ctype, respBody) = appHandler(path: path, headers: headers, body: body)
                        let status = ["200": "200 OK", "403": "403 Forbidden", "404": "404 Not Found"]["\(code)"] ?? "\(code)"
                        var out = Data("HTTP/1.1 \(status)\r\nContent-Type: \(ctype)\r\nContent-Length: \(respBody.count)\r\nConnection: close\r\n\r\n".utf8)
                        out.append(respBody)
                        conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() })
                        return
                    }
                }
                if done || err != nil { conn.cancel(); return }
                read()
            }
        }
        read()
    }
    listener.start(queue: .global())
    print("Auto-attendant sample on :\(PORT)  (control URL → \(BASE)/ivr)")
    if SECRET.isEmpty { print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).") }
}

startServer()
dispatchMain()
// VoicerOnePBX Programmable Voice — Example 1: Auto Attendant (Go, stdlib only)
//
// The classic phone menu: greet, collect one digit, branch to a department.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 go run main.go
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/ivr
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
)

var (
	secret = os.Getenv("VOICER_SECRET")                    // must match the PBX endpoint
	base   = envOr("VOICER_BASE_URL", "http://localhost:8080") // how the PBX reaches this app (for actionUrls)
	port   = envOr("PORT", "8080")
)

// A verb is a single-key object; a CCD is an ordered slice of them.
type Verb map[string]any
type CCD []Verb

// Event is the PBX→you request body (only the fields these handlers read).
type Event struct {
	Event    string `json:"event"`
	CallID   string `json:"callId"`
	From     string `json:"from"`
	To       string `json:"to"`
	Digits   string `json:"digits"`
	Speech   string `json:"speech"`
	Reason   string `json:"reason"`
}

// department maps a menu digit to (spoken confirmation, transfer verb).
var department = map[string]struct {
	phrase   string
	transfer Verb
}{
	"1": {"Connecting you to sales.", Verb{"route": map[string]any{"to": "sales"}}},
	"2": {"Connecting you to support.", Verb{"route": map[string]any{"to": "support"}}},
	"0": {"Connecting you to reception.", Verb{"route": map[string]any{"to": "100"}}},
	"9": {"Connecting your call.", Verb{"forward": map[string]any{"to": "+19055550134"}}},
}

// ── The handlers — this is the app ───────────────────────────────────────────

func handleIVR(e Event) CCD {
	return CCD{
		{"say": map[string]any{"text": "Thank you for calling Acme. " +
			"For sales press 1, for support press 2, for reception press 0, " +
			"or press 9 to reach our after-hours line."}},
		{"gather": map[string]any{"numDigits": 1, "timeout": 6, "actionUrl": base + "/ivr/main"}},
	}
}

func handleMain(e Event) CCD {
	if e.Reason == "hangup" {
		return CCD{} // caller gone
	}
	dep, ok := department[e.Digits]
	if e.Reason == "timeout" || !ok {
		return CCD{
			{"say": map[string]any{"text": "Sorry, I did not get that."}},
			{"redirect": map[string]any{"url": base + "/ivr"}},
		}
	}
	return CCD{{"say": map[string]any{"text": dep.phrase}}, dep.transfer}
}

var routes = map[string]func(Event) CCD{
	"/ivr":      handleIVR,
	"/ivr/main": handleMain,
}

// ── HTTP plumbing (generic; identical across the three examples) ──────────────

// verify checks the PBX's HMAC signature: sha256=hex(HMAC-SHA256(secret, ts + "." + body)).
func verify(body []byte, h http.Header) bool {
	if secret == "" {
		return true // dev mode — verification skipped (warned at startup)
	}
	ts := h.Get("X-Voicer-Timestamp")
	sent := trimPrefix(h.Get("X-Voicer-Signature"), "sha256=")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(sent))
}

func handle(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	if !verify(body, r.Header) {
		http.Error(w, `{"error":"bad signature"}`, http.StatusForbidden)
		return
	}
	fn, ok := routes[r.URL.Path]
	if !ok {
		http.Error(w, `{"error":"no such route"}`, http.StatusNotFound)
		return
	}
	var e Event
	_ = json.Unmarshal(body, &e)
	log.Printf("  → %s  event=%s digits=%q callId=%.8s", r.URL.Path, e.Event, e.Digits, e.CallID)

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(fn(e)) // ← run the app logic
}

func main() {
	for p := range routes {
		http.HandleFunc(p, handle)
	}
	fmt.Printf("Auto-attendant sample on :%s  (control URL → %s/ivr)\n", port, base)
	if secret == "" {
		fmt.Println("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
	}
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

func trimPrefix(s, p string) string {
	if len(s) >= len(p) && s[:len(p)] == p {
		return s[len(p):]
	}
	return s
}

Download: Python (app.py) · Swift (main.swift) · Go (main.go)

2. Automated Survey

A multi-step flow that keeps state across callbacks (keyed by callId) and ends by recording a spoken comment.

control URL /survey verbs: say · gather (DTMF) · record · hangup

#!/usr/bin/env python3
"""
VoicerOnePBX Programmable Voice — Example 2: Phone Survey
========================================================

A multi-step flow that keeps **state across callbacks**. The whole point of this example is the pattern for
carrying data from one step to the next: the platform is stateless between requests, so YOU hold the state,
keyed by the stable `callId` the PBX echoes on every callback.

Flow (a 3-question satisfaction survey ending in a recorded comment):

    POST /survey            (call.inbound.pre_route)   → Q1: rate 1–5
    POST /survey/q2         (gather.completed)         store Q1 → Q2: recommend us? 1=yes 2=no
    POST /survey/q3         (gather.completed)         store Q2 → "leave a comment after the tone"
    POST /survey/done       (record.completed)         store transcript → thank + hang up, print summary

`record` is the same request→response shape as `gather`: it POSTs record.completed to its actionUrl and you
return the next CCD.

Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 python3 app.py
Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/survey
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRET = os.environ.get("VOICER_SECRET", "")
BASE   = os.environ.get("VOICER_BASE_URL", "http://localhost:8080")
PORT   = int(os.environ.get("PORT", "8080"))

# Per-call state, keyed by callId. In production this is a row in your database, not a process-memory dict —
# but the KEY is the same: the callId the PBX gives you on every callback for this call.
RESPONSES: dict[str, dict] = {}


# ── The handlers — this is the app ───────────────────────────────────────────

def handle_start(event: dict) -> list:
    """Inbound call: open a fresh record for this callId and ask question 1."""
    RESPONSES[event["callId"]] = {"from": event.get("from"), "rating": None, "recommend": None, "comment": None}
    return [
        {"say": {"text": "Thanks for taking our two-question survey. "
                         "On a scale of 1 to 5, how satisfied were you with your call today? "
                         "Press a number from 1 to 5."}},
        {"gather": {"numDigits": 1, "timeout": 8, "actionUrl": f"{BASE}/survey/q2"}},
    ]


def handle_q2(event: dict) -> list:
    """Answer to Q1 arrived. Store it, then ask Q2."""
    rec = RESPONSES.get(event["callId"])
    if rec is None or event.get("reason") == "hangup":
        return []                                            # caller gone or unknown call — nothing to do
    if event.get("digits"):
        rec["rating"] = event["digits"]                       # ← state carried forward by callId
    return [
        {"say": {"text": "Thank you. Would you recommend us to a friend? Press 1 for yes, 2 for no."}},
        {"gather": {"numDigits": 1, "timeout": 8, "actionUrl": f"{BASE}/survey/q3"}},
    ]


def handle_q3(event: dict) -> list:
    """Answer to Q2 arrived. Store it, then invite a recorded comment."""
    rec = RESPONSES.get(event["callId"])
    if rec is None or event.get("reason") == "hangup":
        return []
    rec["recommend"] = {"1": "yes", "2": "no"}.get(event.get("digits", ""), "unknown")
    return [
        {"say": {"text": "Last thing: leave any comments after the tone, then press pound. "
                         "Or press pound now to finish."}},
        # `record` posts record.completed to actionUrl. transcribe=true gives an on-device transcript;
        # playBeep=true plays the tone the prompt promised.
        {"record": {"maxSeconds": 30, "finishOnKey": "#", "transcribe": True, "playBeep": True,
                    "actionUrl": f"{BASE}/survey/done"}},
    ]


def handle_done(event: dict) -> list:
    """The recording finished. Store the transcript, finalize, and thank the caller."""
    rec = RESPONSES.pop(event["callId"], None)
    if rec is not None:
        rec["comment"] = event.get("transcript", "")          # empty if the caller skipped or said nothing
        # "Persist" the completed survey. Replace this print with a DB insert.
        print("  ✅ SURVEY COMPLETE:", json.dumps(rec))
    # A document with no gather/record/redirect ends the call after it finishes speaking; hangup is explicit.
    return [{"say": {"text": "Thank you for your feedback. Goodbye."}}, {"hangup": {}}]


ROUTES = {
    "/survey":    handle_start,
    "/survey/q2": handle_q2,
    "/survey/q3": handle_q3,
    "/survey/done": handle_done,
}


# ── HTTP plumbing (generic; identical across the three examples) ──────────────

def verify(raw_body: bytes, headers) -> bool:
    if not SECRET:
        return True
    ts = headers.get("X-Voicer-Timestamp", "")
    sent = headers.get("X-Voicer-Signature", "").replace("sha256=", "")
    expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sent)


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def log_message(self, *_):
        pass

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length)
        if not verify(raw, self.headers):
            self._send(403, b'{"error":"bad signature"}', "application/json")
            return
        handler = ROUTES.get(self.path.split("?")[0])
        if handler is None:
            self._send(404, b'{"error":"no such route"}', "application/json")
            return
        event = json.loads(raw or b"{}")
        print(f"  → {self.path}  event={event.get('event')} "
              f"digits={event.get('digits','')!r} callId={event.get('callId','')[:8]}")
        ccd = handler(event)
        self._send(200, json.dumps(ccd).encode(), "application/json")

    def _send(self, code: int, body: bytes, ctype: str):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def main():
    print(f"Survey sample on :{PORT}  (control URL → {BASE}/survey)")
    if not SECRET:
        print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()


if __name__ == "__main__":
    main()
// VoicerOnePBX Programmable Voice — Example 2: Phone Survey (Swift, stdlib only)
//
// A multi-step flow that keeps state across callbacks, keyed by the stable callId.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
// Uses only system frameworks (Foundation, Network, CryptoKit) — no packages.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 swift main.swift
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/survey

import Foundation
import Network
import CryptoKit

let SECRET = ProcessInfo.processInfo.environment["VOICER_SECRET"] ?? ""
let BASE   = ProcessInfo.processInfo.environment["VOICER_BASE_URL"] ?? "http://localhost:8080"
let PORT   = UInt16(ProcessInfo.processInfo.environment["PORT"] ?? "8080") ?? 8080

typealias Verb = [String: Any]
typealias CCD = [Verb]

// Per-call state, keyed by callId. In production this is a DB row; the KEY is what matters. Lock-guarded
// because connections are handled concurrently on a global queue.
final class Store {
    private let lock = NSLock()
    private var records: [String: [String: String]] = [:]
    func start(_ id: String, from: String) { lock.lock(); records[id] = ["from": from]; lock.unlock() }
    func set(_ id: String, _ key: String, _ value: String) {
        lock.lock(); if records[id] != nil { records[id]![key] = value }; lock.unlock()
    }
    func exists(_ id: String) -> Bool { lock.lock(); defer { lock.unlock() }; return records[id] != nil }
    func take(_ id: String) -> [String: String]? { lock.lock(); defer { lock.unlock() }; return records.removeValue(forKey: id) }
}
let store = Store()

// ── The handlers — this is the app ───────────────────────────────────────────

func handleStart(_ e: [String: Any]) -> CCD {
    store.start(e["callId"] as? String ?? "", from: e["from"] as? String ?? "")
    return [
        ["say": ["text": "Thanks for taking our two-question survey. On a scale of 1 to 5, how satisfied "
                       + "were you with your call today? Press a number from 1 to 5."]],
        ["gather": ["numDigits": 1, "timeout": 8, "actionUrl": "\(BASE)/survey/q2"]],
    ]
}

func handleQ2(_ e: [String: Any]) -> CCD {
    let id = e["callId"] as? String ?? ""
    guard store.exists(id), (e["reason"] as? String) != "hangup" else { return [] }
    if let d = e["digits"] as? String, !d.isEmpty { store.set(id, "rating", d) }   // ← carried forward by callId
    return [
        ["say": ["text": "Thank you. Would you recommend us to a friend? Press 1 for yes, 2 for no."]],
        ["gather": ["numDigits": 1, "timeout": 8, "actionUrl": "\(BASE)/survey/q3"]],
    ]
}

func handleQ3(_ e: [String: Any]) -> CCD {
    let id = e["callId"] as? String ?? ""
    guard store.exists(id), (e["reason"] as? String) != "hangup" else { return [] }
    store.set(id, "recommend", ["1": "yes", "2": "no"][e["digits"] as? String ?? ""] ?? "unknown")
    return [
        ["say": ["text": "Last thing: leave any comments after the tone, then press pound. "
                       + "Or press pound now to finish."]],
        ["record": ["maxSeconds": 30, "finishOnKey": "#", "transcribe": true, "playBeep": true,
                    "actionUrl": "\(BASE)/survey/done"]],
    ]
}

func handleDone(_ e: [String: Any]) -> CCD {
    if var rec = store.take(e["callId"] as? String ?? "") {
        rec["comment"] = e["transcript"] as? String ?? ""
        print("  ✅ SURVEY COMPLETE: \(rec)")                    // replace with a DB insert
    }
    return [["say": ["text": "Thank you for your feedback. Goodbye."]], ["hangup": [:]]]
}

func route(path: String, event: [String: Any]) -> CCD? {
    switch path {
    case "/survey":      return handleStart(event)
    case "/survey/q2":   return handleQ2(event)
    case "/survey/q3":   return handleQ3(event)
    case "/survey/done": return handleDone(event)
    default:             return nil
    }
}

// ── HTTP plumbing (generic; identical across the three examples) ──────────────

func verify(body: Data, headers: [String: String]) -> Bool {
    if SECRET.isEmpty { return true }
    let ts = headers["x-voicer-timestamp"] ?? ""
    let sent = (headers["x-voicer-signature"] ?? "").replacingOccurrences(of: "sha256=", with: "")
    var msg = Data("\(ts).".utf8); msg.append(body)
    let mac = HMAC<SHA256>.authenticationCode(for: msg, using: SymmetricKey(data: Data(SECRET.utf8)))
    let hexStr = mac.map { String(format: "%02x", $0) }.joined()
    guard hexStr.utf8.count == sent.utf8.count else { return false }
    var diff: UInt8 = 0
    for (a, b) in zip(hexStr.utf8, sent.utf8) { diff |= a ^ b }
    return diff == 0
}

func appHandler(path: String, headers: [String: String], body: Data) -> (Int, String, Data) {
    if !verify(body: body, headers: headers) {
        return (403, "application/json", Data(#"{"error":"bad signature"}"#.utf8))
    }
    let event = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] ?? [:]
    print("  → \(path)  event=\(event["event"] ?? "") digits=\(event["digits"] ?? "")")
    guard let ccd = route(path: path, event: event) else {
        return (404, "application/json", Data(#"{"error":"no such route"}"#.utf8))
    }
    let data = (try? JSONSerialization.data(withJSONObject: ccd)) ?? Data("[]".utf8)
    return (200, "application/json", data)
}

func startServer() {
    let listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: PORT)!)
    listener.newConnectionHandler = { conn in
        conn.start(queue: .global())
        var buf = Data()
        func read() {
            conn.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, done, err in
                if let data = data { buf.append(data) }
                if let hdrEnd = buf.range(of: Data("\r\n\r\n".utf8)) {
                    let head = String(data: buf.subdata(in: 0..<hdrEnd.lowerBound), encoding: .utf8) ?? ""
                    let lines = head.components(separatedBy: "\r\n")
                    let reqParts = lines.first?.components(separatedBy: " ") ?? []
                    let path = reqParts.count > 1 ? reqParts[1] : "/"
                    var headers = [String: String]()
                    for l in lines.dropFirst() {
                        if let c = l.firstIndex(of: ":") {
                            headers[l[..<c].trimmingCharacters(in: .whitespaces).lowercased()] =
                                l[l.index(after: c)...].trimmingCharacters(in: .whitespaces)
                        }
                    }
                    let clen = Int(headers["content-length"] ?? "0") ?? 0
                    let bodyStart = hdrEnd.upperBound
                    if buf.count - bodyStart >= clen {
                        let body = buf.subdata(in: bodyStart..<(bodyStart + clen))
                        let (code, ctype, respBody) = appHandler(path: path, headers: headers, body: body)
                        let status = ["200": "200 OK", "403": "403 Forbidden", "404": "404 Not Found"]["\(code)"] ?? "\(code)"
                        var out = Data("HTTP/1.1 \(status)\r\nContent-Type: \(ctype)\r\nContent-Length: \(respBody.count)\r\nConnection: close\r\n\r\n".utf8)
                        out.append(respBody)
                        conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() })
                        return
                    }
                }
                if done || err != nil { conn.cancel(); return }
                read()
            }
        }
        read()
    }
    listener.start(queue: .global())
    print("Survey sample on :\(PORT)  (control URL → \(BASE)/survey)")
    if SECRET.isEmpty { print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).") }
}

startServer()
dispatchMain()
// VoicerOnePBX Programmable Voice — Example 2: Phone Survey (Go, stdlib only)
//
// A multi-step flow that keeps state across callbacks, keyed by the stable callId.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 go run main.go
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/survey
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"sync"
)

var (
	secret = os.Getenv("VOICER_SECRET")
	base   = envOr("VOICER_BASE_URL", "http://localhost:8080")
	port   = envOr("PORT", "8080")
)

type Verb map[string]any
type CCD []Verb

type Event struct {
	Event      string `json:"event"`
	CallID     string `json:"callId"`
	From       string `json:"from"`
	Digits     string `json:"digits"`
	Reason     string `json:"reason"`
	Transcript string `json:"transcript"`
}

// Per-call state, keyed by callId. In production this is a DB row; the KEY is what matters.
type response struct {
	From, Rating, Recommend, Comment string
}

var (
	mu        sync.Mutex
	responses = map[string]*response{}
)

// ── The handlers — this is the app ───────────────────────────────────────────

func handleStart(e Event) CCD {
	mu.Lock()
	responses[e.CallID] = &response{From: e.From}
	mu.Unlock()
	return CCD{
		{"say": map[string]any{"text": "Thanks for taking our two-question survey. " +
			"On a scale of 1 to 5, how satisfied were you with your call today? " +
			"Press a number from 1 to 5."}},
		{"gather": map[string]any{"numDigits": 1, "timeout": 8, "actionUrl": base + "/survey/q2"}},
	}
}

func handleQ2(e Event) CCD {
	mu.Lock()
	rec := responses[e.CallID]
	if rec != nil && e.Digits != "" {
		rec.Rating = e.Digits // ← state carried forward by callId
	}
	mu.Unlock()
	if rec == nil || e.Reason == "hangup" {
		return CCD{}
	}
	return CCD{
		{"say": map[string]any{"text": "Thank you. Would you recommend us to a friend? Press 1 for yes, 2 for no."}},
		{"gather": map[string]any{"numDigits": 1, "timeout": 8, "actionUrl": base + "/survey/q3"}},
	}
}

func handleQ3(e Event) CCD {
	mu.Lock()
	rec := responses[e.CallID]
	if rec != nil {
		rec.Recommend = map[string]string{"1": "yes", "2": "no"}[e.Digits]
		if rec.Recommend == "" {
			rec.Recommend = "unknown"
		}
	}
	mu.Unlock()
	if rec == nil || e.Reason == "hangup" {
		return CCD{}
	}
	return CCD{
		{"say": map[string]any{"text": "Last thing: leave any comments after the tone, then press pound. " +
			"Or press pound now to finish."}},
		{"record": map[string]any{"maxSeconds": 30, "finishOnKey": "#", "transcribe": true, "playBeep": true,
			"actionUrl": base + "/survey/done"}},
	}
}

func handleDone(e Event) CCD {
	mu.Lock()
	rec := responses[e.CallID]
	delete(responses, e.CallID)
	mu.Unlock()
	if rec != nil {
		rec.Comment = e.Transcript
		out, _ := json.Marshal(rec)
		log.Printf("  ✅ SURVEY COMPLETE: %s", out) // replace with a DB insert
	}
	return CCD{{"say": map[string]any{"text": "Thank you for your feedback. Goodbye."}}, {"hangup": map[string]any{}}}
}

var routes = map[string]func(Event) CCD{
	"/survey":      handleStart,
	"/survey/q2":   handleQ2,
	"/survey/q3":   handleQ3,
	"/survey/done": handleDone,
}

// ── HTTP plumbing (generic; identical across the three examples) ──────────────

func verify(body []byte, h http.Header) bool {
	if secret == "" {
		return true
	}
	ts := h.Get("X-Voicer-Timestamp")
	sent := trimPrefix(h.Get("X-Voicer-Signature"), "sha256=")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(sent))
}

func handle(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	if !verify(body, r.Header) {
		http.Error(w, `{"error":"bad signature"}`, http.StatusForbidden)
		return
	}
	fn, ok := routes[r.URL.Path]
	if !ok {
		http.Error(w, `{"error":"no such route"}`, http.StatusNotFound)
		return
	}
	var e Event
	_ = json.Unmarshal(body, &e)
	log.Printf("  → %s  event=%s digits=%q callId=%.8s", r.URL.Path, e.Event, e.Digits, e.CallID)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(fn(e))
}

func main() {
	for p := range routes {
		http.HandleFunc(p, handle)
	}
	fmt.Printf("Survey sample on :%s  (control URL → %s/survey)\n", port, base)
	if secret == "" {
		fmt.Println("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
	}
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

func trimPrefix(s, p string) string {
	if len(s) >= len(p) && s[:len(p)] == p {
		return s[len(p):]
	}
	return s
}

Download: Python (app.py) · Swift (main.swift) · Go (main.go)

The caller speaks a question; your backend searches a knowledge base and reads the answer back.

control URL /support verbs: say · gather (speech) · redirect · route · hangup

#!/usr/bin/env python3
"""
VoicerOnePBX Programmable Voice — Example 3: Spoken Support Search
=================================================================

The caller SPEAKS a question; your backend searches a knowledge base and reads the answer back. This example
is about **speech input** (`gather` with `input:"speech"`) and wiring a gather callback to a real backend
lookup — the front end for a support/FAQ system.

Flow:

    POST /support           (call.inbound.pre_route)   → "ask your question after the tone"  (speech gather)
    POST /support/search    (gather.completed)         transcribe → search KB → read answer → offer next
    POST /support/after     (gather.completed)         1 = ask another · 2 = human agent · else = goodbye

Speech is transcribed **on-device** by the PBX (Apple Speech — audio never leaves the box) and handed to you in
the `speech` field. Your job is just to search on that text.

Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 python3 app.py
Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/support
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRET = os.environ.get("VOICER_SECRET", "")
BASE   = os.environ.get("VOICER_BASE_URL", "http://localhost:8080")
PORT   = int(os.environ.get("PORT", "8080"))

# A toy knowledge base: each entry has trigger keywords and a spoken answer. Swap this for your real search
# (a DB full-text query, a vector store, an LLM call — the shape is the same: text in, answer out).
KB = [
    (("hours", "open", "close", "time"),
     "We are open Monday to Friday, 9 A M to 6 P M Eastern, and closed on weekends."),
    (("password", "reset", "login", "sign in"),
     "To reset your password, visit acme dot com slash reset and follow the emailed link."),
    (("refund", "return", "money back"),
     "Refunds are processed within 5 business days to your original payment method."),
    (("shipping", "delivery", "track", "order"),
     "Standard shipping takes 3 to 5 business days. You will get a tracking link by email once it ships."),
    (("cancel", "subscription", "unsubscribe"),
     "You can cancel any time from Account, then Billing, then Cancel Subscription."),
]


def search_kb(query: str) -> str | None:
    """Naive keyword search — return the answer whose keywords best overlap the spoken query."""
    q = query.lower()
    best, best_score = None, 0
    for keywords, answer in KB:
        score = sum(1 for k in keywords if k in q)
        if score > best_score:
            best, best_score = answer, score
    return best


# ── The handlers — this is the app ───────────────────────────────────────────

def handle_start(event: dict) -> list:
    """Inbound call: invite a spoken question."""
    return [
        {"say": {"text": "Welcome to Acme support. After the tone, ask your question in a few words. "
                         "For example, what are your hours, or how do I reset my password."}},
        # input:"speech" → the PBX records the caller, transcribes on-device, and returns the text in `speech`.
        # It stops on end-of-speech (silence), the speechTimeout window, or hangup. Use "dtmf speech" to also
        # allow a key-press to barge in (it would come back in `digits`).
        {"gather": {"input": "speech", "speechTimeout": 8, "actionUrl": f"{BASE}/support/search"}},
    ]


def handle_search(event: dict) -> list:
    """The transcript arrived. Search the KB and read back the best answer."""
    if event.get("reason") == "hangup":
        return []
    query = (event.get("speech") or "").strip()

    # Nothing recognised (silence/timeout) — apologise and loop back to the prompt with `redirect`.
    if not query:
        return [
            {"say": {"text": "Sorry, I didn't catch that."}},
            {"redirect": {"url": f"{BASE}/support"}},
        ]

    answer = search_kb(query)
    if answer is None:
        spoken = (f"I heard: {query}. I couldn't find an answer to that. "
                  "Let me connect you with someone who can help.")
        # No match → escalate straight to a human (an internal queue/extension).
        return [{"say": {"text": spoken}}, {"route": {"to": "support"}}]

    print(f"  🔎 query={query!r} → answered")
    return [
        {"say": {"text": f"Here's what I found. {answer}"}},
        {"say": {"text": "To ask another question press 1, to speak to an agent press 2, "
                         "or you can hang up. Thanks for calling."}},
        {"gather": {"numDigits": 1, "timeout": 6, "actionUrl": f"{BASE}/support/after"}},
    ]


def handle_after(event: dict) -> list:
    """After an answer: ask another, escalate, or end."""
    if event.get("reason") == "hangup":
        return []
    digits = event.get("digits", "")
    if digits == "1":
        return [{"redirect": {"url": f"{BASE}/support"}}]                 # loop for another question
    if digits == "2":
        return [{"say": {"text": "Connecting you to an agent."}}, {"route": {"to": "support"}}]
    return [{"say": {"text": "Thanks for calling Acme support. Goodbye."}}, {"hangup": {}}]


ROUTES = {
    "/support":        handle_start,
    "/support/search": handle_search,
    "/support/after":  handle_after,
}


# ── HTTP plumbing (generic; identical across the three examples) ──────────────

def verify(raw_body: bytes, headers) -> bool:
    if not SECRET:
        return True
    ts = headers.get("X-Voicer-Timestamp", "")
    sent = headers.get("X-Voicer-Signature", "").replace("sha256=", "")
    expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sent)


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def log_message(self, *_):
        pass

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length)
        if not verify(raw, self.headers):
            self._send(403, b'{"error":"bad signature"}', "application/json")
            return
        handler = ROUTES.get(self.path.split("?")[0])
        if handler is None:
            self._send(404, b'{"error":"no such route"}', "application/json")
            return
        event = json.loads(raw or b"{}")
        print(f"  → {self.path}  event={event.get('event')} "
              f"speech={event.get('speech','')!r} digits={event.get('digits','')!r}")
        ccd = handler(event)
        self._send(200, json.dumps(ccd).encode(), "application/json")

    def _send(self, code: int, body: bytes, ctype: str):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def main():
    print(f"Support-search sample on :{PORT}  (control URL → {BASE}/support)")
    if not SECRET:
        print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()


if __name__ == "__main__":
    main()
// VoicerOnePBX Programmable Voice — Example 3: Spoken Support Search (Swift, stdlib only)
//
// The caller speaks a question; the backend searches a knowledge base and reads the answer back.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
// Uses only system frameworks (Foundation, Network, CryptoKit) — no packages.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 swift main.swift
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/support

import Foundation
import Network
import CryptoKit

let SECRET = ProcessInfo.processInfo.environment["VOICER_SECRET"] ?? ""
let BASE   = ProcessInfo.processInfo.environment["VOICER_BASE_URL"] ?? "http://localhost:8080"
let PORT   = UInt16(ProcessInfo.processInfo.environment["PORT"] ?? "8080") ?? 8080

typealias Verb = [String: Any]
typealias CCD = [Verb]

// A toy knowledge base. Swap for a DB full-text query, a vector store, or an LLM call — text in, answer out.
let kb: [(keywords: [String], answer: String)] = [
    (["hours", "open", "close", "time"],
     "We are open Monday to Friday, 9 A M to 6 P M Eastern, and closed on weekends."),
    (["password", "reset", "login", "sign in"],
     "To reset your password, visit acme dot com slash reset and follow the emailed link."),
    (["refund", "return", "money back"],
     "Refunds are processed within 5 business days to your original payment method."),
    (["shipping", "delivery", "track", "order"],
     "Standard shipping takes 3 to 5 business days. You will get a tracking link by email once it ships."),
    (["cancel", "subscription", "unsubscribe"],
     "You can cancel any time from Account, then Billing, then Cancel Subscription."),
]

func searchKB(_ query: String) -> String? {
    let q = query.lowercased()
    var best: String?; var bestScore = 0
    for entry in kb {
        let score = entry.keywords.reduce(0) { $0 + (q.contains($1) ? 1 : 0) }
        if score > bestScore { best = entry.answer; bestScore = score }
    }
    return best
}

// ── The handlers — this is the app ───────────────────────────────────────────

func handleStart(_ e: [String: Any]) -> CCD {
    [
        ["say": ["text": "Welcome to Acme support. After the tone, ask your question in a few words. "
                       + "For example, what are your hours, or how do I reset my password."]],
        // input:"speech" → the PBX records + transcribes on-device, returning the text in `speech`.
        ["gather": ["input": "speech", "speechTimeout": 8, "actionUrl": "\(BASE)/support/search"]],
    ]
}

func handleSearch(_ e: [String: Any]) -> CCD {
    if (e["reason"] as? String) == "hangup" { return [] }
    let query = (e["speech"] as? String ?? "").trimmingCharacters(in: .whitespaces)
    if query.isEmpty {
        return [["say": ["text": "Sorry, I didn't catch that."]],
                ["redirect": ["url": "\(BASE)/support"]]]
    }
    guard let answer = searchKB(query) else {
        return [["say": ["text": "I heard: \(query). I couldn't find an answer to that. "
                               + "Let me connect you with someone who can help."]],
                ["route": ["to": "support"]]]
    }
    print("  🔎 query=\"\(query)\" → answered")
    return [
        ["say": ["text": "Here's what I found. \(answer)"]],
        ["say": ["text": "To ask another question press 1, to speak to an agent press 2, "
                       + "or you can hang up. Thanks for calling."]],
        ["gather": ["numDigits": 1, "timeout": 6, "actionUrl": "\(BASE)/support/after"]],
    ]
}

func handleAfter(_ e: [String: Any]) -> CCD {
    if (e["reason"] as? String) == "hangup" { return [] }
    switch e["digits"] as? String ?? "" {
    case "1": return [["redirect": ["url": "\(BASE)/support"]]]
    case "2": return [["say": ["text": "Connecting you to an agent."]], ["route": ["to": "support"]]]
    default:  return [["say": ["text": "Thanks for calling Acme support. Goodbye."]], ["hangup": [:]]]
    }
}

func route(path: String, event: [String: Any]) -> CCD? {
    switch path {
    case "/support":        return handleStart(event)
    case "/support/search": return handleSearch(event)
    case "/support/after":  return handleAfter(event)
    default:                return nil
    }
}

// ── HTTP plumbing (generic; identical across the three examples) ──────────────

func verify(body: Data, headers: [String: String]) -> Bool {
    if SECRET.isEmpty { return true }
    let ts = headers["x-voicer-timestamp"] ?? ""
    let sent = (headers["x-voicer-signature"] ?? "").replacingOccurrences(of: "sha256=", with: "")
    var msg = Data("\(ts).".utf8); msg.append(body)
    let mac = HMAC<SHA256>.authenticationCode(for: msg, using: SymmetricKey(data: Data(SECRET.utf8)))
    let hexStr = mac.map { String(format: "%02x", $0) }.joined()
    guard hexStr.utf8.count == sent.utf8.count else { return false }
    var diff: UInt8 = 0
    for (a, b) in zip(hexStr.utf8, sent.utf8) { diff |= a ^ b }
    return diff == 0
}

func appHandler(path: String, headers: [String: String], body: Data) -> (Int, String, Data) {
    if !verify(body: body, headers: headers) {
        return (403, "application/json", Data(#"{"error":"bad signature"}"#.utf8))
    }
    let event = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] ?? [:]
    print("  → \(path)  event=\(event["event"] ?? "") speech=\(event["speech"] ?? "") digits=\(event["digits"] ?? "")")
    guard let ccd = route(path: path, event: event) else {
        return (404, "application/json", Data(#"{"error":"no such route"}"#.utf8))
    }
    let data = (try? JSONSerialization.data(withJSONObject: ccd)) ?? Data("[]".utf8)
    return (200, "application/json", data)
}

func startServer() {
    let listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: PORT)!)
    listener.newConnectionHandler = { conn in
        conn.start(queue: .global())
        var buf = Data()
        func read() {
            conn.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, done, err in
                if let data = data { buf.append(data) }
                if let hdrEnd = buf.range(of: Data("\r\n\r\n".utf8)) {
                    let head = String(data: buf.subdata(in: 0..<hdrEnd.lowerBound), encoding: .utf8) ?? ""
                    let lines = head.components(separatedBy: "\r\n")
                    let reqParts = lines.first?.components(separatedBy: " ") ?? []
                    let path = reqParts.count > 1 ? reqParts[1] : "/"
                    var headers = [String: String]()
                    for l in lines.dropFirst() {
                        if let c = l.firstIndex(of: ":") {
                            headers[l[..<c].trimmingCharacters(in: .whitespaces).lowercased()] =
                                l[l.index(after: c)...].trimmingCharacters(in: .whitespaces)
                        }
                    }
                    let clen = Int(headers["content-length"] ?? "0") ?? 0
                    let bodyStart = hdrEnd.upperBound
                    if buf.count - bodyStart >= clen {
                        let body = buf.subdata(in: bodyStart..<(bodyStart + clen))
                        let (code, ctype, respBody) = appHandler(path: path, headers: headers, body: body)
                        let status = ["200": "200 OK", "403": "403 Forbidden", "404": "404 Not Found"]["\(code)"] ?? "\(code)"
                        var out = Data("HTTP/1.1 \(status)\r\nContent-Type: \(ctype)\r\nContent-Length: \(respBody.count)\r\nConnection: close\r\n\r\n".utf8)
                        out.append(respBody)
                        conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() })
                        return
                    }
                }
                if done || err != nil { conn.cancel(); return }
                read()
            }
        }
        read()
    }
    listener.start(queue: .global())
    print("Support-search sample on :\(PORT)  (control URL → \(BASE)/support)")
    if SECRET.isEmpty { print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).") }
}

startServer()
dispatchMain()
// VoicerOnePBX Programmable Voice — Example 3: Spoken Support Search (Go, stdlib only)
//
// The caller speaks a question; the backend searches a knowledge base and reads the answer back.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 go run main.go
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/support
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
)

var (
	secret = os.Getenv("VOICER_SECRET")
	base   = envOr("VOICER_BASE_URL", "http://localhost:8080")
	port   = envOr("PORT", "8080")
)

type Verb map[string]any
type CCD []Verb

type Event struct {
	Event  string `json:"event"`
	CallID string `json:"callId"`
	Digits string `json:"digits"`
	Speech string `json:"speech"`
	Reason string `json:"reason"`
}

// A toy knowledge base. Swap for a DB full-text query, a vector store, or an LLM call — text in, answer out.
var kb = []struct {
	keywords []string
	answer   string
}{
	{[]string{"hours", "open", "close", "time"},
		"We are open Monday to Friday, 9 A M to 6 P M Eastern, and closed on weekends."},
	{[]string{"password", "reset", "login", "sign in"},
		"To reset your password, visit acme dot com slash reset and follow the emailed link."},
	{[]string{"refund", "return", "money back"},
		"Refunds are processed within 5 business days to your original payment method."},
	{[]string{"shipping", "delivery", "track", "order"},
		"Standard shipping takes 3 to 5 business days. You will get a tracking link by email once it ships."},
	{[]string{"cancel", "subscription", "unsubscribe"},
		"You can cancel any time from Account, then Billing, then Cancel Subscription."},
}

func searchKB(query string) (string, bool) {
	q := strings.ToLower(query)
	best, bestScore := "", 0
	for _, e := range kb {
		score := 0
		for _, k := range e.keywords {
			if strings.Contains(q, k) {
				score++
			}
		}
		if score > bestScore {
			best, bestScore = e.answer, score
		}
	}
	return best, bestScore > 0
}

// ── The handlers — this is the app ───────────────────────────────────────────

func handleStart(e Event) CCD {
	return CCD{
		{"say": map[string]any{"text": "Welcome to Acme support. After the tone, ask your question in a few words. " +
			"For example, what are your hours, or how do I reset my password."}},
		// input:"speech" → the PBX records + transcribes on-device, returning the text in `speech`.
		{"gather": map[string]any{"input": "speech", "speechTimeout": 8, "actionUrl": base + "/support/search"}},
	}
}

func handleSearch(e Event) CCD {
	if e.Reason == "hangup" {
		return CCD{}
	}
	query := strings.TrimSpace(e.Speech)
	if query == "" {
		return CCD{
			{"say": map[string]any{"text": "Sorry, I didn't catch that."}},
			{"redirect": map[string]any{"url": base + "/support"}},
		}
	}
	answer, ok := searchKB(query)
	if !ok {
		return CCD{
			{"say": map[string]any{"text": fmt.Sprintf("I heard: %s. I couldn't find an answer to that. "+
				"Let me connect you with someone who can help.", query)}},
			{"route": map[string]any{"to": "support"}},
		}
	}
	log.Printf("  🔎 query=%q → answered", query)
	return CCD{
		{"say": map[string]any{"text": "Here's what I found. " + answer}},
		{"say": map[string]any{"text": "To ask another question press 1, to speak to an agent press 2, " +
			"or you can hang up. Thanks for calling."}},
		{"gather": map[string]any{"numDigits": 1, "timeout": 6, "actionUrl": base + "/support/after"}},
	}
}

func handleAfter(e Event) CCD {
	if e.Reason == "hangup" {
		return CCD{}
	}
	switch e.Digits {
	case "1":
		return CCD{{"redirect": map[string]any{"url": base + "/support"}}}
	case "2":
		return CCD{{"say": map[string]any{"text": "Connecting you to an agent."}}, {"route": map[string]any{"to": "support"}}}
	default:
		return CCD{{"say": map[string]any{"text": "Thanks for calling Acme support. Goodbye."}}, {"hangup": map[string]any{}}}
	}
}

var routes = map[string]func(Event) CCD{
	"/support":        handleStart,
	"/support/search": handleSearch,
	"/support/after":  handleAfter,
}

// ── HTTP plumbing (generic; identical across the three examples) ──────────────

func verify(body []byte, h http.Header) bool {
	if secret == "" {
		return true
	}
	ts := h.Get("X-Voicer-Timestamp")
	sent := strings.TrimPrefix(h.Get("X-Voicer-Signature"), "sha256=")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(sent))
}

func handle(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	if !verify(body, r.Header) {
		http.Error(w, `{"error":"bad signature"}`, http.StatusForbidden)
		return
	}
	fn, ok := routes[r.URL.Path]
	if !ok {
		http.Error(w, `{"error":"no such route"}`, http.StatusNotFound)
		return
	}
	var e Event
	_ = json.Unmarshal(body, &e)
	log.Printf("  → %s  event=%s speech=%q digits=%q", r.URL.Path, e.Event, e.Speech, e.Digits)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(fn(e))
}

func main() {
	for p := range routes {
		http.HandleFunc(p, handle)
	}
	fmt.Printf("Support-search sample on :%s  (control URL → %s/support)\n", port, base)
	if secret == "" {
		fmt.Println("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
	}
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

Download: Python (app.py) · Swift (main.swift) · Go (main.go)

Where to take these

These samples stay tiny and dependency-free on purpose. When you build the real thing, keep the verify() signature check and the verb-builder helpers, and grow outward:

State & data

The examples keep state in in-memory dictionaries. Swap those for your database, calendar, CRM or knowledge base — the PBX never holds your data.

A production server

The samples hand-roll a minimal HTTP server to stay dependency-free. In real life use your framework of choice — Flask / FastAPI, Vapor / Hummingbird, chi / echo — and keep the two helpers.

Outbound calls

To place a call and drive it with the same verbs, POST /api/voice/originate. You then receive call.answered instead of call.inbound.pre_route and reply with the same CCDs. See Messaging APIs.

Conversational AI

For a bot that listens and talks over the call (not a menu), the stream verb opens a Twilio-Media-Streams-compatible WebSocket — feed the audio to your speech/LLM stack in real time.

The full contract

These are the worked code; the authoritative request/response reference (every event, every verb, field by field) is the Programmable Voice Integration contract that ships with VoicerOnePBX. Start from a sample, reach for the contract when you need the exact shape of a field.