# signal-cli-rest-api: A Dockerized REST Wrapper Around signal-cli

> bbernhard/signal-cli-rest-api puts a REST layer in front of signal-cli so programs can send and receive Signal messages over HTTP. It is a thin wrapper, and the execution mode you pick decides whether that thinness costs you latency or memory.

**bbernhard/signal-cli-rest-api** — Dockerized Signal Messenger REST API

- Repository: https://github.com/bbernhard/signal-cli-rest-api
- Website: https://bbernhard.github.io/signal-cli-rest-api/
- Stars: 2,850 · Forks: 307
- Language: Go
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/bbernhard-signal-cli-rest-api

## The gap signal-cli-rest-api fills between signal-cli and your code

signal-cli is a command line client. If you want a shell script, a cron job or a small service to send a Signal message, you either shell out to it or wrap it yourself. This project does the wrapping: the README states that it creates "a small dockerized REST API around signal-cli", and the exposed surface includes registering a number, verifying it with an SMS code, sending messages with attachments to multiple recipients or a group, receiving messages, linking devices, creating, listing and removing groups, listing, serving and deleting attachments, and updating a profile.

The audience is narrow and specific. It is for people who already run Signal on a phone and want a second, programmatic path into the same account, or who want a self-hosted endpoint that other internal tools can POST to. It is not a messaging platform. One container maps to one registered Signal number, and the configuration directory that holds the password and cryptographic keys is the thing you must not lose.

## How the wrapper actually runs signal-cli: four execution modes

The MODE environment variable selects how signal-cli is invoked, and this is the design decision that shapes everything else. In normal mode, the default, the signal-cli executable is invoked for every REST API request. Because signal-cli is a Java application, each call starts a new JVM, which the README describes as increasing latency and being the slowest mode. In native mode a precompiled GraalVM binary, signal-cli-native, handles each request instead, giving lower latency and memory per call. The README adds two caveats: on armv7 this mode is not available and falls back to normal, and native mode may be less stable because the GraalVM compiler is experimental.

The two json-rpc modes change the shape rather than the speed of each call. json-rpc spawns a single JVM-based signal-cli instance as a daemon, which the README calls usually the fastest mode but notes requires more memory because the JVM keeps running. json-rpc-native starts the native binary in daemon mode, combining the lower per-call cost of native with the persistent process of json-rpc. The README's own comparison table ranks json-rpc-native highest on speed with normal resident memory, json-rpc highest on speed but increased memory, native in the middle, and normal at the bottom.

That table is the honest summary of the trade-off. There is no mode that is both cheapest in memory and fastest per call without a daemon; the daemon is what buys the speed, and the daemon is what holds the memory. Pick based on whether your host is memory-constrained or latency-constrained, not on which name sounds best.

## Installing signal-cli-rest-api and sending a first message

The README's getting started path assumes Docker and a directory you keep outside the container, so that deleting and recreating the container does not force you to re-register your Signal number. Create that directory first:

```bash
mkdir -p $HOME/.local/share/signal-api
```

Then start the container, mapping the host directory onto the signal-cli configuration path inside the container and selecting native mode:

```bash
docker run -d --name signal-api --restart=always -p 8080:8080 \
  -v $HOME/.local/share/signal-api:/home/.local/share/signal-cli \
  -e 'MODE=native' bbernhard/signal-cli-rest-api
```

For the next step the README registers the container as a secondary device rather than a fresh number. Open http://localhost:8080/v1/qrcodelink?device_name=signal-api in a browser, then in the Signal mobile app go to Settings > Linked devices and scan the QR code with the + button. The container is now linked to your existing account.

To confirm the API works, POST to /v2/send with your own number and a recipient, both in international format:

```bash
curl -X POST -H "Content-Type: application/json" 'http://localhost:8080/v2/send' \
  -d '{"message": "Test via Signal API!", "number": "+4412345", "recipients": [ "+44987654" ]}'
```

The README says the recipient should then have received the message. The repository also ships a docker-compose.yml that uses the rootless-latest image tag, sets user: "1000:1000", adds no-new-privileges:true, and mounts a tmpfs at /run with size=64m. If you use that file, change the UID:GID to match your host user, because the volume comment warns that the user must be able to read and write the configuration directory.

## AUTO_RECEIVE_SCHEDULE and the receive endpoint's destructive habit

The README attaches a warning to AUTO_RECEIVE_SCHEDULE that is worth reading twice. The setting accepts cron expressions and automatically calls the receive endpoint on that schedule, for example 0 22 * * * for daily at 10pm. It is only needed in normal or native mode, because signal-cli recommends calling receive regularly.

The warning is the important part: calling receive fetches all messages for the registered Signal number from the Signal server. The README states directly that if you are using the REST API for receiving messages, enabling AUTO_RECEIVE_SCHEDULE is not a good idea because you might lose messages that way. The scheduled call competes with your own polling for the same queue, and whichever runs first takes the messages.

So the two use cases pull in opposite directions. If the API is only a send path, a scheduled receive keeps the signal-cli session healthy and costs you nothing. If the API is your receive path, you want to own that call yourself and leave the schedule out of docker-compose.yml. This is a real design constraint, not a documentation footnote, and it is the single most common way a deployment of this project can quietly drop data.

