SDK reference
Every surface on one page: the four named surfaces under a connected client (ease.me, ease.auth, ease.passkey, ease.contacts, ease.chat), the top-level lifecycle, the room handles, and the core types. Signatures are shown in TypeScript notation; the API shape is identical on every SDK, only the language syntax differs. Ctrl-F is the index. Methods list the EaseError codes they can raise; the generic infrastructure codes (db_error and the http_ fallbacks) and access_token_invalid, which the SDK retries once with a refreshed token before surfacing, are omitted everywhere. The errors concept page holds the full catalogue with recovery guidance.
Ease (top level)
One Ease instance per application. connect() is tenant bootstrap, not user-scoped. The four named surfaces — me, auth, contacts, chat — hang off the instance, alongside the socket, session, and blob lifecycle methods.
connect(): Promise<TenantInfo>- Resolve tenant configuration from the bootstrap service. Required before any session-bearing call.Throws api_key_missing api_key_malformed api_key_invalid api_key_revoked api_key_env_mismatch
getTenant(): TenantInfo | null- Read the cached tenant info populated by connect(); null before connect() has resolved.
getSession(): Session | null- Read the active session; null before the user is signed in.
getSocketInfo(): SocketInfo | null- Read the current SocketInfo; null when no socket is open.
openSocket(): Promise<SocketInfo>- Open the realtime socket. Idempotent: returns the cached SocketInfo when already open.
closeSocket(): Promise<void>- Close the realtime socket. Idempotent.
sendPing(): Promise<PongInfo>- Send a keepalive ping on the open socket and return the server timestamp plus measured round-trip time.
onSocketClosed(handler: () => void): () => void- Subscribe to socket-close events; returns an unsubscribe function.
refreshSession(): Promise<Session>- Refresh the current session's access and refresh tokens, leaving protocol-store state unchanged.
maintainSession(onRefresh?: (s: Session) => void, signal?: AbortSignal): Promise<void>- Run an auto-refresh loop that refreshes shortly before the access token expires; stop it with an AbortSignal.
uploadBlob(opts: { roomId, data, mimeType, e2ee? }): Promise<BlobRef>- Encrypt bytes client-side and upload them to blob storage, returning a BlobRef the sender embeds in a typed event. e2ee defaults to true.
downloadBlob(ref: BlobRef): Promise<Uint8Array>- Fetch and, for E2EE refs, decrypt a blob; throws blob_decrypt_failed on a failed auth tag.
static get savedSession(): Session | null- The last Session this device persisted, readable before constructing an Ease instance to decide whether to skip the login screen.
static clearSavedSession(): void- Wipe the persisted session manually; signOut already does this.
ease.me
The user-self surface: what the signed-in user is — profile and avatar. Reads and writes delegate to the server.
getProfile(): Promise<MyProfile>- Read the authenticated user's profile from the server.
updateProfile(update: ProfileUpdate): Promise<void>- Update mutable profile fields.
getAvatar(): Promise<Uint8Array | null>- Read the authenticated user's avatar bytes, if any.
setAvatar(input: Blob | File | Uint8Array): Promise<Uint8Array>- Upload a new avatar and return the canonical bytes the server stored.
clearAvatar(): Promise<void>- Remove the user's avatar. Idempotent.
devices.list(): Promise<Device[]>- The linked-devices management surface: every active device with its platform, addedAt, and lastSeenAt. Pair with revoke for a Settings-style 'Linked Devices' screen.
devices.current(): Promise<Device>- The calling device's own row — use its deviceId to mark 'this device' and to hide its revoke control.
devices.revoke(deviceId: string): Promise<void>- Revoke (unlink) another device. The server refuses to revoke the primary or the calling device.
devices.revokeAllOthers(): Promise<void>- Sign out everywhere else — revoke every device except this one.
devices.renameThis(input: { name?: string; platform?: DevicePlatform }): Promise<void>- Rename the calling device (self only).
devices.onLinked(fn: (e: DeviceLinkedEvent) => void): () => void- Subscribe to device-linked events on the user's existing devices; the newly-linked device itself does not receive this. Returns an unsubscribe handle.
devices.onRevoked(fn: (e: DeviceRevokedEvent) => void): () => void- Subscribe to device-revoked events. If the revoked id is the local device, the SDK has already cleared local session and encryption state by the time the listener runs.
ease.auth
How a user enters or leaves the system: sign-up, login, OTP, passkey, sign-out, session restore, and the QR device-link flow.
sendSignupOtp(email: string): Promise<void>- Send a one-time signup code to email, delivered out-of-band for the caller to pass back to signUp. Email-password tenants only.Throws not_connected email_invalid auth_method_mismatch tenant_email_not_configured otp_rate_limited email_send_failed
sendSignupOtpByPhone(phone: string): Promise<void>- Phone variant of sendSignupOtp; an email tenant returns auth_method_mismatch.Throws not_connected phone_invalid auth_method_mismatch tenant_sms_not_configured otp_rate_limited sms_send_failed
verifySignupOtp(email: string, otp: string): Promise<void>- Optional pre-check that an OTP is valid before the rest of the signup form is collected; signUp still re-checks and consumes the code.Throws not_connected email_invalid otp_invalid otp_expired otp_attempts_exceeded
verifySignupOtpByPhone(phone: string, otp: string): Promise<void>- Phone variant of verifySignupOtp.
signUp(email, password, otp, options?): Promise<Session>- Create a new user with email, password, and the delivered OTP; options carries username and name. Returns a Session.Throws not_connected email_invalid otp_invalid otp_expired otp_attempts_exceeded password_weak email_taken username_taken username_invalid name_required
signUpByPhone(phone, password, otp, options?): Promise<Session>- Phone variant of signUp; options carries username and name.
login(email: string, password: string): Promise<Session>- Log in with email and password. Returns a Session.Throws not_connected email_invalid credentials_invalid bad_passphrase
loginByPhone(phone: string, password: string): Promise<Session>- Log in with phone and password. Returns a Session.
registerWithPasskey(handle: string, otp: string, options?): Promise<Session>- Register a new user with a WebAuthn passkey instead of a password; handle is an email or phone routed by the tenant auth_method. Requires the PRF extension and throws passkey_prf_unavailable without it.
loginWithPasskey(handle: string): Promise<Session>- Log in an existing user with a WebAuthn passkey; auth succeeds on a valid assertion, while messaging additionally needs the keystore from a prior registration on this browser.
restoreSession(session: Session): Promise<Session>- Restore an existing session loaded from secure storage.Throws not_connected not_signed_in refresh_token_invalid
signOut(): Promise<void>- Clear the current session. Idempotent.
startDeviceLink(platform: string, name?: string): Promise<DeviceLinkStart>- New device, step 1: the SDK generates a libsignal bundle internally and returns the pairing payload to render as a QR; hold the returned claimSecret for the complete step and never display or log it.
getDeviceLinkStatus(claimId: string): Promise<DeviceLinkStatus>- New device, step 3: poll the claim's status, proceeding to redeem once it flips to approved.
lookupDeviceLink(pairingCode: string): Promise<DeviceLinkPreview>- Primary device, pre-approval preview: read-only lookup of a pending claim by its pairing code, returning the new device's self-reported name and platform plus timestamps so the UI can confirm what's being linked before approving. Never mutates the claim.
approveDeviceLink(pairingCode: string): Promise<void>- Primary device, step 2: approve a new-device link by its pairing code, flipping the claim row to approved. Pair with lookupDeviceLink to confirm the device first.
completeDeviceLink(opts: { claimId, claimSecret, platform, name, passcode }): Promise<Session>- New device, final step: redeem the approved claim and bring the local libsignal store online; the passcode encrypts the keystore at rest and is sensitive. Web supports the passcode variant.
onDeviceLinked(handler: (e: DeviceLinkedEvent) => void): () => void- Subscribe to device-link-completed events on the user's existing devices; the newly-linked device itself does not receive this. Returns an unsubscribe handle.
ease.passkey
The WebAuthn PRF extension exposed as a building block, distinct from passkey auth. A passkey can derive stable, audience-scoped secrets for client-side key wrapping.
deriveSecret(audience: string): Promise<Uint8Array>- Derive a deterministic 32-byte secret from the signed-in user's passkey, scoped to audience; triggers a WebAuthn assertion on every call and rejects the reserved keystore audience.
prfAvailable(): Promise<boolean>- Report whether the WebAuthn PRF extension is available in this browser; a false is conservative and never triggers a user prompt.
ease.contacts
The graph of who the user knows. Gates DM eligibility, group membership, and approval flows, and carries the presence and contact-lifecycle event subscriptions.
list(): Promise<Contact[]>- List the user's established contacts.
sendRequest(target: ContactTarget, welcomeMessage?: string): Promise<SendContactRequestResult>- Send a contact request by userId, email, or phone; a welcomeMessage, when provided, is libsignal-encrypted to the recipient.Throws not_connected not_signed_in identity_missing welcome_too_long self_contact_invalid user_not_found invalid_handle lookup_kind_not_supported_by_tenant already_contacts already_blocked too_many_pending_requests recipient_unmessageable
incomingRequests(): Promise<IncomingContactRequest[]>- List pending requests received.
outgoingRequests(): Promise<OutgoingContactRequest[]>- List pending requests sent.
accept(requestId: string): Promise<string>- Accept a contact request and return the new contact's user ID.Throws not_connected not_signed_in identity_missing request_not_found not_recipient welcome_not_decrypted
decline(requestId: string): Promise<void>- Decline a contact request.
rescind(requestId: string): Promise<void>- Rescind a contact request the user previously sent.
remove(userId: string): Promise<void>- Remove an established contact.
block(userId: string): Promise<void>- Block a user from contacting the authenticated user.
unblock(userId: string): Promise<void>- Unblock a previously blocked user.
lookup(handle: { email } | { phone } | { username }): Promise<{ userId: string }>- Resolve a contact handle to a user ID; throws not_found when the handle maps to no discoverable user.Throws not_connected not_signed_in invalid_handle lookup_kind_not_supported_by_tenant user_not_found rate_limited
checkHandle(handle: { email } | { phone }): Promise<boolean>- Check whether a handle maps to a discoverable user without surfacing the user ID; true when taken.
checkUsername(username: string): Promise<boolean>- Check username availability for signup; true when the username is available, false when taken.
getProfile(userId: string): Promise<ContactProfile>- Read a specific contact's profile.
getAvatar(userId: string): Promise<Uint8Array | null>- Read a specific contact's avatar bytes, if any.
setLocalName(userId: string, name: string | null): Promise<void>- Set or clear the local-only display name for a contact; pass null to fall back to the contact's own displayName.
onPresenceUpdate(fn: (e: PresenceEvent) => void): () => void- Subscribe to per-contact presence-change deltas.
onPresenceSnapshot(fn: (snapshot: PresenceEvent[]) => void): () => void- Subscribe to full-list presence snapshots, fired on socket open and each reconnect for painting a complete who's-online view.
onContactRequestReceived(fn: (e: ContactRequestReceivedEvent) => void): () => void- Subscribe to incoming contact requests — someone wants to add you.
onContactRequestAccepted(fn: (e: ContactRequestAcceptedEvent) => void): () => void- Subscribe to your outgoing contact requests being accepted.
onContactRequestDeclined(fn: (e: ContactRequestDeclinedEvent) => void): () => void- Subscribe to your outgoing contact requests being declined.
onContactRemoved(fn: (e: ContactRemovedEvent) => void): () => void- Subscribe to events fired when a contact removes you, so the UI can drop them without polling.
ease.chat
The conversational surface: room factories for DMs and groups, cross-room verbs for creating and joining groups, group admin metadata, and the aggregate event subscriptions that fan across every room.
dm(userId: string): DmRoom- Open a DM room with another user; the room ID is derived from the recipient's user ID.
group(id: string): GroupRoom- Open a group room by its server-issued GRUP_ ID; throws invalid_room_id for a non-group ID.
createGroup(name: string, memberUserIds: string[], options?): Promise<Group>- Create a new group with the caller as owner; a duplicate-roster guard throws duplicate_roster, bypassable with options { force: true }.Throws not_connected not_signed_in name_required name_too_long group_too_small group_full member_set_invalid member_not_in_contacts duplicate_roster
listMyGroups(opts?: { limit? }): Promise<GroupListPage>- List the groups the user is a member of.
joinGroupByToken(token: string): Promise<Group>- Join an existing group by its QR or invite token, bypassing the contact gate and approval toggle; returns the post-join Group snapshot.Throws not_connected not_signed_in invite_token_invalid qr_join_disabled group_full
setGroupRequiresApproval(groupId: string, enabled: boolean): Promise<Group>- Set the group's requires-approval toggle (owner-only); when enabled, regular-member adds route through pending approval.
setGroupQrJoinEnabled(groupId: string, enabled: boolean): Promise<Group>- Owner/admin toggle for joining via the group QR / invite link; disabling hides the token, re-enabling restores the existing link.
setGroupAvatar(groupId: string, jpegBytes: Uint8Array): Promise<void>- Upload a group avatar (owner or admin only); bytes must be a JPEG within the tenant's per-group cap.
getGroupAvatar(groupId: string): Promise<Uint8Array | null>- Fetch a group's avatar bytes; null when no avatar is set.
clearGroupAvatar(groupId: string): Promise<void>- Remove a group's avatar (owner or admin only). Idempotent.
onIncomingMessage(fn: (msg: IncomingMessage) => void): () => void- Subscribe to every incoming decrypted message across all DMs and groups; returns an unsubscribe function.
onIncomingDecryptError(fn: (err: IncomingDecryptError) => void): () => void- Subscribe to the decrypt-error buffer, the silent-drop class an app surfaces as messages that couldn't be read.
onIncomingGroupMessage(fn: (msg: IncomingGroupMessage) => void): () => void- Subscribe to incoming group messages across all groups, carrying group context the per-message stream does not.
onGroupMembershipChange(fn: (ev: GroupMembershipChangeEvent) => void): () => void- Subscribe to group-membership changes across all groups — adds, removes, role transitions, leaves, ownership transfers — with a changeKind to branch on.
onGroupRemoved(fn: (ev: GroupRemovedEvent) => void): () => void- Subscribe to group-removed events when a group the user is in is disbanded.
onGroupJoinRequest(fn: (ev: GroupJoinRequestEvent) => void): () => void- Subscribe to incoming join-request events for groups the user owns or admins, fired when a member nominates someone under the approval gate.
onGroupJoinRequestResolved(fn: (ev: GroupJoinRequestResolvedEvent) => void): () => void- Subscribe to join-request resolution events so the nominator's UI updates when a nomination is approved or rejected.
DmRoom & GroupRoom
The room handle returned by ease.chat.dm and ease.chat.group. Both implement the BaseRoom contract — send, on, off, ready, close — plus capability methods. GroupRoom additionally exposes membership, metadata, approval, and invite-token operations.
send(event: OutgoingEvent): Promise<SendAck>- Encode a MessageEnvelope and send it into the room; for groups the SDK fans the sender-key distribution to the roster.Throws not_connected not_signed_in identity_missing message_too_large socket_closed recipient_identity_changed recipient_no_session recipient_unmessageable member_muted group_not_found
on(eventName: string, handler: (event: IncomingEvent) => void): Subscription- Subscribe to a named room event and return a token to cancel with off(); DM rooms surface message and decryptError, group rooms add memberJoined, memberLeft, and metadataUpdated.
off(subscription: Subscription): void- Cancel a subscription returned by on().
ready(): Promise<void>- Resolve once the underlying socket is open; does not wait for backlog drain.
close(): Promise<void>- Release room-local subscriptions; the shared transport stays open for other rooms.
history(limit: number, before?: string): Promise<HistoryEntry[]>- Page back through locally-logged DM messages oldest-first; an app that never built a persisting snapshot sees an empty log.Throws not_connected
search(query: string, options?: SearchOptions): Promise<SearchResult[]>- Search locally-logged DM messages by substring, with optional sender, since, until, and limit filters.
members(): Promise<Member[]>- GroupRoom: list the current roster with identity, role, and joinedAt.
addMember(userId: string): Promise<Group>- GroupRoom: nominate a contact for membership; under the approval gate a member's nomination throws pending_approval with the request id, otherwise it resolves with the post-add Group.Throws not_connected group_not_found group_full member_already_present member_not_in_contacts nomination_already_pending pending_approval
removeMember(userId: string): Promise<void>- GroupRoom: remove a member (owner or admin only). Alias kick.Throws not_connected not_signed_in group_not_found forbidden member_not_present
leave(): Promise<void>- GroupRoom: self-leave; an owner with remaining members must transferOwnership first or this throws creator_must_transfer_first.
promote(userId: string): Promise<void>- GroupRoom: promote a member to admin (owner-only); a fourth admin throws admin_cap_reached.
demote(userId: string): Promise<void>- GroupRoom: demote an admin back to member (owner-only).
mute(userId: string, until: Date | null): Promise<void>- Owner/admin: mute userId until the given time (null unmutes). A muted member keeps receiving but their sends are rejected with member_muted. The owner is never mutable.Throws not_connected not_signed_in group_not_found forbidden member_not_present cannot_mute_owner
ban(userId: string, reason?: string): Promise<void>- Owner/admin: ban userId — removes them and blocks re-add via a tombstone every join path checks, bumping the group epoch. An optional reason (up to 500 chars) is stored on the ban and the audit event. The owner may ban anyone; an admin may ban plain members only.Throws not_connected not_signed_in group_not_found forbidden cannot_ban_owner cannot_ban_self cannot_ban_admin already_banned reason_too_long
unban(userId: string): Promise<void>- Owner/admin: lift a ban by removing the tombstone. Does not re-add the member — they must rejoin through a normal path.Throws not_connected not_signed_in group_not_found forbidden not_banned
bans(): Promise<GroupBan[]>- Owner/admin: list the group's active bans. Each entry carries the moderator, the timestamp, an optional reason, and the banned user's display fields.Throws not_connected not_signed_in group_not_found forbidden
moderationEvents(opts?: { targetUserId?: string; action?: string; limit?: number; before?: string }): Promise<ModerationEventsPage>- Owner/admin: read the append-only moderation audit log, newest first — every mute, unmute, ban, unban, kick, and role change. Filter by targetUserId and/or action and paginate with limit + before to answer how many times a member has been acted on.Throws not_connected not_signed_in group_not_found forbidden
transferOwnership(newOwnerUserId: string): Promise<void>- GroupRoom: transfer ownership to a current member (owner-only); the caller drops to member.
disband(): Promise<void>- GroupRoom: disband the group entirely (owner-only), fanning group_removed to every member.
rename(name: string): Promise<Group>- GroupRoom: rename the group (owner or admin only).
setAvatar(jpegBytes: Uint8Array): Promise<void>- GroupRoom: set the group avatar (owner or admin only).
getAvatar(): Promise<Uint8Array | null>- GroupRoom: fetch the current group avatar; null if unset.
clearAvatar(): Promise<void>- GroupRoom: clear the group avatar (owner or admin only).
joinRequests(): Promise<GroupJoinRequestRow[]>- GroupRoom: list open join-request nominations awaiting approval (owner or admin only).
pendingJoinRequestCount(): Promise<number>- GroupRoom: count open join-request nominations, for a chat-info badge.
approveJoinRequest(requestId: string): Promise<void>- GroupRoom: approve a pending nomination (owner or admin only), promoting it to a real member.
rejectJoinRequest(requestId: string): Promise<void>- GroupRoom: reject a pending nomination (owner or admin only).
regenerateInviteToken(): Promise<InviteToken>- GroupRoom: rotate the group's invite token (owner-only); the old token stops being accepted.
getInviteToken(): Promise<InviteToken>- GroupRoom: read the current invite token without rotating (owner or admin only).
groupHistory(): Promise<GroupHistoryRow[]>- GroupRoom: fetch interleaved message and system-event history oldest-first, each row tagged with its rowKind.
Core types
The shapes that flow through the surfaces above. Field lists are abbreviated; the full definitions live in sdks/web/src.
Session { userId; email?; phone?; accessToken; accessTokenExpiresAt; refreshToken; refreshTokenExpiresAt }- The signed-in session: user id, the handle signed up with, and the token pair with their expiry timestamps.
MessageEnvelope — type: "ease.text" | "ease.image" | "ease.voice" | "ease.video" | "ease.file" | "ease.location" | "ease.reaction"- The discriminated union a caller hands to room.send; each type carries its own payload (text, blob ref, coordinates, or reaction target).
Contact { userId; name?; username?; email?; phone?; online; lastSeenAt; localName; hasAvatar; ... }- An established contact: identity fields, live presence, an optional private local-name override, and avatar metadata.
Member { identity: Identity; role: MemberRole; joinedAt: Date }- A participant in a room — identity, role (owner / admin / member), and the time they joined.
Group { groupId; name; creatorUserId; members; epoch; createdAt; lastMessageAt; hasAvatar; avatarUpdatedAt }- A group snapshot: identity, the inlined roster, the sender-key epoch, and avatar and last-message metadata.