signal-sdk

repo
Created Jul 2025
Original
TypeScript
Stars
9
Forks
0
Size
1.0 MB
Last Update
5 days ago

A comprehensive TypeScript SDK for interacting with signal messenger, providing JSON-RPC communication and a powerful bot framework.

README.md

Signal SDK — TypeScript SDK for Signal Messenger

A comprehensive TypeScript SDK for interacting with signal-cli, providing JSON-RPC communication and a powerful bot framework.

npm version License: MIT TypeScript Node.js Tests Donate on Liberapay


Table of Contents


Features

Core

  • JSON-RPC communication with signal-cli daemon
  • Complete TypeScript support with strict type definitions
  • Event-driven architecture for real-time message handling
  • Exponential backoff retry mechanism with configurable policies
  • Built-in rate limiter to prevent throttling
  • Multi-level structured logging
  • E.164 phone number validation and input sanitization
  • Automatic signal-cli binary download on install (Linux, macOS, Windows)

Messaging

  • Send and receive text messages, attachments, and media
  • Text formatting — bold, italic, spoiler, strikethrough, monospace
  • Mentions, quotes, URL previews, message editing
  • View-once messages, end-session messages
  • Message reactions, typing indicators, read receipts
  • Remote delete and admin delete (group admins only)
  • Pin / unpin messages in conversations and groups
  • Silent messages — send without triggering a push notification (noUrgent)
  • Polls — create, vote, and terminate
  • Payment notifications (MobileCoin)
  • Note-to-self messages
  • Story replies

Groups

  • Create, update, and leave groups
  • Manage members — add, remove, ban, unban, promote/demote admins
  • Group invite links — generate, reset, send
  • Group permissions — message sending, editing details, adding members
  • Announcement groups (only admins can send)
  • Detailed group info including pending and requesting members

Contacts & Identity

  • List, update, and remove contacts
  • Block and unblock contacts or groups
  • Check Signal registration status
  • Safety number verification and identity trust management
  • Untrusted identity detection

Account & Devices

  • Register, verify, and unregister accounts
  • Update account settings — username, privacy, phone number sharing
  • Set and remove registration lock PIN
  • List and manage linked devices
  • Phone number change with SMS or voice verification
  • Multi-account support via MultiAccountManager

Media & Stickers

  • Retrieve attachments, avatars, and stickers by ID
  • Upload and install custom sticker packs

Receive Options

  • Configurable timeout, message limit, read receipt sending
  • Skip downloading attachments, stories, avatars, or sticker packs
  • Apply receive filters at daemon startup via connect() options

SignalBot Framework

  • Minimal boilerplate — create a working bot in under 20 lines
  • Built-in command parser with configurable prefix
  • Role-based access — admin-only commands
  • Built-in /help and /ping commands
  • Group auto-creation and member management
  • Welcome messages for new members
  • Command cooldown system

Installation

npm install signal-sdk

The postinstall script automatically downloads and installs the correct signal-cli binary for your platform into the package's bin/ directory. No manual signal-cli installation is needed.

macOS / Windows — signal-cli runs on the JVM. Make sure Java 17 or later is installed and available in your PATH.

Linux — The native binary is used. No JVM required.


Prerequisites

  • Node.js 18 or later
  • Java 17+ (macOS and Windows only — required by the JVM-based signal-cli distribution)
  • A Signal account with a registered phone number

To register a phone number with signal-cli before using the SDK:

# Register (triggers an SMS verification code) ./node_modules/signal-sdk/bin/signal-cli -a +33111111111 register # Verify with the code you received ./node_modules/signal-sdk/bin/signal-cli -a +33111111111 verify 123-456

Device Linking

If you want to use an existing Signal account (instead of registering a new number), link the SDK as a secondary device:

# Using the bundled CLI helper npx signal-sdk connect # With a custom device name npx signal-sdk connect "My Bot Device"