## Where signal-cli-rest-api is the wrong tool

The project is a wrapper, and it inherits every constraint of signal-cli plus a few of its own. It is one Signal number per container. There is no account pool, no per-tenant isolation and no queueing layer described in the README. If you need to send on behalf of many users, you are running many containers and managing many configuration directories, each holding keys you cannot regenerate.

The native mode fallback matters on hardware. On armv7 the README says native mode is not available and falls back to normal, so a device you chose for its low power draw gets the slowest execution path. If you were counting on native mode's latency profile on a Raspberry Pi class board, verify the architecture before you commit.

The GraalVM caveat is also stated plainly: native mode may be less stable because the compiler is experimental. For a message path where a dropped send is a real problem, that is a reason to prefer json-rpc-native or plain json-rpc, accepting the daemon's memory cost in exchange for a JVM that is not experimental.

Finally, this is a self-hosted component. The README does not document rollback, high availability or multi-instance coordination, and the Swagger reference is the only API contract given. Anything beyond a single running container is your problem to solve.

## signal-cli-rest-api compared with calling signal-cli directly

The obvious alternative is signal-cli itself, invoked from your own code. The difference is where the process boundary sits. Calling signal-cli directly means your application owns argument construction, output parsing and the JVM startup cost on every invocation, unless you write your own daemon wrapper. signal-cli-rest-api moves that boundary to HTTP: your application speaks JSON to a port, and the container owns the signal-cli invocation, the mode selection and the configuration directory.

That is a genuine trade. You gain a language-neutral interface, a documented Swagger surface and a container image that bundles signal-cli and libsignal-client at pinned versions (the Dockerfile takes SIGNAL_CLI_VERSION=0.14.8 and LIBSIGNAL_CLIENT_VERSION=0.102.1 as build arguments). You lose direct control over signal-cli's flags and its stderr, and you add a network hop and a container to operate. The README also points at pysignalclirestapi, a small Python library listed under community projects, which is the right choice if your only consumer is Python and you would rather not run the HTTP layer at all.

If your environment already has an HTTP-based integration story, the wrapper is the shorter path. If you are one Go or Java service that needs one method call, the wrapper is an extra process you now have to monitor.

## Maintenance, licensing and upgrade cost

The repository is not archived, and the last push was on 2026-09-16, which is recent. The release history shows 0.101-pre on 2026-09-05, 0.100 on 2026-06-11 and 0.100-pre on 2026-06-02, so the project is still cutting releases and the 0.101 line is currently in a pre-release state. The project is licensed under MIT, which permits commercial use and modification; the container also bundles signal-cli and libsignal-client, which carry their own licences, and the README does not restate them, so check those upstream before you redistribute the image.

The upgrade cost is mostly the volume, not the image. Because the configuration directory is mounted from the host and holds the password and cryptographic keys, the documented workflow is to delete and recreate the container and keep the volume. That is cheap. What is not cheap is the coupling in the Dockerfile: signal-cli and libsignal-client versions are build arguments, so a new image may move both at once, and the README does not describe a rollback path if a new signal-cli version changes behaviour. Pin the image tag rather than tracking latest if you care about that.

## Conclusion

Adopt signal-cli-rest-api if you need Signal inside an existing HTTP-based system and can hold a persistent Docker volume for the signal-cli configuration. Do not adopt it if you need a hosted, multi-tenant messaging service: this is a local wrapper around one Signal number, and the README warns that calling receive regularly can lose messages when the API is your receive path. Before deploying, verify which MODE your hardware supports (armv7 falls back from native to normal), confirm the container user can read and write the mounted signal-cli-config directory, and decide whether AUTO_RECEIVE_SCHEDULE belongs in your docker-compose.yml at all.

## FAQ

### How do I install signal-cli-rest-api?

The README's getting started section creates a host directory for the signal-cli configuration, then runs the bbernhard/signal-cli-rest-api image with that directory mounted at /home/.local/share/signal-cli and MODE set to native. The repository also includes a docker-compose.yml that uses the rootless-latest image tag.

### Is there an API for Signal?

Signal does not publish an official REST API, but signal-cli-rest-api exposes a REST interface around signal-cli, covering registration, sending messages with attachments, receiving, device linking, group management and profile updates. The Swagger reference is hosted on the project's documentation site.

### What is signal-cli?

signal-cli is the command line client that signal-cli-rest-api wraps. The README notes that signal-cli recommends calling receive on a regular basis, which is why the wrapper offers the AUTO_RECEIVE_SCHEDULE setting.

## Sources

- [bbernhard/signal-cli-rest-api on GitHub](https://github.com/bbernhard/signal-cli-rest-api)
- [License: MIT](https://github.com/bbernhard/signal-cli-rest-api/blob/master/LICENSE)
- [Project website](https://bbernhard.github.io/signal-cli-rest-api/)
- [README](https://github.com/bbernhard/signal-cli-rest-api/blob/master/README.md)
- [Releases](https://github.com/bbernhard/signal-cli-rest-api/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bbernhard-signal-cli-rest-api
