signal-sdk
repoA 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.
Table of Contents
- Features
- Installation
- Prerequisites
- Device Linking
- Basic Usage
- Advanced Configuration
- Common Use Cases
- SignalBot Framework
- API Reference
- Configuration Reference
- Testing
- Troubleshooting
- Contributing
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
/helpand/pingcommands - 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
| Category | Method | Description |
|---|---|---|
| Connection | connect(options?) | Start JSON-RPC daemon with optional startup flags |
disconnect() | Close the connection immediately | |
gracefulShutdown() | Wait for process to exit cleanly | |
| Messaging | sendMessage(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 | |
| Groups | createGroup(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 | |
| Contacts | listContacts() | 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 | |
| Identity | listIdentities(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 | |
| Account | register(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 | |
| Devices | listDevices() | 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 | |
| Stickers | listStickerPacks() | List installed sticker packs |
addStickerPack(packId, packKey) | Install a sticker pack | |
uploadStickerPack(manifest) | Upload a custom sticker pack | |
getSticker(options) | Retrieve sticker data | |
| Attachments | getAttachment(options) | Retrieve an attachment by ID |
getAvatar(options) | Retrieve a contact or group avatar | |
| Sync | sendSyncRequest() | Request sync from primary device |
sendMessageRequestResponse(recipient, response) | Respond to a message request | |
getVersion() | Get signal-cli version info |
SendMessageOptions
| Option | Type | Description |
|---|---|---|
attachments | string[] | File paths to attach |
mentions | Mention[] | Mentions in the message body |
textStyles | TextStyle[] | Text formatting (BOLD, ITALIC, SPOILER, STRIKETHROUGH, MONOSPACE) |
quote | QuoteOptions | Reply to an existing message |
expiresInSeconds | number | Message expiration timer |
isViewOnce | boolean | View-once message |
editTimestamp | number | Timestamp of the message to edit |
storyTimestamp | number | Timestamp of a story to reply to |
storyAuthor | string | Author of the story to reply to |
previewUrl | string | URL for link preview |
previewTitle | string | Title for link preview |
previewDescription | string | Description for link preview |
previewImage | string | Image path for link preview |
noteToSelf | boolean | Send to own account |
endSession | boolean | End the session |
noUrgent | boolean | Send without push notification |
PinMessageOptions
| Option | Type | Description |
|---|---|---|
targetAuthor | string | Author of the message to pin |
targetTimestamp | number | Timestamp of the message to pin |
groupId | string | Target group (mutually exclusive with recipients) |
recipients | string[] | Target recipients for a direct pin |
noteToSelf | boolean | Pin in your own conversation |
pinDuration | number | Duration in seconds, -1 for forever (default) |
notifySelf | boolean | Send as normal message if self is a recipient |
AdminDeleteOptions
| Option | Type | Description |
|---|---|---|
groupId | string | Group in which to delete the message (required) |
targetAuthor | string | Author of the message to delete |
targetTimestamp | number | Timestamp of the message to delete |
story | boolean | Delete a story instead of a regular message |
notifySelf | boolean | Send as normal message if self is a recipient |
ReceiveOptions
| Option | Type | Description |
|---|---|---|
timeout | number | Seconds to wait for messages (default: 5) |
maxMessages | number | Maximum number of messages to receive |
ignoreAttachments | boolean | Skip downloading attachments |
ignoreStories | boolean | Skip story messages |
ignoreAvatars | boolean | Skip downloading avatars |
ignoreStickers | boolean | Skip downloading sticker packs |
sendReadReceipts | boolean | Automatically send read receipts |
JsonRpcStartOptions (connect)
| Option | Type | Description |
|---|---|---|
ignoreAttachments | boolean | Skip downloading attachments for all received messages |
ignoreStories | boolean | Skip story messages for the entire session |
ignoreAvatars | boolean | Skip downloading avatars for the entire session |
ignoreStickers | boolean | Skip downloading sticker packs for the entire session |
sendReadReceipts | boolean | Auto-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?)
| Parameter | Description |
|---|---|
accountOrPath | Phone number (+33...) or path to a custom signal-cli binary |
account | Phone number when the first argument is a path |
config | SignalCliConfig object (see below) |
SignalCliConfig
| Option | Default | Description |
|---|---|---|
maxRetries | 3 | Number of retry attempts on failure |
retryDelay | 1000 | Initial retry delay in milliseconds |
maxConcurrentRequests | 10 | Maximum parallel JSON-RPC requests |
minRequestInterval | 100 | Minimum delay between requests in milliseconds |
requestTimeout | 60000 | Per-request timeout in milliseconds |
connectionTimeout | 30000 | Connection attempt timeout in milliseconds |
autoReconnect | true | Automatically reconnect on unexpected disconnect |
verbose | false | Enable debug logging |
logFile | undefined | Write logs to a file path |
daemonMode | 'json-rpc' | Connection mode: json-rpc, unix-socket, tcp, http |
socketPath | undefined | Path to Unix socket (unix-socket mode) |
tcpHost | 'localhost' | TCP host (tcp mode) |
tcpPort | 7583 | TCP 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
| Metric | Value |
|---|---|
| Total tests | 442 passing |
| Test suites | 22 |
| Overall coverage | ~84% |
Coverage by module
| Module | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
errors.ts | 100% | 100% | 100% | 100% |
validators.ts | 100% | 100% | 100% | 100% |
config.ts | 100% | 97% | 100% | 100% |
retry.ts | 97% | 85% | 100% | 98% |
BaseManager.ts | 100% | 100% | 100% | 100% |
AccountManager.ts | 91% | 81% | 88% | 95% |
ContactManager.ts | 82% | 66% | 100% | 98% |
DeviceManager.ts | 96% | 81% | 91% | 96% |
GroupManager.ts | 78% | 68% | 83% | 94% |
MessageManager.ts | 84% | 78% | 90% | 85% |
StickerManager.ts | 93% | 83% | 100% | 93% |
SignalCli.ts | 83% | 71% | 80% | 85% |
SignalBot.ts | 73% | 59% | 66% | 73% |
MultiAccountManager.ts | 88% | 74% | 83% | 90% |
Test suites
| Suite | Focus |
|---|---|
errors.test.ts | Error class hierarchy and serialization |
validators.test.ts | Phone number, UUID, and input validation |
config.test.ts | Configuration validation and defaults |
retry.test.ts | Retry logic and exponential backoff |
security.test.ts | Input sanitization and injection prevention |
robustness.test.ts | Edge cases and failure scenarios |
SignalCli.test.ts | Core connection and messaging |
SignalCli.methods.test.ts | Full API method coverage |
SignalCli.advanced.test.ts | Advanced send options, receive, identity |
SignalCli.integration.test.ts | Connection lifecycle and JSON-RPC parsing |
SignalCli.simple.test.ts | isRegistered, sendNoteToSelf |
SignalCli.parsing.test.ts | Envelope parsing and event emission |
SignalCli.events.test.ts | Reaction, receipt, typing events |
SignalCli.connections.test.ts | Unix socket, TCP, HTTP daemon modes |
SignalCli.e2e.test.ts | End-to-end multi-step workflows |
SignalCli.v0140.test.ts | sendPinMessage, sendUnpinMessage, sendAdminDelete, noUrgent, ignoreAvatars, ignoreStickers, JsonRpcStartOptions |
DeviceManager.test.ts | Device listing, linking, renaming |
MultiAccountManager.test.ts | Multi-account management |
MultiAccountManager.coverage.test.ts | Edge cases for multi-account |
SignalBot.test.ts | Bot startup, commands, events |
SignalBot.additional.test.ts | Extended bot features |
SignalBot.coverage.test.ts | Bot 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
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes and add tests
- Ensure all tests pass (
npm test) - 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
- Issues: GitHub Issues
- Examples: examples/
- Docs: docs/
If you find this project useful, consider supporting its development:
Made with ❤️ for the Signal community
