Skip to content

Participants & Identity

Participant

Bases: BaseModel

A participant in a room conversation.

Identity

Bases: BaseModel

A resolved user identity.

IdentityResult

Bases: BaseModel

Result of identity resolution.

IdentityHookResult

Bases: BaseModel

Result from an identity resolution hook.

resolved classmethod

resolved(identity)

Resolved - we know who this is.

pending classmethod

pending(display_name=None, candidates=None)

Pending - create participant with status=pending. Advisor resolves later.

challenge classmethod

challenge(inject, message=None)

Challenge - hold the message, ask the sender to self-identify.

reject classmethod

reject(reason='Unknown sender')

Reject - do not create room or participant.

IdentityResolver

Bases: ABC

Resolves user identity from inbound messages.

resolve abstractmethod async

resolve(message, context)

Resolve the identity of an inbound message sender.

Parameters:

Name Type Description Default
message InboundMessage

The inbound message with sender information.

required
context RoomContext

Current room context (room, bindings, participants).

required

Returns:

Type Description
IdentityResult

An identity result indicating identified, ambiguous, pending,

IdentityResult

unknown, or rejected status.

MockIdentityResolver

MockIdentityResolver(mapping=None, ambiguous=None, unknown_status=UNKNOWN)

Bases: IdentityResolver

Resolves identity from a pre-configured mapping.

Supports three resolution outcomes: - Identified: sender_id found in mapping → single identity. - Ambiguous: sender_id found in ambiguous → multiple candidates. - Unknown/Pending: no match → status controlled by unknown_status.

Verification

Verification

Bases: BaseModel

An identity's verification, valid on every channel its addresses reach (§11.7.1).

method class-attribute instance-attribute

method = None

The integrator's name for how it was proven ("link", "code", "sso").

verified_via class-attribute instance-attribute

verified_via = None

The channel the completing request was opened on.

ended_reason class-attribute instance-attribute

ended_reason = None

Why it ended: "expired", "revoked", "failed".

request_id class-attribute instance-attribute

request_id = None

The request open for the identity, if any.

completed_request_id class-attribute instance-attribute

completed_request_id = None

The request that completed the verification: its end is announced in that request's room, on its channel.

VerificationRequest

Bases: BaseModel

A request asking an identified sender to prove who they are (§11.7.2).

The secret is never stored: only its hash, salted with the request's id.

window_start instance-attribute

window_start

When the message that opened it was received: the start of the window whose held messages are released once verified (§11.7.6).

VerificationPolicy

Bases: BaseModel

How long a verification and its requests last, and how many attempts (§11.7).

ttl class-attribute instance-attribute

ttl = Field(default=900.0, gt=0)

Seconds a completed verification stays in force.

request_ttl class-attribute instance-attribute

request_ttl = Field(default=600.0, gt=0)

Seconds a request may be completed in.

code_request_ttl class-attribute instance-attribute

code_request_ttl = Field(default=300.0, gt=0)

Seconds a code request may be completed in: a code is short (§11.7.7).

max_attempts class-attribute instance-attribute

max_attempts = Field(default=3, ge=1)

Wrong secrets a request accepts before it fails.

code_length class-attribute instance-attribute

code_length = Field(default=6, ge=6, le=12)

Digits of a code sent for the person to reply with.

release_on_verify class-attribute instance-attribute

release_on_verify = True

Whether the messages held during a request are released to the room's agents when it completes (RFC §11.7.6).

release_max_messages class-attribute instance-attribute

release_max_messages = Field(default=20, ge=1)

At most this many held messages are released, the latest ones.

release_max_age class-attribute instance-attribute

release_max_age = Field(default=3600.0, gt=0)

Held messages older than this many seconds are not released.

hold_unverified class-attribute instance-attribute

hold_unverified = True

Whether an unverified sender's message is held from the room's agents unless an ON_VERIFICATION_REQUIRED hook passes it (RFC §11.7.3); False only labels it.

VerificationRefusedError

VerificationRefusedError(reason)

Bases: RoomKitError

A verification request could not be completed (RFC §11.7.2).

reason names why: not_found (no such request in the caller's organization), not_open (completed, failed, expired or cancelled already), expired (its time ran out now), mismatch (a wrong secret, counted as an attempt) or failed (the wrong secret that used up its attempts).

Identity Hooks

Identity hooks (ON_IDENTITY_AMBIGUOUS, ON_IDENTITY_UNKNOWN) allow custom handling when the identity resolver returns ambiguous or unknown results.

Basic Usage

from roomkit import RoomKit, HookTrigger
from roomkit.models.identity import IdentityHookResult

kit = RoomKit(identity_resolver=my_resolver)

@kit.identity_hook(HookTrigger.ON_IDENTITY_AMBIGUOUS)
async def resolve_ambiguous(event, ctx, id_result):
    # Custom logic to resolve ambiguous identity
    if len(id_result.candidates) == 1:
        return IdentityHookResult.resolved(id_result.candidates[0])
    return IdentityHookResult.pending(candidates=id_result.candidates)

Filtering Identity Hooks

Identity hooks support filtering by channel_types, channel_ids, and directions, just like regular hooks. This allows you to target identity resolution logic to specific channels.

from roomkit import ChannelType, HookTrigger
from roomkit.models.enums import ChannelDirection

# Only run for SMS inbound messages
@kit.identity_hook(
    HookTrigger.ON_IDENTITY_AMBIGUOUS,
    channel_types={ChannelType.SMS},
    directions={ChannelDirection.INBOUND},
)
async def resolve_sms_identity(event, ctx, id_result):
    # Only called for SMS inbound events
    return IdentityHookResult.pending(
        display_name=ctx.room.metadata.get("phone_number"),
        candidates=id_result.candidates,
    )

# Only run for specific channel IDs
@kit.identity_hook(
    HookTrigger.ON_IDENTITY_UNKNOWN,
    channel_ids={"sms-support", "sms-sales"},
)
async def handle_unknown_support(event, ctx, id_result):
    # Only called for support/sales SMS channels
    return IdentityHookResult.reject("Unknown sender not allowed")

Filter Parameters

Parameter Type Description
channel_types set[ChannelType] \| None Only run for events from these channel types (None = all)
channel_ids set[str] \| None Only run for events from these channel IDs (None = all)
directions set[ChannelDirection] \| None Only run for events with these directions (None = all)

All specified filters must match for the hook to be invoked. If a filter is None, it matches all values.

Use Cases

SMS-only identity resolution:

@kit.identity_hook(
    HookTrigger.ON_IDENTITY_AMBIGUOUS,
    channel_types={ChannelType.SMS, ChannelType.EMAIL},
    directions={ChannelDirection.INBOUND},
)
async def resolve_transport_identity(event, ctx, id_result):
    # Identity resolution for transport channels only
    # WebSocket/AI channels are already authenticated
    ...

Channel-specific rejection:

@kit.identity_hook(
    HookTrigger.ON_IDENTITY_UNKNOWN,
    channel_ids={"sms-premium"},
)
async def reject_unknown_premium(event, ctx, id_result):
    # Premium channel requires known identity
    return IdentityHookResult.reject("Unknown senders not allowed on premium channel")