CasCap.Api.SignalCli
0.10.31
Prefix Reserved
dotnet add package CasCap.Api.SignalCli --version 0.10.31
NuGet\Install-Package CasCap.Api.SignalCli -Version 0.10.31
<PackageReference Include="CasCap.Api.SignalCli" Version="0.10.31" />
<PackageVersion Include="CasCap.Api.SignalCli" Version="0.10.31" />
<PackageReference Include="CasCap.Api.SignalCli" />
paket add CasCap.Api.SignalCli --version 0.10.31
#r "nuget: CasCap.Api.SignalCli, 0.10.31"
#:package CasCap.Api.SignalCli@0.10.31
#addin nuget:?package=CasCap.Api.SignalCli&version=0.10.31
#tool nuget:?package=CasCap.Api.SignalCli&version=0.10.31
CasCap.Api.SignalCli
A .NET library that wraps the signal-cli REST API (generated against v0.98), providing a typed SignalCliRestClientService for sending and managing Signal encrypted messages, together with a health check and DI registration.
Installation
dotnet add package CasCap.Api.SignalCli
Transport Modes
TransportMode has four values, mirroring the MODE environment variable of the signal-cli REST API server, and must match whatever the server is running. Client-side there are only two behaviours: Normal and Native poll over HTTP, while JsonRpc and JsonRpcNative receive pushed frames over a WebSocket. The performance and memory characteristics below are properties of the server mode, not of this client.
| Mode | Server MODE |
Service | Message reception | Server characteristics |
|---|---|---|---|---|
Normal |
normal |
SignalCliRestClientService |
HTTP polling (GET /v1/receive/{number}) |
Slowest, normal memory |
Native |
native |
SignalCliRestClientService |
HTTP polling (GET /v1/receive/{number}) |
Medium, normal memory |
JsonRpc |
json-rpc |
SignalCliJsonRpcClientService |
WebSocket push (ws://host/v1/receive/{number}) |
Faster, increased memory |
JsonRpcNative |
json-rpc-native |
SignalCliJsonRpcClientService |
WebSocket push (ws://host/v1/receive/{number}) |
Fastest, normal memory |
On the WebSocket transports the connection is persistent and reconnects automatically with exponential backoff (2 s → 2 min, up to 10 attempts); every non-receive operation still goes over HTTP via the underlying SignalCliRestClientService. Registration, verification and device-linking endpoints are unavailable while the server runs in either JSON-RPC mode, per upstream documentation.
Receiving Messages
Both transports implement ISignalCliReceiver, so inbound messages are consumed the same way regardless of TransportMode. Switching between HTTP polling and the WebSocket push stream is a configuration change, not a code change.
public sealed class EchoWorker(ISignalCliReceiver receiver, ISignalCliClient client,
IOptions<SignalCliConfig> options) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var account = options.Value.PhoneNumber;
await foreach (var msg in receiver.StreamMessagesAsync(stoppingToken))
{
var sender = msg.Envelope.Source;
var text = msg.Envelope.DataMessage?.Message;
if (sender is null || string.IsNullOrWhiteSpace(text))
continue;
await client.SendMessage(new SignalMessageRequest
{
Number = account,
Recipients = [sender],
Message = $"echo: {text}"
}, stoppingToken);
}
}
}
StreamMessagesAsync is a cold stream: enumeration starts the transport (connecting the WebSocket where applicable) and abandoning the enumerator stops it. Cancelling the token ends the stream by throwing OperationCanceledException, per the usual IAsyncEnumerable convention. Only one active enumeration per instance is expected; concurrent consumers compete for messages rather than each receiving a copy.
Calling ConnectAsync first is optional but surfaces connection failures at startup instead of on the first message. On the REST transport it is a no-op.
Controller
The MVC controller lives in a separate package, CasCap.Api.SignalCli.AspNetCore, so that worker services, console apps and daemons can consume this library without taking a dependency on MVC or API versioning.
Purpose
ISignalCliClient is the abstraction over the full signal-cli REST API surface. Depend on it rather than the concrete SignalCliRestClientService so the client can be substituted with a fake in tests. It resolves to SignalCliRestClientService in every transport mode, since only message reception differs between them.
Every method returns null or false on failure and logs the cause; failures are not thrown. The exception is caller-requested cancellation, which propagates as OperationCanceledException so an abandoned call is never mistaken for an API error.
SignalCliRestClientService is the HttpClient-backed implementation:
General
| Method | Endpoint | Description |
|---|---|---|
GetAbout() |
GET /v1/about |
Returns version and build information |
GetConfiguration() |
GET /v1/configuration |
Retrieves the signal-cli configuration |
SetConfiguration(config) |
POST /v1/configuration |
Updates the signal-cli configuration |
Messaging
| Method | Endpoint | Description |
|---|---|---|
SendMessage(SignalMessageRequest) |
POST /v2/send |
Sends a message to one or more recipients |
ReceiveMessages(number) |
GET /v1/receive/{number} |
Receives pending messages for the specified account |
ShowTypingIndicator(number, recipient) |
PUT /v1/typing-indicator/{number} |
Shows a typing indicator |
HideTypingIndicator(number, recipient) |
DELETE /v1/typing-indicator/{number} |
Hides a typing indicator |
SendReaction(number, recipient, reaction, targetAuthor, timestamp) |
POST /v1/reactions/{number} |
Sends a reaction to a message |
RemoveReaction(number, recipient, reaction, targetAuthor, timestamp) |
DELETE /v1/reactions/{number} |
Removes a reaction from a message |
SendReceipt(number, recipient, receiptType, timestamp) |
POST /v1/receipts/{number} |
Sends a read/viewed receipt |
RemoteDelete(number, recipient, timestamp) |
DELETE /v1/remote-delete/{number} |
Remotely deletes a sent message |
Registration
| Method | Endpoint | Description |
|---|---|---|
RegisterNumber(number) |
POST /v1/register/{number} |
Registers a phone number |
VerifyNumber(number, token) |
POST /v1/register/{number}/verify/{token} |
Verifies registration |
UnregisterNumber(number) |
POST /v1/unregister/{number} |
Unregisters a phone number |
Accounts
| Method | Endpoint | Description |
|---|---|---|
ListAccounts() |
GET /v1/accounts |
Lists all registered accounts |
SetPin(number, pin) |
POST /v1/accounts/{number}/pin |
Sets a registration PIN |
RemovePin(number) |
DELETE /v1/accounts/{number}/pin |
Removes the registration PIN |
SubmitRateLimitChallenge(number, challengeToken, captcha) |
POST /v1/accounts/{number}/rate-limit-challenge |
Submits a rate-limit challenge |
UpdateAccountSettings(number, discoverableByNumber, shareNumber) |
PUT /v1/accounts/{number}/settings |
Updates account settings |
SetUsername(number, username) |
POST /v1/accounts/{number}/username |
Sets a username |
RemoveUsername(number) |
DELETE /v1/accounts/{number}/username |
Removes the username |
Contacts
| Method | Endpoint | Description |
|---|---|---|
ListContacts(number) |
GET /v1/contacts/{number} |
Lists contacts for an account |
UpdateContact(number, recipient, name, expirationInSeconds) |
PUT /v1/contacts/{number} |
Updates a contact |
SyncContacts(number) |
POST /v1/contacts/{number}/sync |
Triggers a contact sync |
Devices
| Method | Endpoint | Description |
|---|---|---|
GetQrCodeLink(deviceName) |
GET /v1/qrcodelink |
Retrieves a QR code link for device linking |
GetQrCodeLinkRaw(deviceName) |
GET /v1/qrcodelink/raw |
Retrieves the device-link URI |
ListLinkedDevices(number) |
GET /v1/devices/{number} |
Lists linked devices |
AddDevice(number, uri) |
POST /v1/devices/{number} |
Links a new device |
RemoveLinkedDevice(number, deviceId) |
DELETE /v1/devices/{number}/{deviceId} |
Removes a linked device |
DeleteLocalAccountData(number) |
DELETE /v1/devices/{number}/local-data |
Deletes local account data |
Groups
| Method | Endpoint | Description |
|---|---|---|
ListGroups(number) |
GET /v1/groups/{number} |
Lists all groups |
GetGroup(number, groupId) |
GET /v1/groups/{number}/{groupId} |
Retrieves a specific group |
CreateGroup(number, group) |
POST /v1/groups/{number} |
Creates a new group |
UpdateGroup(number, groupId, group) |
PUT /v1/groups/{number}/{groupId} |
Updates a group |
DeleteGroup(number, groupId) |
DELETE /v1/groups/{number}/{groupId} |
Deletes a group |
AddGroupMembers(number, groupId, members) |
POST /v1/groups/{number}/{groupId}/members |
Adds members to a group |
RemoveGroupMembers(number, groupId, members) |
DELETE /v1/groups/{number}/{groupId}/members |
Removes members from a group |
AddGroupAdmins(number, groupId, admins) |
POST /v1/groups/{number}/{groupId}/admins |
Adds admins to a group |
RemoveGroupAdmins(number, groupId, admins) |
DELETE /v1/groups/{number}/{groupId}/admins |
Removes admins from a group |
JoinGroup(number, groupId) |
POST /v1/groups/{number}/{groupId}/join |
Joins a group |
QuitGroup(number, groupId) |
POST /v1/groups/{number}/{groupId}/quit |
Leaves a group |
BlockGroup(number, groupId) |
POST /v1/groups/{number}/{groupId}/block |
Blocks a group |
GetGroupAvatar(number, groupId) |
GET /v1/groups/{number}/{groupId}/avatar |
Downloads a group avatar |
Identities
| Method | Endpoint | Description |
|---|---|---|
ListIdentities(number) |
GET /v1/identities/{number} |
Lists all known identities |
TrustIdentity(number, numberToTrust) |
PUT /v1/identities/{number}/trust/{numberToTrust} |
Trusts an identity |
Attachments
| Method | Endpoint | Description |
|---|---|---|
ListAttachments() |
GET /v1/attachments |
Lists all attachments |
GetAttachment(attachmentId) |
GET /v1/attachments/{attachmentId} |
Retrieves an attachment |
DeleteAttachment(attachmentId) |
DELETE /v1/attachments/{attachmentId} |
Deletes an attachment |
Profile
| Method | Endpoint | Description |
|---|---|---|
UpdateProfile(number, profile) |
PUT /v1/profiles/{number} |
Updates the Signal profile |
Search
| Method | Endpoint | Description |
|---|---|---|
SearchNumbers(number, numbers) |
GET /v1/search/{number} |
Searches for registered phone numbers |
Sticker Packs
| Method | Endpoint | Description |
|---|---|---|
ListStickerPacks(number) |
GET /v1/sticker-packs/{number} |
Lists installed sticker packs |
AddStickerPack(number, packId, packKey) |
POST /v1/sticker-packs/{number} |
Installs a sticker pack |
Configuration
Registered via IServiceCollection.AddSignalCli(). Configuration section: CasCap:SignalCliConfig.
| Setting | Type | Default | Required | Description |
|---|---|---|---|---|
TransportMode |
SignalCliTransport |
JsonRpc |
— | Transport mode: Normal, Native (HTTP polling) or JsonRpc, JsonRpcNative (WebSocket) |
BaseAddress |
string |
— | ✓ | Base URL of the signal-cli REST API (e.g. http://localhost:8080) |
HealthCheckUri |
string |
"v1/health" |
— | Path used to verify API connectivity |
HealthCheck |
KubernetesProbeTypes |
Readiness |
— | Kubernetes probe type for the health check tag |
PhoneNumber |
string |
— | ✓ | Registered Signal sender number (e.g. "+49151...") |
PhoneNumberDebug |
string? |
null |
— | Optional recipient number for debug/diagnostic messages ("Note to Self" feed) |
SendTimeoutMs |
int |
180000 |
— | Per-request timeout in milliseconds for POST /v2/send |
ChannelCapacity |
int |
256 |
— | Bounded capacity of the internal message channel (back-pressure when full) |
ReceivePollIntervalMs |
int |
1000 |
— | Delay between GET /v1/receive/{number} polls when streaming over the REST transport; ignored by the JSON-RPC transports |
MaxReconnectAttempts |
int |
10 |
— | Maximum number of WebSocket reconnection attempts before giving up |
InitialReconnectDelayMs |
int |
2000 |
— | Initial backoff delay in milliseconds for WebSocket reconnection |
MaxReconnectDelayMs |
int |
120000 |
— | Maximum backoff delay in milliseconds for WebSocket reconnection |
ReceiveStalenessTimeoutMs |
int |
0 |
— | Max silent period (ms) on the receive stream before the watchdog logs an error and forces a reconnect; 0 disables it. Tune above your longest expected inbound gap to avoid false positives on quiet accounts |
BasicAuthEnabled |
bool |
false |
— | Whether to attach HTTP Basic credentials to outgoing requests (cross-cluster access via ingress) |
Username |
string? |
null |
— | Basic auth username, required when BasicAuthEnabled is set unless ApiAuthConfig supplies one |
Password |
string? |
null |
— | Basic auth password, required when BasicAuthEnabled is set unless ApiAuthConfig supplies one |
Basic authentication
When the signal-cli REST API sits behind an authenticating reverse proxy, set BasicAuthEnabled and supply Username and Password. The credentials are applied to both the HttpClient and the JSON-RPC WebSocket handshake.
If Username and Password are left unset, the library falls back to CasCap:ApiAuthConfig, which suits hosts that already bind a single set of ingress credentials for every API they call. When neither source yields credentials, registration throws an InvalidOperationException naming both configuration keys rather than failing later with an opaque 401.
Configuration Examples
Minimal
{
"CasCap": {
"SignalCliConfig": {
"BaseAddress": "http://signalcli.monitoring.svc.cluster.local",
"PhoneNumber": "+49151..."
}
}
}
Fully configured
{
"CasCap": {
"SignalCliConfig": {
"TransportMode": "JsonRpc",
"BaseAddress": "http://signalcli.monitoring.svc.cluster.local",
"HealthCheckUri": "v1/about",
"HealthCheck": "Readiness",
"PhoneNumber": "+49151...",
"PhoneNumberDebug": "+49151...",
"SendTimeoutMs": 180000,
"ChannelCapacity": 256,
"ReceivePollIntervalMs": 1000,
"MaxReconnectAttempts": 10,
"InitialReconnectDelayMs": 2000,
"MaxReconnectDelayMs": 120000,
"ReceiveStalenessTimeoutMs": 0
}
}
}
Health Check
SignalCliConnectionHealthCheck – Verifies that the signal-cli REST API is reachable by issuing a GET request to {BaseAddress}/{HealthCheckUri}.
Class Hierarchy
classDiagram
direction LR
HttpClientBase <|-- SignalCliRestClientService
HttpEndpointCheckBase <|-- SignalCliConnectionHealthCheck
ISignalCliClient <|.. SignalCliRestClientService
ISignalCliReceiver <|.. SignalCliRestClientService
ISignalCliReceiver <|.. SignalCliJsonRpcClientService
SignalCliJsonRpcClientService ..> SignalCliRestClientService : delegates
class ISignalCliClient {
<<interface>>
+GetAbout(CancellationToken) SignalAbout?
+SendMessage(SignalMessageRequest, CancellationToken) SignalMessageResponse?
+ReceiveMessages(number, CancellationToken) SignalReceivedMessage[]?
+ListGroups(number, CancellationToken) SignalGroup[]?
+ListContacts(number, allRecipients, CancellationToken) SignalContact[]?
}
class ISignalCliReceiver {
<<interface>>
+ConnectAsync(CancellationToken) Task
+StreamMessagesAsync(CancellationToken) IAsyncEnumerable~SignalReceivedMessage~
}
class SignalCliRestClientService {
-SignalCliConfig _config
+GetAbout() SignalAbout?
+SendMessage(SignalMessageRequest) SignalMessageResponse?
+ReceiveMessages(number) SignalReceivedMessage[]?
+ListAccounts() string[]?
+ListGroups(number) SignalGroup[]?
+ListContacts(number) SignalContact[]?
+ListIdentities(number) SignalIdentity[]?
+ListLinkedDevices(number) SignalDevice[]?
+ListAttachments() string[]?
+ListStickerPacks(number) SignalStickerPack[]?
+SearchNumbers(number, numbers) SearchResult[]?
-GetAsync~T~(requestUri) T?
-PostAsync~T~(requestUri, body) T?
-PostBoolAsync(requestUri, body) bool
-PutAsync(requestUri, body) bool
-DeleteAsync(requestUri, body) bool
-DeleteAsync~T~(requestUri, body) T?
}
class SignalCliJsonRpcClientService {
-SignalCliRestClientService _restClient
-ClientWebSocket? _webSocket
+ConnectAsync(CancellationToken) Task
+StreamMessagesAsync(CancellationToken) IAsyncEnumerable~SignalReceivedMessage~
-ReceiveLoopWithReconnectAsync(CancellationToken) Task
+BuildWebSocketUri(baseAddress, phoneNumber)$ Uri
}
class SignalCliConnectionHealthCheck {
+Name$ string
}
class SignalCliConfig {
+TransportMode SignalCliTransport
+BaseAddress string
+HealthCheckUri string
+HealthCheck KubernetesProbeTypes
+SendTimeoutMs int
+PhoneNumber string
+PhoneNumberDebug string?
+ChannelCapacity int
+ReceivePollIntervalMs int
+MaxReconnectAttempts int
+InitialReconnectDelayMs int
+MaxReconnectDelayMs int
+BasicAuthEnabled bool
+Username string?
+Password string?
}
SignalCliRestClientService ..> SignalCliConfig : reads
SignalCliJsonRpcClientService ..> SignalCliConfig : reads
SignalCliConnectionHealthCheck ..> SignalCliConfig : reads
DI Registration Flow
flowchart LR
A["AddSignalCli()"] --> B["Bind SignalCliConfig"]
A --> C["Register HttpClient"]
A --> J["ISignalCliClient \u2192 SignalCliRestClientService"]
A --> D{"TransportMode?"}
A --> E["Register SignalCliConnectionHealthCheck"]
D -->|Normal / Native| F["INotifier + ISignalCliReceiver \u2192 SignalCliRestClientService"]
D -->|JsonRpc / JsonRpcNative| G["INotifier + ISignalCliReceiver \u2192 SignalCliJsonRpcClientService"]
G --> H["delegates non-receive ops"]
H --> F2["SignalCliRestClientService"]
C --> I["IHttpClientFactory"]
I --> F
I --> G
I --> E
Dependencies
NuGet packages
| Package | Purpose |
|---|---|
| Microsoft.Extensions.Http | HttpClient factory |
| Microsoft.Extensions.Diagnostics.HealthChecks | Health check abstractions |
| CasCap.Common.Configuration | Configuration binding helpers |
| CasCap.Common.Extensions | Shared extension helpers |
| CasCap.Common.Logging | Structured logging helpers |
| CasCap.Common.Net | HTTP client base (HttpClientBase) |
| CasCap.Common.Extensions.Diagnostics.HealthChecks | Kubernetes probe tag helpers |
| CasCap.Common.Services | Shared service utilities |
Project references
| Project | Purpose |
|---|---|
CasCap.Common.Configuration |
Configuration binding helpers |
License
This project is released under The Unlicense. See the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CasCap.Common.Configuration (>= 4.12.8)
- CasCap.Common.Extensions (>= 4.12.8)
- CasCap.Common.Extensions.Diagnostics.HealthChecks (>= 4.12.8)
- CasCap.Common.Logging (>= 4.12.8)
- CasCap.Common.Net (>= 4.12.8)
- CasCap.Common.Services (>= 4.12.8)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.12)
- Microsoft.Extensions.Http (>= 10.0.12)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CasCap.Api.SignalCli:
| Package | Downloads |
|---|---|
|
CasCap.Api.SignalCli.AspNetCore
ASP.NET Core integration for CasCap.Api.SignalCli — a versioned, read-only MVC controller exposing signal-cli account, contact, group, device, identity, attachment and sticker-pack queries. |
GitHub repositories
This package is not used by any popular GitHub repositories.