GEMINIGemini Live — default
Default backend. Reference model gemini-3.1-flash-live-preview.
Get a key from Google AI Studio → aistudio.google.com/apikey
(or a Google Cloud service account). Set it as STROWGER_GEMINI_API_KEY.
Install the binary, edit one config file, activate a free demo licence, and place a call — all inside one morning.
On Debian/Ubuntu with systemd, the one-liner detects your architecture, downloads and checksum-verifies the signed binary, sets up the systemd unit, and leaves a config skeleton to complete:
curl -fsSL https://lic.strowger.io/install.sh | sudo bash
To pin a version or target, download the script first:
curl -fsSL https://lic.strowger.io/install.sh -o get-strowger.sh
sudo bash get-strowger.sh --version 1.0.0-rc1 --target x86_64-unknown-linux-gnu
The installer verifies the SHA-256 checksum of the tarball before installing anything
(and the minisign signature too, if minisign is present and a public key is
configured) — it refuses to install on a mismatch. It then delegates to
deploy/install.sh, which is idempotent and safe to re-run to upgrade the
binary in place.
Building and installing from source works the same way, once you have a release binary:
cargo build --release
sudo ./deploy/install.sh target/release/strowgerd
deploy/install.sh creates a system user/group strowger,
installs the binary at /opt/strowger/bin/strowgerd (and
strowger-cli alongside it, symlinked into /usr/local/bin if
present next to the daemon binary), writes a config skeleton to
/etc/strowger/config.toml from config.example.toml (without
overwriting an existing one), creates /etc/strowger/strowger.env, installs
the systemd unit, and reserves the configured RTP port range in
net.ipv4.ip_local_reserved_ports so ephemeral client sockets on the host
never steal a port strowger needs. Re-run it whenever rtp_port_range
changes.
Everything lives in one TOML file, /etc/strowger/config.toml, whose path
is read from the STROWGER_CONFIG environment variable. Secrets are never
written in the clear here — they are interpolated from the environment with
${VAR_NAME} syntax so the file itself stays safe to version or hand to a
colleague.
The core process settings: where the HTTP API binds, the public IP the SIP stack identifies itself with, and where state lives on disk.
[daemon]
http_host = "127.0.0.1"
http_port = 7060
public_ip = "203.0.113.10" # REQUIRED — SIP identity; no STUN in v1
data_dir = "/var/lib/strowger" # strowger.db (SQLite) + recordings/ + cdr/
rtp_port_range = "20002-40001" # default RTP media port range
public_ip is required — the daemon needs it to build correct
SIP/SDP for your trunk. http_port defaults to 7060. Under the
shipped systemd unit, ProtectSystem=strict makes everything except
data_dir read-only, so data_dir must stay
/var/lib/strowger in production; only change it for a local dev run outside
systemd.
The real-time speech-to-speech backend. Gemini Live is the default; OpenAI Realtime is a second backend with the same shape. Each call can override the model, and the API key can also be supplied per call (falling back to the config key if omitted).
[ai.gemini]
api_key = "${STROWGER_GEMINI_API_KEY}"
model = "gemini-3.1-flash-live-preview"
playout_ms = 200 # emission playout buffer, in ms
# [ai.openai] # mirrors [ai.gemini]
# api_key = "${STROWGER_OPENAI_API_KEY}"
Without an API key configured, the AI backend is disabled and calls run in echo mode instead — useful for bench testing the SIP/RTP path in isolation.
One block per SIP trunk. Two authentication modes: REGISTER (credentials,
register = true) or IP-auth (register = false — no
REGISTER, the carrier authenticates by source IP and the OPTIONS keepalive is the
liveness signal).
[[trunk]]
name = "ovh-prod"
registrar = "sip-domain.io"
outbound_proxy = "oa285772-ovh-1.sip-proxy.io"
username = "0033184163651"
auth_username = "0033184163651"
password = "${STROWGER_OVH_PASSWORD}"
number = "+33184163651"
transport = "udp"
register = true # false = IP-auth trunk, no REGISTER
Transverse inbound settings, then one route per DID. A route's handler is either a
webhook (the consumer authorises and configures every call live) or
static (objectives and first message fixed in the TOML).
[inbound]
answer_delay_ms = 3000 # 180 Ringing -> 200 OK delay at answer
initial_silence_ms = 1000 # silence before the agent's first word
webhook_timeout_secs = 5 # webhook GET timeout; on expiry -> SIP 486
[[inbound_route]]
trunk = "ovh-prod"
did = "+33184163651" # exact +E164 match
[inbound_route.handler]
type = "webhook"
url = "http://127.0.0.1:9100/api/voice/inbound?token=secret"
[[inbound_route]]
trunk = "ovh-prod"
did = "*184163652" # suffix match (>= 6 digits)
[inbound_route.handler]
type = "static"
objectives = "You are the company's receptionist…"
first_message = "Hello, how can I help you?"
language = "en"
The did field of a route accepts three forms, matched after normalising
to digits only (+, spaces, dots, dashes and URI params stripped):
| Pattern | Meaning |
|---|---|
+33184163651 | exact match — does not match a
0033…/0… presentation of the same number |
*184163651 | suffix match (≥ 6 digits); 9 digits = a French national number, invariant across presentation formats — the sensible default |
* | catch-all for the trunk |
Resolution is deterministic: exact > longest suffix > catch-all. Two patterns that normalise to the same value are rejected at load time.
Further optional knobs exist for advanced tuning — media watchdogs
and RTP latch policy under [media], VAD/barge-in/silence handling under
[ai.gemini]/[ai.openai], and daemon-level call/shutdown
deadlines — see the commented reference in config.example.toml in the
repository for the full list and their defaults.
Strowger supports autonomous agent call transfer (call deflection / human escalation) powered by the built-in transfer_call tool in Gemini Live and OpenAI Realtime.
Configure transfer policy and security whitelists in config.toml:
# /etc/strowger/config.toml
[transfer]
enabled = true # Enable transfer_call tool declaration (default: false)
mode = "refer" # "refer" (carrier delegation) or "hairpin" (bridged 2nd leg)
max_per_call = 3 # Anti-loop protection: max transfers allowed per call
allow = [ # Whitelist: exact E.164 (+331...), prefix (+33184*), or SIP URI
"+33144556677",
"+33184*",
"sip:*@company.com",
]
deny = [ # Blacklist overrides allow
"+33184163999",
]
drain_timeout_ms = 4000 # Playout drain before issuing transfer
refer_timeout_ms = 15000 # Timeout waiting for carrier REFER/NOTIFY progression
hairpin_timeout_ms = 30000 # Timeout waiting for 2nd leg answer in hairpin mode
Transfer Modes:
refer (Default): Performs an in-dialog SIP REFER (RFC 3515) to delegate call routing to the upstream carrier/PBX, releasing audio and channels upon transfer.hairpin: Originates a second outbound leg directly to the target destination, cross-connects bidirectional 8 kHz PCM audio between caller and target (A↔C), cleanly releases the AI voice session, and supervises both legs with cascading BYE.The voice intelligence is a real-time speech-to-speech model, reached over a WebSocket. You bring your own provider key — audio goes to exactly one third party, the model provider you choose, under your own contract and DPA, billed at cost with no markup from us. The model is chosen per backend and can be overridden per call.
Default backend. Reference model gemini-3.1-flash-live-preview.
Get a key from Google AI Studio → aistudio.google.com/apikey
(or a Google Cloud service account). Set it as STROWGER_GEMINI_API_KEY.
Same contract as a second backend. Get a key from the OpenAI platform →
platform.openai.com/api-keys.
Set it as STROWGER_OPENAI_API_KEY.
Put the keys in the secrets file (never in config.toml or the repo); the
config references them by ${VAR}:
# /etc/strowger/strowger.env (0640 root:strowger)
STROWGER_GEMINI_API_KEY=AIza...your-key...
# STROWGER_OPENAI_API_KEY=sk-...your-key...
Pick the backend per call with the backend field of the inbound
webhook / POST /calls body (default: the configured backend). A call may also
override model and carry its own key, falling back to the config key. With no
key configured at all, calls run in echo mode (SIP/RTP path only, no AI).
Real secret values never go in config.toml — and never in the repo.
They live in /etc/strowger/strowger.env, created mode 0640
root:strowger by the installer, and are interpolated into the config file via
${VAR} at startup:
# /etc/strowger/strowger.env — 0640 root:strowger, never versioned
STROWGER_OVH_PASSWORD=...
STROWGER_GEMINI_API_KEY=...
STROWGER_OPENAI_API_KEY=...
The systemd unit loads this file as its EnvironmentFile, so the
variables are only ever visible to the strowger service user, never in the
unit file, never logged, and scrubbed from any HTTP payload that carries them.
sudo systemctl enable --now strowger
journalctl -u strowger -f
Shutdown (systemctl stop, SIGTERM) is graceful: the HTTP API closes
first, then the call worker, then the daemon cleanly un-REGISTERs its trunks, all within
TimeoutStopSec=30. If you update the binary and re-run
install.sh, note that daemon-reload does not restart the
running process — the new binary only takes effect after a
systemctl restart strowger.
The shipped binary enforces its licence — this is compiled into the binary, not a config toggle. Without an activated licence, new calls are refused. Activate it once, after the daemon is running:
# Free demo — light identification, keyless
strowger-cli register --company "Acme SARL" --contact "Jane Doe" \
--email [email protected] --phone +33612345678
# Commercial licence issued in advance
strowger-cli register --key <activation-key>
The demo is capped at 1 trunk, 1 channel, calls limited to
60 seconds, with a 30-day sliding validity that renews itself as long as
the daemon keeps talking to the licence server. Licensing is per concurrent
channel: capacities such as max_concurrent_calls,
max_trunks and max_call_seconds are values read straight from
the verified licence, not booleans you can flip locally.
On expiry or revocation, only new calls are refused — a call already in
progress always finishes normally, and /health reports
degraded with a reason rather than the daemon stopping outright. To lift the
demo caps, contact [email protected]: the same license_id is
upgraded server-side, and the daemon picks up the new capacities on its next contact
with the licence server — no reinstall, no new key to enter.
Everything strowger writes to disk lives under data_dir
(/var/lib/strowger under systemd): the SQLite state file
(strowger.db), a recordings/ directory (opt-in per call), and
a cdr/ directory holding the daily append-only journal.
Per-call CDRs are also pushed straight to your callback_url at the end
of every call — this is the recommended way to consume them; see the
API reference for the full callback payload and the CDR field
list.
With a trunk registered and a licence activated, you're ready to place or receive a
call. The full POST /calls contract, the inbound authorisation webhook, and
the end-of-call callback are documented in the API reference.