This prints a QR code in your terminal. Scan it from the Signal app on your phone: Settings → Linked Devices → Link New Device

You only need to do this once. The device stays linked permanently until explicitly removed.


Basic Usage

const { SignalCli } = require("signal-sdk"); const signal = new SignalCli("+33111111111"); await signal.connect(); // Send a message await signal.sendMessage("+33222222222", "Hello from Signal SDK!"); // Listen for incoming messages signal.on("message", (message) => { const text = message.envelope?.dataMessage?.message; console.log("Received:", text); }); // Graceful shutdown await signal.gracefulShutdown();

Advanced Configuration

const { SignalCli } = require("signal-sdk"); const signal = new SignalCli("+33111111111", undefined, { maxRetries: 3, retryDelay: 1000, maxConcurrentRequests: 5, minRequestInterval: 200, requestTimeout: 60000, connectionTimeout: 30000, autoReconnect: true, verbose: false, }); await signal.connect();

Connect with startup flags

Pass options directly to the jsonRpc subprocess at connection time to control what signal-cli downloads or processes:

await signal.connect({ ignoreAttachments: true, // do not download attachment files ignoreAvatars: true, // do not download contact/profile avatars ignoreStickers: true, // do not download sticker packs ignoreStories: true, // do not receive story messages sendReadReceipts: true, // automatically send read receipts receiveMode: "on-start", // start receiving immediately (default) });

Daemon modes

// Unix socket (daemon must be running separately) const signal = new SignalCli("+33111111111", undefined, { daemonMode: "unix-socket", socketPath: "/run/signal-cli/socket", }); // TCP const signal = new SignalCli("+33111111111", undefined, { daemonMode: "tcp", tcpHost: "localhost", tcpPort: 7583, }); // HTTP const signal = new SignalCli("+33111111111", undefined, { daemonMode: "http", httpBaseUrl: "http://localhost:8080", });

Common Use Cases

Send a message

await signal.sendMessage("+33222222222", "Hello!");

Send a silent message (no push notification)

await signal.sendMessage("+33222222222", "Background sync message", { noUrgent: true, });

Send formatted text

await signal.sendMessage("+33222222222", "Hello *world*!", { textStyles: [{ start: 6, length: 5, style: "BOLD" }], });

Send with mention

await signal.sendMessage("groupId==", "Hey @Alice, check this out!", { mentions: [{ start: 4, length: 6, number: "+33333333333" }], });

Reply to a message

await signal.sendMessage("+33222222222", "I agree!", { quote: { timestamp: 1700000000000, author: "+33222222222", text: "Original message text", }, });

Edit a message

await signal.sendMessage("+33222222222", "Corrected text", { editTimestamp: 1700000000000, });

Send files

await signal.sendMessage("+33222222222", "Here's the document:", { attachments: ["./path/to/document.pdf"], }); await signal.sendMessage("+33222222222", "Photos from today:", { attachments: ["./photo1.jpg", "./photo2.jpg"], });

React to a message

await signal.sendReaction("+33222222222", "+33222222222", 1700000000000, "👍"); // Remove a reaction await signal.sendReaction("+33222222222", "+33222222222", 1700000000000, "👍", true);

Remote delete a message

await signal.remoteDeleteMessage("+33222222222", 1700000000000);

Pin a message

await signal.sendPinMessage({ targetAuthor: "+33222222222", targetTimestamp: 1700000000000, groupId: "groupId==", pinDuration: -1, // -1 = forever (default) });

Unpin a message

await signal.sendUnpinMessage({ targetAuthor: "+33222222222", targetTimestamp: 1700000000000, groupId: "groupId==", });

Admin delete (group admins only)

await signal.sendAdminDelete({ groupId: "groupId==", targetAuthor: "+33222222222", targetTimestamp: 1700000000000, });

Create a poll

await signal.sendPollCreate({ question: "Favorite language?", options: ["TypeScript", "Python", "Go"], groupId: "groupId==", });

Receive messages manually

const messages = await signal.receive({ timeout: 5, ignoreAttachments: true, ignoreAvatars: true, ignoreStickers: true, sendReadReceipts: true, });

Group management

// Create a group const group = await signal.createGroup("My Group", [ "+33222222222", "+33333333333", ]); // Send to group await signal.sendMessage(group.groupId, "Welcome everyone!"); // Update group await signal.updateGroup(group.groupId, { name: "Updated Group Name", description: "New description", addMembers: ["+33444444444"], promoteAdmins: ["+33222222222"], }); // Get detailed group info const groups = await signal.listGroupsDetailed({ detailed: true }); // Send the group invite link to someone await signal.sendGroupInviteLink(group.groupId, "+33555555555"); // Reset the invite link await signal.resetGroupLink(group.groupId); // Leave the group await signal.quitGroup(group.groupId);

Contact management

// List contacts const contacts = await signal.listContacts(); // Update a contact await signal.updateContact("+33222222222", "Alice", { nickGivenName: "Ali", expiration: 3600, }); // Block / unblock await signal.block(["+33222222222"]); await signal.unblock(["+33222222222"]); // Remove a contact await signal.removeContact("+33222222222", { forget: true }); // Check registration status const statuses = await signal.getUserStatus(["+33222222222"]); console.log(statuses[0].isRegistered);

Identity verification

// Get safety number const safetyNumber = await signal.getSafetyNumber("+33222222222"); // Verify and trust const verified = await signal.verifySafetyNumber("+33222222222", safetyNumber); // List untrusted identities const untrusted = await signal.listUntrustedIdentities();

Account settings

// Set a username await signal.setUsername("alice"); // Delete username await signal.deleteUsername(); // Update privacy await signal.updateAccount({ discoverableByNumber: false, numberSharing: false, unrestrictedUnidentifiedSender: false, }); // Registration lock PIN await signal.setPin("123456"); await signal.removePin();

Multi-account

const { MultiAccountManager } = require("signal-sdk"); const manager = new MultiAccountManager(); manager.addAccount("+33111111111"); manager.addAccount("+33222222222"); await manager.connectAll(); const signal1 = manager.getAccount("+33111111111"); await signal1.sendMessage("+33333333333", "Hello from account 1!");

SignalBot Framework

Minimal bot

const { SignalBot } = require("signal-sdk"); require("dotenv").config(); const bot = new SignalBot({ phoneNumber: process.env.SIGNAL_PHONE_NUMBER, admins: [process.env.SIGNAL_ADMIN_NUMBER], }); bot.addCommand({ name: "hello", description: "Greet the user", handler: async (message, args) => { const name = args.join(" ") || "friend"; return `Hello ${name}!`; }, }); bot.on("ready", () => console.log("Bot is ready!")); bot.on("message", (msg) => console.log(`${msg.source}: ${msg.text}`)); await bot.start();

Full configuration

const bot = new SignalBot({ phoneNumber: "+33111111111", admins: ["+33222222222"], group: { name: "My Bot Group", description: "Managed by my bot", createIfNotExists: true, avatar: "./group-avatar.jpg", }, settings: { commandPrefix: "/", // default: "/" logMessages: true, welcomeNewMembers: true, cooldownSeconds: 2, }, });

Bot events

bot.on("ready", () => {}); bot.on("message", (message) => {}); bot.on("command", ({ command, user, args }) => {}); bot.on("groupMemberJoined", ({ groupId, member }) => {}); bot.on("error", (error) => {});

The bot includes built-in /help and /ping commands automatically.


API Reference

SignalCli methods

CategoryMethodDescription
Connectionconnect(options?)Start JSON-RPC daemon with optional startup flags
disconnect()Close the connection immediately
gracefulShutdown()Wait for process to exit cleanly
MessagingsendMessage(recipient, text, options?)Send a message to a number or group
sendReaction(recipient, author, timestamp, emoji, remove?)React to a message
sendTyping(recipient, stop?)Send a typing indicator
sendReceipt(recipient, timestamp, type?)Send a read or viewed receipt
remoteDeleteMessage(recipient, timestamp)Delete a sent message
sendPinMessage(options)Pin a message in a conversation or group
sendUnpinMessage(options)Unpin a message
sendAdminDelete(options)Delete a message for all group members (admin only)
sendPollCreate(options)Create a poll
sendPollVote(recipient, options)Vote on a poll
sendPollTerminate(recipient, options)Close a poll
sendPaymentNotification(recipient, data)Send a MobileCoin payment notification
sendNoteToSelf(message, options?)Send a message to your own account
sendMessageWithProgress(recipient, text, options?)Send with upload progress callback
receive(options?)Manually fetch pending messages
GroupscreateGroup(name, members)Create a new group
updateGroup(groupId, options)Update group settings and members
listGroups()List all groups
listGroupsDetailed(options?)List groups with members and invite links
getGroupsWithDetails(options?)List and parse groups
quitGroup(groupId)Leave a group
joinGroup(uri)Join via invite link
sendGroupInviteLink(groupId, recipient)Send invite link to a contact
resetGroupLink(groupId)Reset the invite link
setBannedMembers(groupId, members)Ban members from a group
ContactslistContacts()List all contacts
getContactsWithProfiles()List contacts with parsed profile data
updateContact(number, name?, options?)Update a contact
removeContact(number, options?)Remove a contact
block(recipients, groupId?)Block contacts or a group
unblock(recipients, groupId?)Unblock contacts or a group
getUserStatus(numbers?, usernames?)Check Signal registration status
sendContacts(options?)Sync contacts to linked devices
IdentitylistIdentities(number?)List identity keys
trustIdentity(number, safetyNumber, verified?)Trust an identity key
getSafetyNumber(number)Get the safety number for a contact
verifySafetyNumber(number, safetyNumber)Verify and auto-trust a safety number
listUntrustedIdentities()List all untrusted identities
Accountregister(number, voice?, captcha?)Register a phone number
verify(number, code, pin?)Complete registration verification
unregister()Deactivate the account
deleteLocalAccountData()Delete all local account data
updateAccount(options)Update account settings
updateAccountConfiguration(config)Update sync configuration
updateProfile(name, about?, emoji?, avatar?, options?)Update profile
setUsername(username)Set a Signal username
deleteUsername()Delete the Signal username
setPin(pin)Set a registration lock PIN
removePin()Remove the registration lock PIN
startChangeNumber(number, voice?, captcha?)Start a phone number change
finishChangeNumber(number, code, pin?)Complete a phone number change
listAccounts()List all local accounts
listAccountsDetailed()List accounts with name and UUID
sendPaymentNotification(recipient, data)Send a payment notification
submitRateLimitChallenge(challenge, captcha)Resolve a rate limit captcha
isRegistered(number)Check if a number is registered on Signal
DeviceslistDevices()List linked devices
removeDevice(deviceId)Remove a linked device
updateDevice(options)Rename a linked device
addDevice(uri, name?)Link a new device by URI
deviceLink(options?)Start device linking and show QR code
StickerslistStickerPacks()List installed sticker packs
addStickerPack(packId, packKey)Install a sticker pack
uploadStickerPack(manifest)Upload a custom sticker pack
getSticker(options)Retrieve sticker data
AttachmentsgetAttachment(options)Retrieve an attachment by ID
getAvatar(options)Retrieve a contact or group avatar
SyncsendSyncRequest()Request sync from primary device
sendMessageRequestResponse(recipient, response)Respond to a message request
getVersion()Get signal-cli version info

SendMessageOptions

OptionTypeDescription
attachmentsstring[]File paths to attach
mentionsMention[]Mentions in the message body
textStylesTextStyle[]Text formatting (BOLD, ITALIC, SPOILER, STRIKETHROUGH, MONOSPACE)
quoteQuoteOptionsReply to an existing message
expiresInSecondsnumberMessage expiration timer
isViewOncebooleanView-once message
editTimestampnumberTimestamp of the message to edit
storyTimestampnumberTimestamp of a story to reply to
storyAuthorstringAuthor of the story to reply to
previewUrlstringURL for link preview
previewTitlestringTitle for link preview
previewDescriptionstringDescription for link preview
previewImagestringImage path for link preview
noteToSelfbooleanSend to own account
endSessionbooleanEnd the session
noUrgentbooleanSend without push notification

PinMessageOptions

OptionTypeDescription
targetAuthorstringAuthor of the message to pin
targetTimestampnumberTimestamp of the message to pin
groupIdstringTarget group (mutually exclusive with recipients)
recipientsstring[]Target recipients for a direct pin
noteToSelfbooleanPin in your own conversation
pinDurationnumberDuration in seconds, -1 for forever (default)
notifySelfbooleanSend as normal message if self is a recipient

AdminDeleteOptions

OptionTypeDescription
groupIdstringGroup in which to delete the message (required)
targetAuthorstringAuthor of the message to delete
targetTimestampnumberTimestamp of the message to delete
storybooleanDelete a story instead of a regular message
notifySelfbooleanSend as normal message if self is a recipient

ReceiveOptions

OptionTypeDescription
timeoutnumberSeconds to wait for messages (default: 5)
maxMessagesnumberMaximum number of messages to receive
ignoreAttachmentsbooleanSkip downloading attachments
ignoreStoriesbooleanSkip story messages
ignoreAvatarsbooleanSkip downloading avatars
ignoreStickersbooleanSkip downloading sticker packs
sendReadReceiptsbooleanAutomatically send read receipts

JsonRpcStartOptions (connect)

OptionTypeDescription
ignoreAttachmentsbooleanSkip downloading attachments for all received messages
ignoreStoriesbooleanSkip story messages for the entire session
ignoreAvatarsbooleanSkip downloading avatars for the entire session
ignoreStickersbooleanSkip downloading sticker packs for the entire session
sendReadReceiptsbooleanAuto-send read receipts for the entire session
receiveMode'on-start' | 'manual'When to start receiving messages

Configuration Reference

Environment variables

SIGNAL_PHONE_NUMBER="+33111111111" SIGNAL_ADMIN_NUMBER="+33222222222" SIGNAL_RECIPIENT_NUMBER="+33333333333" SIGNAL_GROUP_NAME="My Bot Group"

SignalCli constructor

new SignalCli(accountOrPath?, account?, config?)
ParameterDescription
accountOrPathPhone number (+33...) or path to a custom signal-cli binary
accountPhone number when the first argument is a path
configSignalCliConfig object (see below)

SignalCliConfig

OptionDefaultDescription
maxRetries3Number of retry attempts on failure
retryDelay1000Initial retry delay in milliseconds
maxConcurrentRequests10Maximum parallel JSON-RPC requests
minRequestInterval100Minimum delay between requests in milliseconds
requestTimeout60000Per-request timeout in milliseconds
connectionTimeout30000Connection attempt timeout in milliseconds
autoReconnecttrueAutomatically reconnect on unexpected disconnect
verbosefalseEnable debug logging
logFileundefinedWrite logs to a file path
daemonMode'json-rpc'Connection mode: json-rpc, unix-socket, tcp, http
socketPathundefinedPath to Unix socket (unix-socket mode)
tcpHost'localhost'TCP host (tcp mode)
tcpPort7583TCP port (tcp mode)
httpBaseUrl'http://localhost:8080'Base URL (http mode)

Testing

# Run all tests npm test # Run a specific suite npm test -- --testPathPattern="SignalCli.methods" # Run with coverage report npm test -- --coverage # Run in watch mode npm test -- --watch

Test statistics

MetricValue
Total tests442 passing
Test suites22
Overall coverage~84%

Coverage by module

ModuleStatementsBranchesFunctionsLines
errors.ts100%100%100%100%
validators.ts100%100%100%100%
config.ts100%97%100%100%
retry.ts97%85%100%98%
BaseManager.ts100%100%100%100%
AccountManager.ts91%81%88%95%
ContactManager.ts82%66%100%98%
DeviceManager.ts96%81%91%96%
GroupManager.ts78%68%83%94%
MessageManager.ts84%78%90%85%
StickerManager.ts93%83%100%93%
SignalCli.ts83%71%80%85%
SignalBot.ts73%59%66%73%
MultiAccountManager.ts88%74%83%90%

Test suites

SuiteFocus
errors.test.tsError class hierarchy and serialization
validators.test.tsPhone number, UUID, and input validation
config.test.tsConfiguration validation and defaults
retry.test.tsRetry logic and exponential backoff
security.test.tsInput sanitization and injection prevention
robustness.test.tsEdge cases and failure scenarios
SignalCli.test.tsCore connection and messaging
SignalCli.methods.test.tsFull API method coverage
SignalCli.advanced.test.tsAdvanced send options, receive, identity
SignalCli.integration.test.tsConnection lifecycle and JSON-RPC parsing
SignalCli.simple.test.tsisRegistered, sendNoteToSelf
SignalCli.parsing.test.tsEnvelope parsing and event emission
SignalCli.events.test.tsReaction, receipt, typing events
SignalCli.connections.test.tsUnix socket, TCP, HTTP daemon modes
SignalCli.e2e.test.tsEnd-to-end multi-step workflows
SignalCli.v0140.test.tssendPinMessage, sendUnpinMessage, sendAdminDelete, noUrgent, ignoreAvatars, ignoreStickers, JsonRpcStartOptions
DeviceManager.test.tsDevice listing, linking, renaming
MultiAccountManager.test.tsMulti-account management
MultiAccountManager.coverage.test.tsEdge cases for multi-account
SignalBot.test.tsBot startup, commands, events
SignalBot.additional.test.tsExtended bot features
SignalBot.coverage.test.tsBot edge cases and error handling

Troubleshooting

signal-cli not found / ENOENT error

The postinstall script downloads signal-cli automatically. If it failed (e.g. no internet during npm install), re-run it:

node node_modules/signal-sdk/scripts/install.js

Or download manually from the signal-cli releases page and place the binary at node_modules/signal-sdk/bin/signal-cli.

Java not found (macOS / Windows)

The JVM-based signal-cli distribution requires Java 17 or later:

# macOS brew install openjdk@21 # Ubuntu / Debian sudo apt install openjdk-21-jre # Verify java -version

Phone number not registered

./node_modules/signal-sdk/bin/signal-cli -a +33111111111 register ./node_modules/signal-sdk/bin/signal-cli -a +33111111111 verify 123-456

Connection timeout

Test signal-cli directly to confirm the account works:

./node_modules/signal-sdk/bin/signal-cli -a +33111111111 send -m "test" +33222222222

Permission denied on config directory

chmod -R 755 ~/.local/share/signal-cli/

Rate limit error

signal-cli returns exit code 5 when rate-limited. Use submitRateLimitChallenge() to resolve it:

await signal.submitRateLimitChallenge(challengeToken, captchaToken);

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Make your changes and add tests
  4. Ensure all tests pass (npm test)
  5. Submit a pull request

See CONTRIBUTING.md for detailed guidelines.


License

MIT — see LICENSE for details.


Acknowledgments

  • signal-cli by AsamK — the underlying Signal command-line client
  • Signal Protocol — end-to-end encrypted messaging protocol
  • The Signal open-source community

Support

If you find this project useful, consider supporting its development:

Donate on Liberapay


Made with ❤️ for the Signal community

Repository Topics
#bot#messaging#signal#signal-sdk