OpenE2EE

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 6.0.x. Public APIs and persisted formats follow semantic versioning.

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.

FieldTypeRequiredDefaultNotes
userIdstringyes—Canonical account identifier
deviceIdnumberno11 is primary; 2–5 are backend-allocated linked slots
enablePniKeysbooleannofalseGenerate 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

FieldTypeRequiredNotes
storageSignalProtocolLocalStoreyesDevice-local protocol state
relaySignalProtocolRelayServernoOmit for local-only mode; required for sync, prekeys, delivery
remoteObjectStoreSignalProtocolRemoteObjectStorenoRequired only for encrypted attachments
protocolManagerSignalProtocolManagernoAdvanced override for tests and specialized integrations

Interfaces and their obligations are in Adapter interfaces.

protocol

FieldTypeDefaultNotes
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.

FieldTypeDefaultPurpose
loggerLoggerenvironment-aware consoleAll methods optional; console works directly
enableDebugLoggingbooleanfalseVerbose protocol logging
throwDetailedErrorsbooleanfalseDetailed messages instead of generic ones
hooksSignalProtocolClientHooks—Lifecycle callbacks; see Hooks
onProgressProgressCallback—{ stage, percent, message, detail? } during init and sync
mediaSignalProtocolClientMediaConfig—App-owned attachment byte callbacks
contentAdapterSignalProtocolContentAdapterdefault adapterBoundary between protocol and app content shape
ratchetConfigDoubleRatchetConfigmaxSkip 1000, maxMessageKeysStored 1000, keyExpirationMs 604800000Double Ratchet bounds
senderKeysSenderKeysConfig—Group HKDF info string and DoS limits
protocolStrategyProtocolStrategyConfig—Diagnostics and telemetry seam; prefer protocol
onPreKeyLow(remaining: number) => void—Fires below preKeyLowThreshold
preKeyLowThresholdnumber50Product-facing low-watermark
keyRefreshIntervalMsnumber172800000 (2 days)Signed and Kyber prekey rotation interval
maxPreKeyAgeMsnumber1209600000 (14 days)Sending is blocked above this age
preKeyCheckThrottleMsnumber43200000 (12 hours)Activation-check throttle
preKeyMaintenancePreKeyMaintenanceStore—Replaced-prekey bookkeeping for SQLite adapters
onGroupSenderKeyRotated(groupId, newGeneration) => void—Sender-key rotation notification
sealedSenderSealedSenderConfig—trustRoots, certificateProvider?, accessMode?, contactStateStore?
groupsV2object—store, server, credentialPublicKey, aci, and optional pni, endorsementRootPublicKey, endorsementManager, resolveAciBytesByUserIds

Lower-level factory

DefaultSignalProtocolClient.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

PropertyTypeNotes
loggerRequired<Logger>Resolved logger, never undefined
userIdstringGetter
deviceIdnumber1 primary, 2–5 linked
mediaSignalProtocolClientMediaThe one namespace-shaped property
syncStatus'synced' | 'failed' | 'none'Result of the initial sync during create(); 'none' means no relay was configured
isSealedSenderEnabledbooleanGetter
relayConnectionStateRelayConnectionStateGetter; stopped before startRelaySubscription(), after stopRelaySubscription(), and when no relay is configured

Methods

Session and lifecycle

MethodReturns
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

MethodReturns
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

MethodReturns
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

MethodReturns
markAsRead(messageId)Promise<void>
sendReadReceipt(recipientUserId, timestamps)Promise<void>
sendViewedReceipt(recipientUserId, timestamps)Promise<void>

Relay and subscriptions

MethodReturns
startRelaySubscription()void
stopRelaySubscription()void
subscribeRelayConnectionState(listener)Unsubscribe
startRetryRequestSubscription()void
stop()Promise<void>

stop() tears down every subscription and in-flight timer. Call it before the process or screen goes away.

