signal-cli Protocol
siggy communicates with signal-cli using
JSON-RPC 2.0 over stdin/stdout. signal-cli
is spawned as a child process in jsonRpc mode.
Starting signal-cli
signal-cli is launched with:
signal-cli -a +15551234567 jsonRpc
This starts signal-cli in JSON-RPC mode, reading requests from stdin and writing responses/notifications to stdout. Each message is a single JSON line.
Request format
Requests sent from siggy to signal-cli:
{
"jsonrpc": "2.0",
"id": "550e8400-e29b-41d4-a716-446655440000",
"method": "send",
"params": {
"recipient": ["+15551234567"],
"message": "Hello!"
}
}
Each request has a unique UUID id for response correlation.
Response format
Responses from signal-cli for RPC calls:
{
"jsonrpc": "2.0",
"id": "550e8400-e29b-41d4-a716-446655440000",
"result": { ... }
}
Or on error:
{
"jsonrpc": "2.0",
"id": "550e8400-e29b-41d4-a716-446655440000",
"error": {
"code": -1,
"message": "error description"
}
}
Notification format
Notifications are unsolicited JSON-RPC requests from signal-cli (no matching
outbound request). They have a method field but no id:
{
"jsonrpc": "2.0",
"method": "receive",
"params": {
"envelope": {
"source": "+15559876543",
"sourceDevice": 1,
"timestamp": 1700000000000,
"dataMessage": {
"message": "Hey there!",
"timestamp": 1700000000000
}
}
}
}
Methods used
Outbound (siggy -> signal-cli)
| Method | Purpose |
|---|---|
send | Send a message (also used for edits via editTimestamp param) |
listContacts | Request the contact address book |
listGroups | Request the list of groups |
sendSyncRequest | Request a sync from the primary device |
sendReaction | Send an emoji reaction to a message |
remoteDelete | Delete a message for all recipients |
sendTypingIndicator | Send typing started/stopped indicator |
sendReceipt | Send a read receipt for one or more messages |
updateGroup | Create/rename group, add/remove members |
quitGroup | Leave a group |
block | Block a contact or group |
unblock | Unblock a contact or group |
setExpiration | Set disappearing message timer |
updateProfile | Update own Signal profile (name, about, emoji) |
listIdentities | List known identity keys for contacts |
trust | Trust a contact’s identity key |
sendMessageRequestResponse | Accept or delete a message request |
Inbound notifications (signal-cli -> siggy)
| Method | Purpose | Maps to |
|---|---|---|
receive | Incoming message | SignalEvent::MessageReceived |
receiveTyping | Typing indicator | SignalEvent::TypingIndicator |
receiveReceipt | Delivery/read receipt | SignalEvent::ReceiptReceived |
Incoming receive envelopes may also contain:
| Envelope field | Purpose | Maps to |
|---|---|---|
dataMessage.reaction | Incoming reaction | SignalEvent::ReactionReceived |
dataMessage.remoteDelete | Remote delete request | SignalEvent::RemoteDeleteReceived |
dataMessage.quote | Quoted reply metadata | quote field on SignalMessage |
editMessage | Edited message | SignalEvent::EditReceived |
syncMessage.sentMessage | Outgoing sync (own messages from other devices) | Same as above, with is_outgoing = true |
syncMessage.readMessages | Read sync from other devices | SignalEvent::ReadSyncReceived |
dataMessage.sticker | Sticker message | Body set to [Sticker: emoji] |
dataMessage.textStyles / bodyRanges | Text formatting (bold, italic, etc.) | text_styles field on SignalMessage |
dataMessage.expiresInSeconds | Disappearing message timer | expires_in_seconds on SignalMessage |
dataMessage.isViewOnce | View-once message flag | Body set to [View-once message] |
callMessage | Missed call notification | SignalEvent::SystemMessage |
Parsing logic
The stdout reader in SignalClient determines the message type by checking
which fields are present:
- If
methodis present -> it’s a notification, parse based on method name - If
idandresult/errorare present -> it’s a response, look up the method viapending_requests[id]and parse accordingly - Unknown methods are logged and discarded
Sync messages
Messages sent from the primary device arrive as sync messages. They are
identified by having is_outgoing = true in the parsed SignalMessage.
The destination field indicates the recipient, and the message is routed
to the appropriate conversation.