Skip to content

Set up phone calls

With a Session Initiation Protocol (SIP) trunk or a session border controller (SBC) account, the device answers real phone calls. The brain’s SIP front-end answers the call, taking it from a separate gateway service, teaport-sip.

You need:

  • A device with teaport installed. See Install teaport.
  • A SIP trunk or SBC account, with its host, domain, username, and password.
  • The gateway program. The installer creates the teaport-sip service, but it does not build or install the gateway: that is a separate program in its own repository, and the service runs ~/teaport-sip/build/teaport-sip.
  • Access to the device as the user the teaport services run as.

Telephony ships off. The installer lays down the teaport-sip service, gated on a configuration file that does not exist, so nothing starts and no SIP port is bound. The teaport sip configure wizard turns it on.

To configure the line, follow these steps:

  1. On the device, start the wizard:

    Terminal window
    teaport sip configure
  2. Answer its prompts: the registrar or SBC host, the domain, the username, and the password.

  3. Wait for the test registration. The wizard registers from a throwaway port, never port 5060, so it leaves a running gateway untouched. When the trunk answers 200 OK, the wizard writes ~/.config/teaport/teaport-sip.conf with mode 600, which holds the SIP password, and enables the service. If the registration fails, the wizard writes nothing and telephony stays off.

Run the wizard as the user the services run as. As root, add sudo -E: a plain sudo resets HOME, so the wizard writes a configuration the gateway never reads, and it refuses to.

To pass the answers on the command line, for example from a script:

Terminal window
teaport sip configure --host SBC_HOST --domain SIP_DOMAIN --user SIP_USER \
--password SIP_PASSWORD --yes

Replace the following:

  • SBC_HOST: the registrar or SBC host.
  • SIP_DOMAIN: the SIP domain of the trunk.
  • SIP_USER: the username the trunk assigns you.
  • SIP_PASSWORD: the trunk password.

To adopt a gateway .conf that a device ran by hand, or one carried over from another device:

Terminal window
teaport sip configure --conf CONF_PATH

Replace CONF_PATH with the file to adopt. The wizard test-registers through the same path, then installs the file as it is, except for four keys the services own: sip_port becomes 5060, uds_path becomes the socket the brain listens on, register becomes true, and auto_answer becomes false, because the brain answers the call, after a couple of rings. If a gateway or SIP front-end launched by hand is still running, the command refuses and names it: the service it is about to enable needs port 5060 and the socket.

A re-run, for a new password or a new registrar, rewrites only the keys the wizard owns: the trunk’s id_uri, registrar_uri, username, password, and realm, and the four service keys. Every other line of the installed configuration survives: the source filter, sip_source_filter with its sip_source_allow list, nameserver, aec, comments, and keys the wizard does not know. Before the test registration, the wizard lists by name the settings it keeps. To keep nothing and write a fresh template, run teaport sip configure --fresh.

teaport sip status reports the gateway service, the brain’s SIP front-end, the configuration, including whether the running gateway confirms the source filter in it, and the registration from this run:

Terminal window
teaport sip status

To follow the gateway’s journal:

Terminal window
teaport logs sip -f

The call’s own lines are in the brain’s journal, teaport logs brain.

The line survives reboots and crashes on its own. The service is enabled and restarts with a backoff that does not hammer the registrar, and the brain reconnects to the gateway whenever the gateway comes back. The two restart independently: a brain restart, from an update or a configuration change, leaves the call at the gateway, which hands it to the restarted brain, and the caller hears a short “sorry, I lost you for a moment”. The brain picks up a restarted gateway within a couple of seconds.

To restart the gateway, for example after editing its configuration by hand:

Terminal window
teaport sip restart

The command waits for the trunk’s 200 OK; the brain reconnects on its own.

The gateway cancels echo for the caller. teaport sip aec prints the current acoustic echo cancellation (AEC) setting:

Terminal window
teaport sip aec

The flags teaport sip aec off and teaport sip aec on flip aec= in the configuration and restart the gateway:

Terminal window
teaport sip aec off
teaport sip aec on

To turn telephony off:

Terminal window
teaport sip disable

The command stops and disables the gateway. The configuration stays at ~/.config/teaport/teaport-sip.conf; add --purge to remove it too. Run the wizard again to turn telephony back on.

The configuration is the gateway’s own key=value format: registrar_uri, id_uri, username, password, uds_path, aec, auto_answer, and more. Editing it by hand is fine; run teaport sip restart afterwards. Until you do, teaport sip status and teaport doctor don’t show the source filter as confirmed, because the gateway still runs the configuration it started with. Nothing about the line lives in /tmp: the configuration is in ~/.config/teaport, the socket in /run/teaport, and the logs in journald.

