Skip to content

Signal CLI API Reference

Signal API — a secure communication device projecting a holographic signal wave.

Sanctum once ran three separate Signal interfaces. It now runs one: the native signal-cli daemon speaking JSON-RPC on port 8080. The two Docker REST wrappers — signal-cli-rest-api on 18081 and signal-yoda on 18082 — were torn down on 2026-04-26 when the native daemon’s TCP socket made them redundant. If you came here looking for /v1/about on a Docker container, that’s the ghost; see Retired interfaces.

The one rule that survives all of that: port 8080 speaks JSON-RPC, not REST.

InterfacePortProtocolProcessAPI Style
signal-cli native8080JSON-RPCHomebrew binary, com.sanctum.signal-cliPOST /api/v1/rpc
signal-cli native7583JSON-RPC over raw TCPsame process, --tcpline-delimited JSON-RPC

The Homebrew-installed signal-cli 0.14.4.1 binary running as a launchd daemon. This is OpenClaw’s only Signal bridge — Yoda sends and receives through it.

Process (from the live cmdline):

signal-cli -a +15555550100 daemon \
--http localhost:8080 \
--tcp localhost:7583 \
--no-receive-stdout

Two listeners, one daemon: HTTP JSON-RPC on 8080 for local tooling, and raw-TCP JSON-RPC on 7583 for the VM. A socat bridge (com.sanctum.signal-tcp-bridge) republishes 7583 on the Mac↔VM link (10.0.0.1:7583) so the VM’s yoda-chat-consumer can read the same daemon without a second account. One identity, two transports, zero containers.

MethodPathDescription
POST/api/v1/rpcJSON-RPC 2.0 endpoint (the only real endpoint)
GET/api/v1/eventsServer-Sent Events stream for incoming messages
GET/api/v1/checkHealth check

Everything else returns 404. There is no /v1/about. There is no /api/v1/accounts. Those paths belonged to the Docker wrapper, and the Docker wrapper retired.

Send a message:

Terminal window
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"send","params":{"recipient":["+15555550100"],"message":"Hello from Sanctum"},"id":1}' \
http://127.0.0.1:8080/api/v1/rpc

Get version:

Terminal window
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"version","id":2}' \
http://127.0.0.1:8080/api/v1/rpc
# Returns: {"jsonrpc":"2.0","result":{"version":"0.14.4.1"},"id":2}

List groups:

Terminal window
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"listGroups","id":3}' \
http://127.0.0.1:8080/api/v1/rpc

Check if a number is registered:

Terminal window
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"getUserStatus","params":{"recipient":["+15555550100"]},"id":4}' \
http://127.0.0.1:8080/api/v1/rpc

Key methods (camelCase, passed as "method" in the JSON-RPC request): send, sendReceipt, listGroups, updateGroup, getUserStatus, version, sendSyncRequest, listContacts, block, unblock, startLink, finishLink, subscribeReceive, unsubscribeReceive.

Full method list: signal-cli-jsonrpc(5) man page.

OpenClaw reaches signal-cli over HTTP JSON-RPC on port 8080 — not via the CLI binary, not via a socket the binary opens for it. The relevant config in openclaw.json:

{
"channels": {
"signal": {
"accounts": {
"yoda": {
"name": "Yoda Signal",
"enabled": true,
"account": "+15555550100",
"httpUrl": "http://127.0.0.1:8080"
}
},
"enabled": true
}
}
}

The httpUrl is load-bearing: if the daemon on 8080 is down, OpenClaw messaging is down. That is the trade for having exactly one transport instead of three — when the bridge breaks, you know precisely which one. The native daemon’s --http HTTP server is the message flow, so health monitoring leans on it hard.

Terminal window
# Native JSON-RPC (should return version 0.14.4.1)
curl -s -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"version","id":1}' \
http://127.0.0.1:8080/api/v1/rpc
# Daemon is the launchd job — confirm it's loaded
launchctl list | grep com.sanctum.signal-cli
# TCP bridge to the VM (raw JSON-RPC, line-delimited)
nc -z 10.0.0.1 7583 && echo "VM bridge up"
# 18081 should be SILENT — it's the dead port the health check kills on sight
nc -z 127.0.0.1 18081 && echo "WARNING: dead port is active" || echo "dead port quiet (good)"

The signal-health.sh script monitors the daemon and its consumer pipeline, with auto-recovery on --fix.

Location: ~/.sanctum/scripts/signal-health.sh

Terminal window
# Check-only mode (human-readable summary)
~/.sanctum/scripts/signal-health.sh
# Auto-fix mode (restarts failed components)
~/.sanctum/scripts/signal-health.sh --fix
# Machine-readable JSON output (for service-graph.py)
~/.sanctum/scripts/signal-health.sh --json
# Silent mode (log only, no stdout)
~/.sanctum/scripts/signal-health.sh --fix --quiet

Six checks, run in order — the port check first, because the rest depend on it. No Docker references anywhere; the containers are gone and the script knows it.

CheckWhat it verifiesAuto-Fix Action
signal_portJSON-RPC version on :8080 respondslaunchctl kickstart -k the daemon, else direct relaunch
single_daemonExactly one signal-cli Java process, and nothing squatting dead port 18081Kill the dead-port squatter; kill all + restart on duplicates
gateway_pluginforce-flow :4077 up AND a consumer holds an ESTABLISHED conn to :8080Kickstart com.sanctum.force-flow
forceflow_portforce_flow.py still points at :8080 (config-drift detector)sed-patch the port and restart force-flow
websocket_healthNo Signal WebSocket connection-storm in the last 15 minReport only
outbound_sendA real outbound send succeeded recentlyReport only
CodeMeaning
0All components healthy
1One or more were down but self-healed (recovered)
2Manual intervention required

A LaunchAgent runs the check every 5 minutes (StartInterval 300) with --fix --quiet.

Plist: ~/Library/LaunchAgents/com.sanctum.signal-health.plist

Terminal window
# Check status
launchctl list | grep signal-health
# Reload after edits
launchctl unload ~/Library/LaunchAgents/com.sanctum.signal-health.plist
launchctl load ~/Library/LaunchAgents/com.sanctum.signal-health.plist

Health check results append to ~/.sanctum/logs/signal-health.log. LaunchAgent stdout/stderr go to ~/.sanctum/logs/signal-health-stdout.log and signal-health-stderr.log.

Two Docker REST wrappers used to run alongside the native daemon. They are gone — docker ps -a lists no signal container in any state, and :18081 / :18082 answer nothing.

Retired interfaceOld portWhat replaced it
signal-cli-rest-api (bbernhard)18081native daemon :8080 (JSON-RPC)
signal-yoda (bbernhard)18082native daemon — same Yoda account, no second container

The active ~/.openclaw/signal-cli/compose.yaml is now services: {} behind a retirement header. The original two-service definition is preserved at ~/.openclaw/signal-cli/compose.yaml.bak-pre-retire-yoda if the REST containers ever need to come back. The data directories data-rest-api/ and data-yoda/ survive as stale leftovers; neither is mounted by anything live.