Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)

MethodPurpose
sendSend a message (also used for edits via editTimestamp param)
listContactsRequest the contact address book
listGroupsRequest the list of groups
sendSyncRequestRequest a sync from the primary device
sendReactionSend an emoji reaction to a message
remoteDeleteDelete a message for all recipients
sendTypingIndicatorSend typing started/stopped indicator
sendReceiptSend a read receipt for one or more messages
updateGroupCreate/rename group, add/remove members
quitGroupLeave a group
blockBlock a contact or group
unblockUnblock a contact or group
setExpirationSet disappearing message timer
updateProfileUpdate own Signal profile (name, about, emoji)
listIdentitiesList known identity keys for contacts
trustTrust a contact’s identity key
sendMessageRequestResponseAccept or delete a message request

Inbound notifications (signal-cli -> siggy)

MethodPurposeMaps to
receiveIncoming messageSignalEvent::MessageReceived
receiveTypingTyping indicatorSignalEvent::TypingIndicator
receiveReceiptDelivery/read receiptSignalEvent::ReceiptReceived

Incoming receive envelopes may also contain:

Envelope fieldPurposeMaps to
dataMessage.reactionIncoming reactionSignalEvent::ReactionReceived
dataMessage.remoteDeleteRemote delete requestSignalEvent::RemoteDeleteReceived
dataMessage.quoteQuoted reply metadataquote field on SignalMessage
editMessageEdited messageSignalEvent::EditReceived
syncMessage.sentMessageOutgoing sync (own messages from other devices)Same as above, with is_outgoing = true
syncMessage.readMessagesRead sync from other devicesSignalEvent::ReadSyncReceived
dataMessage.stickerSticker messageBody set to [Sticker: emoji]
dataMessage.textStyles / bodyRangesText formatting (bold, italic, etc.)text_styles field on SignalMessage
dataMessage.expiresInSecondsDisappearing message timerexpires_in_seconds on SignalMessage
dataMessage.isViewOnceView-once message flagBody set to [View-once message]
callMessageMissed call notificationSignalEvent::SystemMessage

Parsing logic

The stdout reader in SignalClient determines the message type by checking which fields are present:

  1. If method is present -> it’s a notification, parse based on method name
  2. If id and result/error are present -> it’s a response, look up the method via pending_requests[id] and parse accordingly
  3. 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.