- Jinja 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| defaults | ||
| meta | ||
| tasks | ||
| templates | ||
| .gitignore | ||
| CHANGELOG.md | ||
| LICENSE | ||
| README.md | ||
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_networkand callhttp://<signal_api_instance>-app:8080. - Other hosts reach it through Traefik at
signal_api_domain, limited tosignal_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.dockerAnsible collectionsignal_api_domainneeds Traefik with the externaltraefik_webnetwork
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:
- Open https://signalcaptchas.org/registration/generate.html, solve the captcha. The browser then tries to open a
signalcaptcha://...link; copy the value aftersignalcaptcha://(e.g. from the developer console). ansible-playbook <playbook> -e signal_api_captcha=<value>: Signal sends a code by SMS (or calls, withsignal_api_use_voice: true).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 }}}}