Skip to content

Consent

Consent records what a user wants to hear. A conversation or inbox ID has one of three states. An entity with no record is unknown. The UI locations below are suggested app behavior.

StateMeaningUI location
unknownNobody has decidedRequests
allowedThe user consentedMain inbox
deniedThe user refusedHidden

Consent records are stored locally. When device sync is enabled, device sync copies decisions to the inbox’s other installations. A later consent time wins; at equal times, denied wins over allowed, then unknown.

TriggerState
A peer starts a conversationUsually unknown; an existing DM can inherit its prior state
The user creates a conversationallowed
The user sends a messageallowed
The app sets consent after accepting or subscribingallowed
The app sets consent after blocking or unsubscribingdenied
The user is re-added to a group they leftunknown, unless the group names the own inbox as creator

The user’s creation of the conversation with the contact is considered consent. On the current client, a joined group that names the own inbox as creator is also set to allowed. This check uses the creator field; it does not prove which inbox added the user.

The user’s response to the conversation is considered consent.

Sending a message sets the conversation to allowed from any prior state, including denied. Prevent sending in a denied conversation unless the user explicitly reverses the block.

TaskBrowser, NodeKotlin, Swift
Read group(await group.state()).common.consentStategroup.state().common.consentState with suspend or try await
Read DM(await dm.state()).consentStatedm.state().consentState with suspend or try await
Read an entityclient.preferences.consentState(entity)client.preferences().consentState(entity)
Set conversationconversation.updateConsentState(state)conversation.updateConsentState(state)
Set manyclient.preferences.setConsentStates(records)client.preferences().setConsentStates(records)
Stream changesclient.events(filter)client.events(filter)

For JavaScript, a consent entity is { kind: "conversation", conversationId } or { kind: "inbox", inboxId }. Kotlin uses ConsentEntity.Conversation(id) or ConsentEntity.Inbox(id). Swift uses .conversation(conversationId: id) or .inbox(inboxId: id). A ConsentRecord holds that entity and its state. Swift uses argument labels, such as consentState(entity: entity) and setConsentStates(records: records).

Inbox consent does not automatically set conversation consent. Set the conversation state when the user accepts or blocks a conversation. Consent does not prevent sending. Pass consent filters when listing or reading messages.

The stream reports a ConsentChanged payload with entityKind, entity, and state on Kotlin and Swift. JavaScript wraps this payload in event.consent_changed; its fields are entity_kind, entity, and state.

Batch state changes when possible. A batch commits its final state for each entity in one transaction. Device sync publishes preference changes separately.

const
const stream: EventStream
stream
= await
client: Client
client
.
Client.events(filter: EventFilter): Promise<EventStream>

A stream of the client events that filter selects.

events
({
kinds: EventKind[]
kinds
: ["consent.changed"],
references_own_messages?: boolean | undefined
references_own_messages
: false,
});
const
const receive: Promise<void>
receive
= (async () => {
for await (const
const event: ClientEvent
event
of
const stream: EventStream
stream
) {
if (
const event: ClientEvent
event
.
kind: "conversation.joined" | "conversation.removed" | "conversation.membership_changed" | "conversation.metadata_changed" | "conversation.paused" | "message.received" | "message.status_changed" | "message.deleted" | "message.expired" | "consent.changed" | "hmac_keys.updated" | "identity.registered" | "identity.own_installation_added" | "identity.own_installation_revoked" | "client.rejected_by_server" | "client.lockout_changed" | "conversation.fork_detected" | "notifications.failed" | "archive.restored" | "connection.state_changed" | ... 8 more ... | "lagged"
kind
=== "consent.changed")
handleConsent: (consent: ConsentChange) => void
handleConsent
(
const event: {
readonly kind: "consent.changed";
readonly consent_changed: ConsentChanged;
}
event
);
}
})();

Groups and DMs expose addedByInboxId and creatorInboxId as properties on Browser and Node. Kotlin and Swift expose addedByInboxId() and creatorInboxId() methods. Both values can be absent, for example after restoration. Do not treat a creator field alone as proof that the creator added the user.

Use client events to react to local consent changes. Event listeners do not apply consent filters.