Ansible role for hosting a docker rest server for using the Signal API https://github.com/bbernhard/signal-cli-rest-api
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-11 15:44:10 +00:00
.forgejo/workflows feat: initial version 2026-10-11 22:42:58 +07:00
defaults feat: initial version 2026-10-11 22:42:58 +07:00
meta feat: initial version 2026-10-11 22:42:58 +07:00
tasks feat: initial version 2026-10-11 22:42:58 +07:00
templates feat: initial version 2026-10-11 22:42:58 +07:00
.gitignore feat: initial version 2026-10-11 22:42:58 +07:00
CHANGELOG.md feat: initial version 2026-10-11 22:42:58 +07:00
LICENSE feat: initial version 2026-10-11 22:42:58 +07:00
README.md Prettified Code! 2026-10-11 15:44:10 +00:00

Signal API

Runs signal-cli's own HTTP daemon (official image ghcr.io/asamk/signal-cli) via Docker Compose and registers its Signal number. One Signal account for internal senders and receivers: monitoring, AI agents (e.g. Hermes Agent's Signal gateway).

The API has no authentication, so it is never published to the internet:

  • Containers on the same host join the Docker network signal_api_network and call http://<signal_api_instance>-app:8080.
  • Other hosts reach it through Traefik at signal_api_domain, limited to signal_api_allowed_ips. Use a name that only resolves internally or isn't routed from the outside, and a DNS-challenge certificate resolver.
  • The role itself uses 127.0.0.1:<signal_api_local_port> on the host.

API

Endpoint Use
/api/v1/rpc JSON-RPC 2.0, e.g. send, listAccounts. Methods: signal-cli-jsonrpc(5).
/api/v1/events Incoming messages as Server-Sent Events (?account=+49...).
/api/v1/check Health, 200.

The daemon runs with --receive-mode on-connection: messages are only fetched from Signal while a client listens on /api/v1/events, so nothing gets lost while that client is down. Only one client should listen.

Send a message:

curl -H "Content-Type: application/json" https://signal.int.example.com/api/v1/rpc -d '{
  "jsonrpc": "2.0", "id": 1, "method": "send",
  "params": {"account": "+4915112345678", "recipient": ["+4917612345678"], "message": "Hello"}
}'

Requirements

  • Docker
  • community.docker Ansible collection
  • signal_api_domain needs Traefik with the external traefik_web network

Variables

Variable Default Description
signal_api_instance signal-api Compose project name and container name prefix.
signal_api_data_path /data/signal-api signal-cli data (account keys). Back it up.
signal_api_docker_path /docker/signal-api Compose file.
signal_api_version 0.14.9 ghcr.io/asamk/signal-cli image tag. -native uses far less memory, needs an x86-64-v3 CPU.
signal_api_network signal_api Docker network for containers on the same host. Created by the role.
signal_api_local_port 8080 Port on 127.0.0.1 of the host, used by the role.
signal_api_domain ~ Serves the API through Traefik on this domain.
signal_api_allowed_ips [] Source IPs/CIDRs allowed through Traefik. Required with signal_api_domain.
signal_api_traefik_certresolver default Traefik certificate resolver.
signal_api_number ~ Signal number, +<country><number>.
signal_api_use_voice false Request the code by voice call (landlines).
signal_api_captcha ~ One-time, see below.
signal_api_verify_code ~ One-time, see below.
signal_api_profile_name ~ Display name in Signal. Required for the profile settings below.
signal_api_profile_avatar ~ Profile picture, path to a JPEG/PNG on the Ansible controller.
signal_api_profile_about ~ "About" text of the profile.
signal_api_uninstall false Stops and removes containers and the compose directory.
signal_api_purge false Also removes the data directory. Requires signal_api_uninstall: true. Irreversible.

Registering the number

Signal requires a captcha and a code sent to the number, so registration takes two runs. Both values are only valid once and don't belong in the inventory:

  1. Open https://signalcaptchas.org/registration/generate.html, solve the captcha. The browser then tries to open a signalcaptcha://... link; copy the value after signalcaptcha:// (e.g. from the developer console).
  2. ansible-playbook <playbook> -e signal_api_captcha=<value>: Signal sends a code by SMS (or calls, with signal_api_use_voice: true).
  3. ansible-playbook <playbook> -e signal_api_verify_code=<code>: the number is registered.

Later runs see the number in listAccounts and skip all of this. The profile (name, picture, about) is applied after the registration and again whenever it changes; a checksum of the last applied one is kept in signal_api_docker_path.

The keys live in signal_api_data_path. Don't use the number in the Signal app on a phone at the same time, registering it there logs this one out.

Uptime Kuma

Kuma's "Signal" notification type speaks the bbernhard REST API, not this one. Use a "Webhook" notification instead: URL http://<signal_api_instance>-app:8080/api/v1/rpc, request body "Custom Body":

{"jsonrpc": "2.0", "id": 1, "method": "send", "params": {"account": "+4915112345678", "recipient": ["+4917612345678"], "message": {{ msg | json }}}}