relayConnectionState is the state of the relay subscription on this device. Its state is stopped, connecting, connected, or reconnecting. since is the Unix time in milliseconds of the transition. On reconnecting, reason names the site that failed: handshake, protocol, closed, error, frame, authentication, or silent. The reason is never an error message. The state is local to this device and is not presence.

subscribeRelayConnectionState(listener) calls the listener once for each new state. It does not call the listener with the current state, and it never sends the same value twice. A token renewal on a live connection is not a transition.

Linked-device sync

Each of these sends a sync message to the account's other devices. All return Promise<void>.

MethodInput
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

MethodReturns
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

MethodReturns
checkPreKeyStatus()Promise<{ oneTimePreKeysRemaining, needsReplenishment }>
rotatePreKeys()Promise<PreKeyRotationResult>: signedRotated, kyberRotated, oneTimeReplenished, errors

Replenishment happens inside syncToServer() and rotatePreKeys(). See key rotation.

Sender-key groups

MethodReturns
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

MethodReturns
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

MethodReturns
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 });
});
HookSignature
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 useSessionHealth, useKeyRotation, 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.

MethodSignature
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.

SubpathPrincipal exports
/keysgenerateIdentityKeyPair, generateEcSignedPreKey, createCompositeIdentityV1, type CompositeIdentityV1, type IdentityTrustState
/safetygenerateCompositeSafetyNumber, compareSafetyNumbers, isValidSafetyNumber, ScannableFingerprint, verify-link helpers
/device/provisioninggenerateProvisioningQR, parseProvisioningQR, provisionDevice, connectToProvisioningSession, receiveProvisioningMessage, getDeviceMetadata, cancelProvisioning
/deviceprepareNewDeviceTransfer, prepareOldDeviceTransferWithBackup, createDeviceBackup, restoreDeviceBackup, encryptDeviceName, MAX_DEVICES
/device/device-idgetDeviceId, getDeviceIdSync, preloadDeviceId, clearDeviceIdCache
/device/lifecycleDeviceLifecycleManager, getLocalDeviceMetadata
/sealed-senderderiveAccessKey, ACCESS_KEY_BYTES
/blockingSignalProtocolBlockingManager — blockRecipient, unblockRecipient, isBlocked, listBlockedRecipients, applySyncSnapshot
/profileencryptProfileName, decryptProfileName, getOrCreateOwnProfileKey, setProfileKeyStorage, updateEncryptedProfile
/username, /username/linkformatUsername, hashUsername, parseUsername; createUsernameLink, decryptUsernameLink
/groupsGroupsV2Manager, createGroupId, type GroupServer, type GroupStateStore
/zk/groups, /zk/credentialscomputeProfileKeyVersion, deriveGroupSecretParams, getGroupPublicParams; credential-key serialization
/clientbindRelayLifecycle(signal, appState, { keepOpenInBackground? })
/hooksuseSessionHealth({ signal, userId }), useKeyRotation({ signal?, rateLimitMs?, enabled?, onRotationComplete?, onRotationError? }), useKeyRotationWithControls, useRelayConnectionState({ signal }), useRelayLifecycle({ signal, keepOpenInBackground? }), useGroupMembership, useSingleFlight
/filesstreamingEncrypt, streamingDecrypt, DEFAULT_SEGMENT_SIZE, secureZeroBytes
/encoding, /encoding/hexbytesToBase64, base64ToBytes, bytesToHex, hexToBytes
/utils/retrywithRetry, isRetryableError — see Errors
/logger, /server-clockcreateDefaultSignalProtocolLogger, resolveSignalProtocolLogger; recordServerClockSample, estimateServerTimestamp

useSessionHealth returns { health, isLoading, error, refresh }.

useRelayConnectionState returns the current RelayConnectionState and renders the component again on each transition. useRelayLifecycle passes React Native's AppState to bindRelayLifecycle and does nothing on web. bindRelayLifecycle stops the relay subscription when the app goes to the background state, and starts it again when the app becomes active. See React Native app state.

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 6.0.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

On this page