The brain runs every front-end in one process, and the engine serves one conversation at a time; the session arbiter decides which one. A phone call and a Talk session are peers there. See Architecture.

The brain answers the call, not the gateway. The delay is a setting, SIP_ANSWER_AFTER_SECS, a couple of rings, and the brain builds the call’s pipeline and words its greeting during the ring, so the greeting plays as soon as the caller is connected. See the settings reference.

While the call rings, the agent asks the live conversation, in the conversation’s language: “Someone’s calling me — Ann Smith. Should I step away for a moment?” The agent uses the caller’s name when the call carries a real one. A placeholder such as “WIRELESS CALLER” does not count. Otherwise the agent says the last four digits of the number: “Someone’s calling me — a number ending in 8 5 0 0…”. The device’s screen shows the whole number. When the call carries no name or number, the agent asks “Someone’s calling me. Should I step away…”.

The question has three outcomes:

  • Yes: the agent says “I’ll take the call — back in a moment.” The conversation goes on hold: its connection and everything said in it are kept, and it hears nothing while the call is up. After the call, the conversation resumes with “Sorry about that — where were we?”
  • No: the call is never answered. The caller hears it ring until they give up.
  • No answer: TEAPORT_CALL_PROMPT_SECS, 8 seconds by default, after the question starts, and at least 3 seconds after it ends, the call is taken by default (TEAPORT_CALL_PROMPT_DEFAULT); it can instead keep ringing. If someone is still talking at that point, the agent waits up to 5 seconds more. The caller hears it ring the whole time, and many carriers send an unanswered call to voicemail after 20 to 30 seconds. With nobody answering, the call is picked up about 12 to 14 seconds after it rings.

An answer counts only after the question has been heard: played to the end, or cut after the user heard at least half of “Should I step away for a moment?”. While the question is still being said, the agent takes no answer. If talk in the room cuts the question before its last part, the agent takes no answer yet. When the user stops speaking, the agent asks the question again, one time only. After that second time, the agent takes the user’s answer even if the question was cut again. Without an answer, the no-answer default applies. The waiting time still counts from the ring, so with the question asked twice and nobody answering, the call is picked up about 16 to 18 seconds after it rings.

With TEAPORT_TOOL_ANSWER_PHONE_CALL=0, nobody is asked. A Talk session hears “Sorry, a phone call is coming in and I have to take it…” and closes; a conversation at the device keeps it, and the caller hears the busy line.

A call that is already connected is not asked about, because the caller waits on a silent line: a configuration with auto_answer=true, or a call that was up when the brain restarted. The brain takes the call and holds the conversation.

While a call is live, the brain refuses a Talk client with “Sorry, I’m on a phone call right now…”, and the gateway turns away a second call with 486 Busy Here; the caller’s phone handles that, as voicemail or a busy tone.

To dedicate a device to the phone, turn off the other front-ends, not the brain, which is what answers calls: the local audio bridge, the Discord bridge, and Talk in OpenClaw.

A question that goes to the agent can take 15 to 35 seconds to answer, and sometimes minutes. Talk sessions and conversations at the device play a typing sound through the wait. A phone call plays none unless you turn the typing on for calls with SIP_THINKING_SOUND. To change the setting and apply it, see Configure teaport.

Three other settings decide the typing on calls as well:

  • TEAPORT_THINKING_SOUND=0 turns the typing off everywhere, calls included.
  • With SIP_HALF_DUPLEX on, SIP_THINKING_SOUND has no effect. That setting drops the caller’s audio while the typing plays, so a caller who keeps talking through the wait couldn’t stop it.
  • In agent-first mode, TEAPORT_AGENT_FIRST, calls play the typing whatever SIP_THINKING_SOUND and SIP_HALF_DUPLEX say.

On a call, the typing plays only after the line has been quiet for TEAPORT_THINKING_GRACE_S, 1.5 seconds by default, and it stops when the caller speaks. The exception is agent-first mode with SIP_HALF_DUPLEX on: there the brain doesn’t hear the caller while the typing plays, the same as while the agent speaks. An OLED face on the device doesn’t type along to a call.

Before you turn the typing on for calls, listen to it on a call. The typing is made at 24 kHz, and a phone line carries 8 kHz audio, which keeps little of the typing’s click.

Phone calls are not recorded unless you turn recording on. One setting, TEAPORT_CALL_RECORDING, decides what a call leaves behind:

  • off, the default: nothing of the call is kept.
  • audio: both sides of the call in one stereo WAV file, the caller on the left channel and the agent on the right.
  • transcript: a text file of the caller’s and the agent’s lines, timed from the start of the call. A reply the caller cut off is written as far as they heard it.
  • both: the two files.

Talk sessions and the room mic are never recorded by this setting.

