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

FieldTypeRequiredDefaultNotes
userIdstringyesCanonical account identifier
deviceIdnumberno11 is primary; 25 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: 25. See multi-device.

adapters

FieldTypeRequiredNotes
storageISignalProtocolLocalStoreyesDevice-local protocol state
relayISignalProtocolRelayServernoOmit for local-only mode; required for sync, prekeys, delivery
remoteObjectStoreSignalProtocolRemoteObjectStorenoRequired only for encrypted attachments
protocolManagerISignalProtocolManagernoAdvanced override for tests and specialised 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
loggerILoggerenvironment-aware consoleAll methods optional; console works directly
enableDebugLoggingbooleanfalseVerbose protocol logging
throwDetailedErrorsbooleanfalseDetailed messages instead of generic ones
hooksSignalProtocolClientHooksLifecycle callbacks; see Hooks
onProgressProgressCallback{ stage, percent, message, detail? } during init and sync
mediaSignalProtocolClientMediaConfigApp-owned attachment byte callbacks
contentAdapterSignalProtocolContentAdapterdefault adapterBoundary between protocol and app content shape
ratchetConfigDoubleRatchetConfigmaxSkip 1000, maxMessageKeysStored 1000, keyExpirationMs 604800000Double Ratchet bounds
senderKeysSenderKeysConfigGroup HKDF info string and DoS limits
protocolStrategyProtocolStrategyConfigDiagnostics and telemetry seam; prefer protocol
onPreKeyLow(remaining: number) => voidFires 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
preKeyMaintenancePreKeyMaintenanceStoreReplaced-prekey bookkeeping for SQLite adapters
onGroupSenderKeyRotated(groupId, newGeneration) => voidSender-key rotation notification
sealedSenderSealedSenderConfigtrustRoots, certificateProvider?, accessMode?, contactStateStore?
groupsV2objectstore, 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

PropertyTypeNotes
loggerRequired<ILogger>Resolved logger, never undefined
userIdstringGetter
deviceIdnumber1 primary, 25 linked
mediaSignalProtocolClientMediaThe one namespace-shaped property
syncStatus'synced' | 'failed' | 'none'Result of the initial sync during create(); 'none' means no relay was configured
isSealedSenderEnabledbooleanGetter

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

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 }>
rotateEcSignedPreKey()Promise<boolean>
rotateKyberPreKey()Promise<boolean>

Replenishment itself happens inside syncToServer(). 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 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.

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
/blockingSignalProtocolBlockingManagerblockRecipient, unblockRecipient, isBlocked, listBlockedRecipients, applySyncSnapshot
/profileencryptProfileName, decryptProfileName, getOrCreateOwnProfileKey, setProfileKeyStorage, updateEncryptedProfile
/username, /username/linkformatUsername, hashUsername, parseUsername; createUsernameLink, decryptUsernameLink
/groupsGroupsV2Manager, createGroupId, type IGroupServer, type IGroupStateStore
/zk/groups, /zk/credentialscomputeProfileKeyVersion, deriveGroupSecretParams, getGroupPublicParams; credential-key serialisation
/hooksuseSessionHealth({ signal, userId }), useConnectionPresence({ relay, deviceId, enabled?, logger? }), useKeyRotation, 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

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

On this page