Signal CLI API Reference

Sanctum runs three separate Signal interfaces. They share a name and a purpose but almost nothing else. Confusing them is the most common source of 404 errors in the stack, because “signal-cli on port 8080” and “signal-cli on port 18081” are not the same service, the same protocol, or the same API. They are three strangers with the same surname who happen to live in the same building.
The Three Interfaces
Section titled “The Three Interfaces”| Interface | Port | Protocol | Container / Process | API Style |
|---|---|---|---|---|
| signal-cli native | 8080 | JSON-RPC | Homebrew binary (PID on Mac) | POST /api/v1/rpc |
| signal-cli-rest-api | 18081 | REST | Docker: signal-cli-rest-api | /v1/..., /v2/... |
| signal-yoda | 18082 | REST | Docker: signal-yoda | /v1/..., /v2/... |
1. signal-cli Native (Port 8080)
Section titled “1. signal-cli Native (Port 8080)”The Homebrew-installed signal-cli 0.14.2 binary running as a daemon with --http 127.0.0.1:8080. This is OpenClaw’s primary bridge — Yoda sends and receives messages through this interface.
Process: signal-cli -a +1XXXXXXXXXX daemon --http 127.0.0.1:8080 --no-receive-stdout
Endpoints
Section titled “Endpoints”| 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 belong to the Docker wrapper, not this interface.
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":["+1XXXXXXXXXX"],"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.2"},"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":["+1XXXXXXXXXX"]},"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.
2. signal-cli-rest-api (Port 18081)
Section titled “2. signal-cli-rest-api (Port 18081)”The bbernhard/signal-cli-rest-api Docker wrapper. Provides a proper REST API with versioned endpoints. Runs its own signal-cli instance internally with a separate data directory (data-rest-api/).
Container: signal-cli-rest-api on 127.0.0.1:18081
Key REST Endpoints
Section titled “Key REST Endpoints”| Method | Path | Description |
|---|---|---|
GET | /v1/about | Version info, capabilities, mode |
GET | /v1/accounts | List registered accounts |
POST | /v2/send | Send a message (v2 — supports quotes, mentions) |
GET | /v1/groups/{number} | List groups for an account |
GET | /v1/receive/{number} | Receive pending messages |
REST Example
Section titled “REST Example”# Check versioncurl -s http://127.0.0.1:18081/v1/about# Returns: {"versions":["v1","v2"],"build":2,"mode":"json-rpc","version":"0.98",...}
# Send a messagecurl -s -X POST -H "Content-Type: application/json" \ -d '{"message":"Hello","number":"+1XXXXXXXXXX","recipients":["+1XXXXXXXXXX"]}' \ http://127.0.0.1:18082/v2/send3. signal-yoda (Port 18082)
Section titled “3. signal-yoda (Port 18082)”Identical to the REST API above but with a separate data directory (data-yoda/). This is the Yoda-specific Signal identity used by the agent council. Same image, same API, different account.
Container: signal-yoda on 127.0.0.1:18082
Docker Compose
Section titled “Docker Compose”Both Docker containers are defined in ~/.openclaw/signal-cli/compose.yaml:
services: signal-cli-rest-api: image: bbernhard/signal-cli-rest-api:latest container_name: signal-cli-rest-api restart: unless-stopped environment: - MODE=json-rpc ports: - "127.0.0.1:18081:8080" - "127.0.0.1:6001:6001" volumes: - ./data-rest-api:/home/.local/share/signal-cli
signal-cli-yoda: image: bbernhard/signal-cli-rest-api:latest container_name: signal-yoda restart: unless-stopped environment: - MODE=json-rpc ports: - "127.0.0.1:18082:8080" - "127.0.0.1:6002:6001" volumes: - ./data-yoda:/home/.local/share/signal-cliOpenClaw Integration
Section titled “OpenClaw Integration”OpenClaw connects to signal-cli via the native CLI binary, not through any HTTP API. The relevant config in openclaw.json:
{ "channels": { "signal": { "accounts": { "yoda": { "name": "Yoda Signal", "enabled": true, "account": "+1XXXXXXXXXX", "cliPath": "/opt/homebrew/bin/signal-cli" } }, "enabled": true } }}This means OpenClaw messaging works even when the HTTP/REST interfaces are down. The CLI binary talks directly to the signal-cli daemon via its internal socket, not via HTTP. The HTTP API is for external tooling, health checks, and regression tests — not for the primary message flow.
Quick Diagnostic
Section titled “Quick Diagnostic”# Native JSON-RPC (should return version)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
# REST API container 1 (should return about info)curl -s http://127.0.0.1:18081/v1/about
# REST API container 2 / Yoda (should return about info)curl -s http://127.0.0.1:18082/v1/about
# Docker container status (both should be "running")docker inspect --format '{{.State.Status}}' signal-cli-rest-api signal-yodaHealth Monitoring
Section titled “Health Monitoring”The signal-health.sh script provides military-grade monitoring of all Signal interfaces with auto-recovery capabilities.
Location: ~/.sanctum/scripts/signal-health.sh
# Check-only mode (human-readable)~/.sanctum/scripts/signal-health.sh
# Auto-fix mode (restarts failed components)~/.sanctum/scripts/signal-health.sh --fix
# Machine-readable JSON output~/.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”| Component | Check | Auto-Fix Action |
|---|---|---|
Native daemon (:8080) | JSON-RPC version call | Restart via launchctl or direct launch |
signal-cli-rest-api (:18081) | /v1/accounts returns expected account | docker restart signal-cli-rest-api |
signal-yoda (:18082) | /v1/accounts returns Yoda number | docker restart signal-yoda |
OpenClaw bridge (:1977) | Gateway listening + node bridge connected to :8080 | Report only (manual) |
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 | All components healthy |
1 | One or more components were down but auto-recovered |
2 | Manual intervention required |
LaunchAgent (Periodic Monitoring)
Section titled “LaunchAgent (Periodic Monitoring)”A LaunchAgent runs the health check every 5 minutes 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.plistAll health check results are appended to ~/.sanctum/logs/signal-health.log. LaunchAgent stdout/stderr go to ~/.sanctum/logs/signal-health-stdout.log and signal-health-stderr.log.