Recording-consent law differs by place. Most states in the United States need the consent of one party to the call, California and about a dozen others need everyone’s, and other countries have their own rules. You choose the setting and are responsible for it; this page is not legal advice.

Each call’s files are named by the time the call started and its call id, for example 20261102-141503-abc123.wav, in TEAPORT_CALL_RECORDING_DIR (~/teaport-recordings by default). Recording starts when the caller is told, before the greeting; a call the brain can’t hear isn’t recorded. If the brain restarts during a call, the rest of the call goes to files beside the first ones (.r1.wav, .r1.txt), and the caller hears the notice again.

With recording on, a call starts with “This call is being recorded.” in the call’s language, before the greeting. The agent doesn’t listen while the notice plays, so the caller can’t talk over it. To turn the notice off, set TEAPORT_CALL_RECORDING_NOTICE=0. Either way, the agent knows the call is recorded and says so if the caller asks.

Recordings older than TEAPORT_CALL_RECORDING_KEEP_DAYS (30 by default) are deleted, and then the oldest until the directory fits in TEAPORT_CALL_RECORDING_MAX_MB (2048 by default). The check runs as each call starts. See the settings reference.

brain/tests/fake_sip_gateway.py stands in for the gateway on the brain’s side of the socket. It binds the socket, rings and answers a call from a caller ID you choose, plays a WAV as the caller in real time, records what the caller hears, and hangs up when the bot has answered, partway through the answer, or after a number of seconds. A brain that drops mid-call is handed the call again when it reconnects, as the real gateway does. Nothing registers and no SIP port is bound, so it works on a device whose trunk account is registered somewhere else. It needs only the system python3, run from a checkout that contains it.

The brain needs nothing: its SIP front-end looks for the socket every 2 seconds, TEAPORT_SIP_SOCKET, /run/teaport/teaport-sip.sock by default, and takes calls from whatever serves it.

To test the phone path, follow these steps:

  1. From a checkout that contains tools/sip-line-guard and brain/tests/fake_sip_gateway.py, guard the real gateway for the session; it must not start and register while the fake is up:

    Terminal window
    sudo tools/sip-line-guard on

    The guard is a runtime drop-in whose condition cannot hold, so the service skips every start of it, and the drop-in is gone at the next boot. The command ends with GUARDED: teaport-sip.service cannot start.

  2. Prove that the guard holds, without starting anything. The check exits 0 when the drop-in is loaded, the guard condition fails, and the gateway is not running:

    Terminal window
    tools/sip-line-guard status
  3. Create the socket directory and a directory for the recordings. The socket directory is the gateway’s runtime directory, absent while the service is off:

    Terminal window
    sudo install -d -o teaspoon -g teaspoon -m 0750 /run/teaport
    install -d -m 0700 ~/fake-sip
  4. Start the fake gateway. It waits up to 120 seconds for the brain, which connects within about 2 seconds:

    Terminal window
    python3 brain/tests/fake_sip_gateway.py --socket /run/teaport/teaport-sip.sock \
    --allow-live-path --from +15551234567 --wav brain/test/question.wav \
    --record ~/fake-sip/bot.wav --record-stereo ~/fake-sip/both.wav \
    --summary-json ~/fake-sip/summary.json

    The call rings in from +15551234567; replace it with any caller ID. The caller’s audio, question.wav, asks: “What is the capital of France, and is it a large city?”

  5. Follow the call from the brain’s side, in a second shell:

    Terminal window
    journalctl -u teaport-brain -f

    The front-end logs SIP front-end: connected to the gateway at ... when it connects.

  6. End the fake; it removes the socket when it exits. Then clean up:

    Terminal window
    sudo rmdir /run/teaport
    sudo tools/sip-line-guard off

The recordings and the result land in ~/fake-sip:

  • bot.wav: what the caller heard.
  • both.wav: the caller on the left channel and the bot on the right.
  • summary.json: whether the call was answered, when the greeting and the answer started and ended, the answer’s latency measured from the end of the WAV, who hung up, and the control messages the brain sent.

The fake exits 1 when the call was not answered, there was no greeting, there was no answer to the WAV, or the protocol was violated: an audio frame that is not exactly 640 bytes, a datagram over the gateway’s 2048-byte read, or unparseable control.

If brain.env sets TEAPORT_SIP_SOCKET to another path, serve that path instead; it must not be empty, because empty turns the SIP front-end off. The fake refuses to bind a path that something already listens on, compares real paths, so a symlink such as /var/run does not pass, and without --allow-live-path it refuses the default path as well.

Any 16-bit WAV works as the caller’s audio; other sample rates and stereo are converted to the 16 kHz mono the gateway speaks. For a call that exercises a tool, question_host_status.wav asks for the host status.

These pages cover the rest: