Signal CLI API Reference

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.
The One Interface
Section titled “The One Interface”| Interface | Port | Protocol | Process | API Style |
|---|---|---|---|---|
| signal-cli native | 8080 | JSON-RPC | Homebrew binary, com.sanctum.signal-cli | POST /api/v1/rpc |
| signal-cli native | 7583 | JSON-RPC over raw TCP | same process, --tcp | line-delimited JSON-RPC |
signal-cli Native (Ports 8080 + 7583)
Section titled “signal-cli Native (Ports 8080 + 7583)”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-stdoutTwo 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.
HTTP Endpoints (Port 8080)
Section titled “HTTP Endpoints (Port 8080)”| Method | Path | Description |
|---|---|---|
POST | /api/v1/rpc | JSON-RPC 2.0 endpoint (the only real endpoint) |
GET | /api/v1/events | Server-Sent Events stream for incoming messages |
GET | /api/v1/check | Health 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.
JSON-RPC Examples
Section titled “JSON-RPC Examples”Send a message:
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/rpcGet version:
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:
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/rpcCheck if a number is registered:
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/rpcAvailable JSON-RPC Methods
Section titled “Available JSON-RPC Methods”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 Integration
Section titled “OpenClaw Integration”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.
Quick Diagnostic
Section titled “Quick Diagnostic”# 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 loadedlaunchctl 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 sightnc -z 127.0.0.1 18081 && echo "WARNING: dead port is active" || echo "dead port quiet (good)"Health Monitoring
Section titled “Health Monitoring”The signal-health.sh script monitors the daemon and its consumer pipeline, with auto-recovery on --fix.
Location: ~/.sanctum/scripts/signal-health.sh
# 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 --quietWhat It Checks
Section titled “What It Checks”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.
| Check | What it verifies | Auto-Fix Action |
|---|---|---|
signal_port | JSON-RPC version on :8080 responds | launchctl kickstart -k the daemon, else direct relaunch |
single_daemon | Exactly one signal-cli Java process, and nothing squatting dead port 18081 | Kill the dead-port squatter; kill all + restart on duplicates |
gateway_plugin | force-flow :4077 up AND a consumer holds an ESTABLISHED conn to :8080 | Kickstart com.sanctum.force-flow |
forceflow_port | force_flow.py still points at :8080 (config-drift detector) | sed-patch the port and restart force-flow |
websocket_health | No Signal WebSocket connection-storm in the last 15 min | Report only |
outbound_send | A real outbound send succeeded recently | Report only |
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 | All components healthy |
1 | One or more were down but self-healed (recovered) |
2 | Manual intervention required |
LaunchAgent (Periodic Monitoring)
Section titled “LaunchAgent (Periodic Monitoring)”A LaunchAgent runs the check every 5 minutes (StartInterval 300) with --fix --quiet.
Plist: ~/Library/LaunchAgents/com.sanctum.signal-health.plist
# Check statuslaunchctl list | grep signal-health
# Reload after editslaunchctl unload ~/Library/LaunchAgents/com.sanctum.signal-health.plistlaunchctl load ~/Library/LaunchAgents/com.sanctum.signal-health.plistHealth 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.
Retired interfaces (2026-04-26)
Section titled “Retired interfaces (2026-04-26)”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 interface | Old port | What replaced it |
|---|---|---|
signal-cli-rest-api (bbernhard) | 18081 | native daemon :8080 (JSON-RPC) |
signal-yoda (bbernhard) | 18082 | native 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.