API reference
The curated client surface — createSignalProtocolClient options, every SignalProtocolClient method by category, properties, hooks, and the standalone modules.
Applies to @open-e2ee/signal-protocol-sdk 0.1.x. Public APIs and persisted formats may change before 1.0.
This page is the curated surface: the shapes you compose against, grouped so you can scan them. It is not the generated reference. For exhaustive signatures and type parameters, see the TypeDoc output at docs/api/README.md. It also lists every exported symbol. The TypeDoc generator builds that output from exported declarations.
There are no client.device.* namespaces
The only namespace-shaped property on the client is client.media. There is no client.device, no client.keys, no client.safety, and no client.sealedSender. Everything else is either a flat method on the client or a standalone module at its own package subpath.
client.safety.generate(...) and client.device.link(...) do not exist. Use client.verify(userId) and the /device/provisioning module respectively.
Device linking, key generation primitives, safety-number rendering data, and sealed-sender access-key derivation all live outside the client object. See Standalone modules below and Package subpaths.
createSignalProtocolClient()
import { createSignalProtocolClient } from '@open-e2ee/signal-protocol-sdk';
const signal = await createSignalProtocolClient({
identity: { userId },
adapters: { storage, relay },
});Returns Promise<SignalProtocolClient>. Initialization is complete when the promise resolves.
identity
Required. One client instance represents one account on one device.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
userId | string | yes | — | Canonical account identifier |
deviceId | number | no | 1 | 1 is primary; 2–5 are backend-allocated linked slots |
enablePniKeys | boolean | no | false | Generate and sync both ACI and PNI key material |
Provision identity material in the supplied storage before you create a linked-device client with deviceId: 2–5. See multi-device.
adapters
| Field | Type | Required | Notes |
|---|---|---|---|
storage | ISignalProtocolLocalStore | yes | Device-local protocol state |
relay | ISignalProtocolRelayServer | no | Omit for local-only mode; required for sync, prekeys, delivery |
remoteObjectStore | SignalProtocolRemoteObjectStore | no | Required only for encrypted attachments |
protocolManager | ISignalProtocolManager | no | Advanced override for tests and specialised integrations |
Interfaces and their obligations are in Adapter interfaces.
protocol
| Field | Type | Default | Notes |
|---|---|---|---|
postQuantum | 'required' | 'compatible' | 'required' | 'compatible' is explicit opt-in and does not allow downgrade recovery |
braid | 'required' | 'disabled' | 'required' | 'disabled' selects the local direct ML-KEM SPQR mode |
There is no public postQuantum: 'disabled' mode. The root package exports constants as PostQuantumPolicy and BraidPolicy. Full semantics: Security and protocol policy.
Top-level options
Everything below sits at the top level of the composition object, alongside identity, adapters, and protocol.
| Field | Type | Default | Purpose |
|---|---|---|---|
logger | ILogger | environment-aware console | All methods optional; console works directly |
enableDebugLogging | boolean | false | Verbose protocol logging |
throwDetailedErrors | boolean | false | Detailed messages instead of generic ones |
hooks | SignalProtocolClientHooks | — | Lifecycle callbacks; see Hooks |
onProgress | ProgressCallback | — | { stage, percent, message, detail? } during init and sync |
media | SignalProtocolClientMediaConfig | — | App-owned attachment byte callbacks |
contentAdapter | SignalProtocolContentAdapter | default adapter | Boundary between protocol and app content shape |
ratchetConfig | DoubleRatchetConfig | maxSkip 1000, maxMessageKeysStored 1000, keyExpirationMs 604800000 | Double Ratchet bounds |
senderKeys | SenderKeysConfig | — | Group HKDF info string and DoS limits |
protocolStrategy | ProtocolStrategyConfig | — | Diagnostics and telemetry seam; prefer protocol |
onPreKeyLow | (remaining: number) => void | — | Fires below preKeyLowThreshold |
preKeyLowThreshold | number | 50 | Product-facing low-watermark |
keyRefreshIntervalMs | number | 172800000 (2 days) | Signed and Kyber prekey rotation interval |
maxPreKeyAgeMs | number | 1209600000 (14 days) | Sending is blocked above this age |
preKeyCheckThrottleMs | number | 43200000 (12 hours) | Activation-check throttle |
preKeyMaintenance | PreKeyMaintenanceStore | — | Replaced-prekey bookkeeping for SQLite adapters |
onGroupSenderKeyRotated | (groupId, newGeneration) => void | — | Sender-key rotation notification |
sealedSender | SealedSenderConfig | — | trustRoots, certificateProvider?, accessMode?, contactStateStore? |
groupsV2 | object | — | store, server, credentialPublicKey, aci, and optional pni, endorsementRootPublicKey, endorsementManager, resolveAciBytesByUserIds |
The SDK source contains a stale comment describing prekey rotation as weekly. The keyRefreshIntervalMs default of 172800000, two days, is authoritative. The same stale comment appears in the generated TypeDoc for rotateEcSignedPreKey() and rotateKyberPreKey().
Lower-level factory
SignalProtocolClient.create(userId, config) takes the flattened SignalProtocolClientConfig: storage, relay, deviceId, protocol, and the rest at one level. The constructor is private. createSignalProtocolClientConfig(options) converts the composition shape into that flattened config without constructing a client.
Properties
| Property | Type | Notes |
|---|---|---|
logger | Required<ILogger> | Resolved logger, never undefined |
userId | string | Getter |
deviceId | number | 1 primary, 2–5 linked |
media | SignalProtocolClientMedia | The one namespace-shaped property |
syncStatus | 'synced' | 'failed' | 'none' | Result of the initial sync during create(); 'none' means no relay was configured |
isSealedSenderEnabled | boolean | Getter |
Methods
Session and lifecycle
| Method | Returns |
|---|---|
isInitialized() | Promise<boolean> |
getIdentityPublicKey() | Promise<PublicKey> |
address() | ProtocolAddress |
syncToServer(onProgress?) | Promise<void> |
establishSession(remoteAddress, prekeyBundle, recipientIdentityType?) | Promise<void> |
hasSession(remoteAddress) | Promise<boolean> |
deleteSession(remoteAddress) | Promise<void> |
archiveSession(remoteAddress) | Promise<void> |
fetchSenderCertificate() | Promise<string> |
rotateAccountIdentity(expectedCurrentCommitment, identityType?) | Promise<void> |
forceCompleteKeyReset() | Promise<ForceKeyResetResult> |
stop() | Promise<void> |
Its JSDoc limits forceCompleteKeyReset() to development and debugging. Treat it as break-glass, not as a production recovery step. It clears and regenerates key material. Peers will see an identity change.
syncToServer() replenishes and uploads prekeys. The SDK normally establishes sessions implicitly. send() establishes what it needs, and establishSession() is for callers that already hold a bundle.
Messaging
| Method | Returns |
|---|---|
send(recipientId, content, options?) | Promise<SendResult> |
encryptMessage(remoteAddress, plaintext) | Promise<Ciphertext> |
decryptMessage(remoteAddress, ciphertext) | Promise<string> |
encryptMessages(remoteAddress, plaintexts) | Promise<Ciphertext[]> |
decryptMessages(remoteAddress, ciphertexts) | Promise<string[]> |
processIncomingEnvelope(envelope, options?) | Promise<string> |
processIncomingEnvelopes(envelopes, options?) | Promise<Array<{ envelope, plaintext } | { envelope, error }>> |
encryptFile(remoteAddress, fileBlob, mimeType?) | Promise<{ … }> |
decryptFile(remoteAddress, encryptedBlob, encryptedKey) | Promise<Blob> |
encryptFiles(remoteAddress, files) | Promise<Array<{ … }>> |
decryptFiles(remoteAddress, files) | Promise<Blob[]> |
uploadAttachment(data, options) | Promise<PreparedAttachmentUpload> |
downloadAttachment(attachment, options?) | Promise<DownloadedAttachment> |
deleteRemoteAttachment(attachment, options?) | Promise<void> |
content on send() is DataMessageInput | string | Uint8Array. processIncomingEnvelopes() resolves per-envelope: successes and failures come back in the same array rather than one rejection aborting the batch.
Trust and safety
| Method | Returns |
|---|---|
verify(userId, identityType?) | Promise<SafetyNumber> |
confirmSafetyNumber(confirmation) | Promise<void> |
acceptIdentityRotation(userId, identity, identityType?) | Promise<ContactIdentityRecord> |
There are two different SafetyNumber shapes and they do not merge.
client.verify() returns { numeric, fingerprint, userId, identityType, trustState, confirmation }: no emojis, no scannable.
generateCompositeSafetyNumber() from /safety returns { numeric, emojis, hex, qrData, scannable }: no confirmation.
signal.trustIdentity(...) does not exist. The SDK's own docs/ERROR_HANDLING.md is stale on this point. Use acceptIdentityRotation(). Details in identity change and safety numbers.
Read state
| Method | Returns |
|---|---|
markAsRead(messageId) | Promise<void> |
sendReadReceipt(recipientUserId, timestamps) | Promise<void> |
sendViewedReceipt(recipientUserId, timestamps) | Promise<void> |
Relay and subscriptions
| Method | Returns |
|---|---|
startRelaySubscription() | void |
stopRelaySubscription() | void |
startRetryRequestSubscription() | void |
stop() | Promise<void> |
stop() tears down every subscription and in-flight timer. Call it before the process or screen goes away.
Linked-device sync
Each of these sends a sync message to the account's other devices. All return Promise<void>.
| Method | Input |
|---|---|
syncReadToLinkedDevices(entries) | ReadSyncEntryInput[] |
syncViewOnceOpenToLinkedDevices(entry) | ViewOnceOpenSyncInput |
syncMediaAttachmentDeleteToLinkedDevices(entry) | MediaAttachmentDeleteSyncInput |
syncConfigurationToLinkedDevices(configuration) | ConfigurationSyncInput |
syncUsernameStateToLinkedDevices(usernameState) | UsernameStateSyncInput |
syncRecipientUsernameToLinkedDevices(recipientUsername) | RecipientUsernameSyncInput |
syncVerificationStateToLinkedDevices(verificationState) | VerificationStateSyncInput |
syncTaskNotificationAckToLinkedDevices(input) | TaskNotificationAckSyncInput without acknowledgedOnDevice |
syncBlockedRecipientsToLinkedDevices(blocked) | BlockedRecipientsSyncInput |
Typing and Sesame
| Method | Returns |
|---|---|
sendTypingIndicator(recipientUserId, recipientDeviceId, conversationId, action, groupId?) | Promise<void> |
receive(message) | Promise<string> |
getSesameStats() | Promise<SesameStats> |
cleanupExpiredSesameSessions() | Promise<number> |
action is a TypingAction. receive() takes a SesameMessage and returns the decrypted plaintext.
Prekeys
| Method | Returns |
|---|---|
checkPreKeyStatus() | Promise<{ oneTimePreKeysRemaining, needsReplenishment }> |
rotateEcSignedPreKey() | Promise<boolean> |
rotateKyberPreKey() | Promise<boolean> |
Replenishment itself happens inside syncToServer(). See key rotation.
Sender-key groups
| Method | Returns |
|---|---|
createGroupSenderKey(groupId) | Promise<{ … }> |
getGroupSenderKeyDistribution(groupId) | Promise<SenderKeyDistributionMessage | null> |
processGroupSenderKeyDistribution(groupId, senderId, senderDeviceId, message) | Promise<void> |
distributeSenderKeyToUser(groupId, recipientUserId) | Promise<void> |
distributeGroupSenderKey(groupId, memberUserIds) | Promise<void> |
encryptGroupMessage(groupId, plaintext) | Promise<Uint8Array> |
decryptGroupMessage(groupId, senderId, senderDeviceId, framedMessage) | Promise<string> |
rotateGroupSenderKey(groupId) | Promise<{ … }> |
deleteGroupSenderKey(groupId) | Promise<void> |
hasGroupSenderKey(groupId) | Promise<boolean> |
getGroupSenderKeyStats(groupId, senderId, senderDeviceId) | Promise<{ … }> |
handleGroupMembershipChange(groupId, change) | Promise<{ rotated, distributionMessage }> |
change is 'member_added' | 'member_removed' | 'metadata_changed'.
GroupsV2 encrypted group state
| Method | Returns |
|---|---|
createGroupV2(creatorAci, creatorProfileKey, members, title, options?) | Promise<{ … }> |
getGroupStateV2(groupId) | Promise<DecryptedGroup> |
syncGroupV2(groupId) | Promise<DecryptedGroup> |
addGroupMemberV2(groupId, editorAci, newMemberAci, newMemberProfileKey) | Promise<void> |
removeGroupMemberV2(groupId, editorAci, targetAci) | Promise<void> |
leaveGroupV2(groupId, userAci) | Promise<void> |
updateGroupTitleV2(groupId, editorAci, title) | Promise<void> |
updateGroupDescriptionV2(groupId, editorAci, description) | Promise<void> |
updateGroupAccessControlV2(groupId, editorAci, updates) | Promise<void> |
createGroupInviteLinkV2(groupId, editorAci) | Promise<string> |
joinGroupViaInviteLinkV2(url, userAci, userProfileKey) | Promise<{ groupId, status }> |
members is Array<{ aciBytes, profileKey }>. status is 'joined' | 'pending_approval'. GroupsV2 requires the groupsV2 config block. Both group APIs are real and coexist: see groups.
Misc
| Method | Returns |
|---|---|
getStats() | Promise<{ … }> |
getSessionHealth(userId) | Promise<SessionHealthResult> |
cleanupExpiredKeys(remoteAddress) | Promise<boolean> |
runPeriodicCleanup() | number |
clearAllData() | Promise<void> |
registerHook(name, callback) | void |
Hooks
registerHook(name, callback) attaches one lifecycle callback. Hook errors are caught internally and do not affect protocol operation, which also means a throwing hook fails silently: log inside it.
signal.registerHook('onMessageDecrypted', async (message) => {
await appMessages.insert({ senderId: message.senderId, body: message.content });
});| Hook | Signature |
|---|---|
onSessionEstablished | (sessionId, remoteAddress) |
onSessionDeleted | (sessionId) |
onSessionArchived | (sessionId) |
onKeyRotated | (keyType: 'ecSignedPreKey' | 'kemLastResortPreKey') |
onMessageEncrypted | (sessionId, counter) |
onMessageDecrypted | (envelope: DecryptedEnvelope) |
onDecryptionError | (sessionId, error) |
onEncryptionError | (sessionId, error) |
onKeysCleanedUp | (sessionId, removedCount) |
onDeliveryReceiptReceived | (senderId, timestamps) |
onReadReceiptReceived | (senderId, timestamps) |
onViewedReceiptReceived | (senderId, timestamps) |
onTypingIndicatorReceived | (senderId, conversationId, action) |
DecryptedEnvelope carries messageId, sessionId, senderId, senderDeviceId, conversationId, content, timestamp, serverTimestamp?, receivedAt, isGroup, and messageType?. You can supply the same set through the hooks config option.
These are not the React hooks at /hooks. Those are useConnectionPresence, useSessionHealth, and siblings. See Standalone modules.
client.media
A durable job facade over the configured local store. Jobs survive process restarts because store metadata persists the queue.
| Method | Signature |
|---|---|
upload | (input, options?) => Promise<SignalProtocolClientMediaUploadResult> |
download | (input, options?) => Promise<SignalProtocolClientMediaDownloadResult> |
cleanup | (input, options?) => Promise<SignalProtocolClientMediaCleanupResult> |
processPending | (options?) => Promise<SignalProtocolClientMediaProcessResult> |
Use status to discriminate results: 'completed', 'pending', 'failed', 'skipped'. This page does not reproduce input field shapes. Read them from the generated reference rather than guessing.
Media moves bytes if you supply the media config callbacks: loadLocalAttachment, saveUploadedAttachment, saveDownloadedAttachment, deleteLocalAttachment, and optionally syncDelete. Your app owns the bytes and the cache. Upload and download also require adapters.remoteObjectStore. See attachments and object storage.
Standalone modules
Everything the client does not expose as a method lives at an explicit subpath. Full list with export names: Package subpaths.
| Subpath | Principal exports |
|---|---|
/keys | generateIdentityKeyPair, generateEcSignedPreKey, createCompositeIdentityV1, type CompositeIdentityV1, type IdentityTrustState |
/safety | generateCompositeSafetyNumber, compareSafetyNumbers, isValidSafetyNumber, ScannableFingerprint, verify-link helpers |
/device/provisioning | generateProvisioningQR, parseProvisioningQR, provisionDevice, connectToProvisioningSession, receiveProvisioningMessage, getDeviceMetadata, cancelProvisioning |
/device | prepareNewDeviceTransfer, prepareOldDeviceTransferWithBackup, createDeviceBackup, restoreDeviceBackup, encryptDeviceName, MAX_DEVICES |
/device/device-id | getDeviceId, getDeviceIdSync, preloadDeviceId, clearDeviceIdCache |
/device/lifecycle | DeviceLifecycleManager, getLocalDeviceMetadata |
/sealed-sender | deriveAccessKey, ACCESS_KEY_BYTES |
/blocking | SignalProtocolBlockingManager — blockRecipient, unblockRecipient, isBlocked, listBlockedRecipients, applySyncSnapshot |
/profile | encryptProfileName, decryptProfileName, getOrCreateOwnProfileKey, setProfileKeyStorage, updateEncryptedProfile |
/username, /username/link | formatUsername, hashUsername, parseUsername; createUsernameLink, decryptUsernameLink |
/groups | GroupsV2Manager, createGroupId, type IGroupServer, type IGroupStateStore |
/zk/groups, /zk/credentials | computeProfileKeyVersion, deriveGroupSecretParams, getGroupPublicParams; credential-key serialisation |
/hooks | useSessionHealth({ signal, userId }), useConnectionPresence({ relay, deviceId, enabled?, logger? }), useKeyRotation, useGroupMembership, useSingleFlight |
/files | streamingEncrypt, streamingDecrypt, DEFAULT_SEGMENT_SIZE, secureZeroBytes |
/encoding, /encoding/hex | bytesToBase64, base64ToBytes, bytesToHex, hexToBytes |
/utils/retry | withRetry, isRetryableError — see Errors |
/logger, /server-clock | createDefaultSignalProtocolLogger, resolveSignalProtocolLogger; recordServerClockSample, estimateServerTimestamp |
useConnectionPresence imports AppState from react-native and useConvex from convex/react. It is React Native plus Convex oriented, not general-purpose. useSessionHealth returns { health, isLoading, error, refresh }.
Roadmap language in the SDK repository
docs/GETTING_STARTED.md and docs/CLIENT_COMPOSITION.md in the SDK repository preview a "Target Message-First API" that does not exist in 0.1.x. If you find deviceStorage, signal.messages.send(), signal.messages.subscribe(), createInMemoryDeviceStorage(), or createExpoDeviceStorage() from /device/storage/expo, those are forward-looking sketches, not shipped API.
The real equivalents are adapters.storage, signal.send(), signal.registerHook() plus signal.startRelaySubscription(), and the store factories at /local/store/*.
Next
- Package subpaths: the complete exports map
- Adapter interfaces: what you must implement
- Errors: the taxonomy and retry rules
- Quickstart: the shortest working client
Reference
Exact surfaces rather than explanation — the client API, the adapter contracts, every importable subpath, the error taxonomy, runtime support, and the security policy.
Adapter interfaces
The four adapter slots — local store, relay, remote object store, protocol manager — with the guarantees each implementation must provide.