Imperal Docs
SDK Reference

Gateway namespaces: ctx.conversations / users / apps / rbac

The four platform-data namespaces on ctx — conversations, users, apps and rbac: every method, what it returns, how scoping works, and the errors it raises.

Four namespaces on ctx reach platform data — the records the platform itself owns, rather than your own extension's storage:

Prop

Type

They exist because these calls were previously hand-rolled. Four extensions were assembling user routes themselves, each with its own spelling; admin alone had nineteen distinct call sites for ctx.users and fourteen for ctx.rbac, including three different spellings of the same scope-listing query. One renamed query parameter and they break in four different ways on four different days. A namespace states each route once.

Do not hand-roll these calls. If you find yourself building a URL, a service token or an X-Acting-User header inside an extension, you are re-implementing something the SDK already owns — and you will drift from it. There is no supported way for an extension to talk to these routes directly.

What every namespace shares

All four are built on the same client, so four things are true of every method below.

Scoping: whose data you get

Each namespace is bound to the acting user — the person whose turn is running. You never pass a user id to say "me"; it is already known, and for ctx.conversations there is no user parameter at all, so there is no shape of request that could reach another person's history.

To act on behalf of somebody else — legitimately, in an administrative tool — re-scope explicitly with for_user():

# Every namespace has it, and it returns the same concrete type,
# so chained calls keep working.
other = ctx.users.for_user("imp_u_OTHER")
record = await other.get("imp_u_OTHER")

other.user_id                        # -> the id this client acts for
ctx.users.for_user("")               # -> ValueError: needs a non-empty user_id

for_user() re-scopes the caller identity; it does not grant permission. The platform still decides whether that call is allowed, and says no if it is not.

All four namespaces carry it with identical semantics — conversations.for_user(id), users.for_user(id), apps.for_user(id) and rbac.for_user(id) each return a re-scoped copy of their own concrete client, so a chained call keeps its real type and its own methods.

Authority is the platform's answer, not a local guess

Most ctx.users, ctx.apps and ctx.rbac routes are administrative: they act on another user or on somebody's app, and the platform enforces who may do that. These clients deliberately do not pre-check permissions locally — a client that guessed would be both wrong and unsafe. The call goes through and the platform's answer stands.

Errors are typed

Every method raises rather than returning an error object, so a failure cannot be silently ignored:

Prop

Type

from imperal_sdk.errors import APIError, AuthError, NotFoundError

try:
    data = await ctx.conversations.messages(cid, limit=40)
except NotFoundError:
    return ActionResult.error("That conversation no longer exists.")
except AuthError:
    return ActionResult.error("That conversation belongs to someone else.")
except APIError as e:
    log.warning("conversations.messages failed: %s", e)
    return ActionResult.error("The conversation store is unavailable.")

Never put a raw APIError into user-visible text: its message can contain internal platform addresses. Log the exception, show the user a sentence you wrote.

Return shape

Every method returns the platform's own dict payload, unwrapped — no SDK envelope in the way. The relevant keys are named per method below.


ctx.conversations

The acting user's own conversation threads with Webbee — the data behind the Thoughts room. Panel, Telegram and terminal all read the same history, and every message carries the surface it was said on.

Owner-scoped by construction. The routes behind this namespace accept no user parameter at all, so there is no shape of request that could reach another person's history. That is not a check you must remember to write; it is the absence of a way to ask.

At any moment exactly one thread is live — the one every surface is reading right now. list() reports it as active_id; compare against that rather than guessing which row is current.

Prop

Type

@chat.function("recall", action_type="read", description="Find what we discussed before.")
async def fn_recall(ctx, params) -> ActionResult:
    listing = await ctx.conversations.list(limit=30)
    live = listing.get("active_id", "")

    for thread in listing.get("conversations", []):
        if params.topic.lower() in (thread.get("title") or "").lower():
            transcript = await ctx.conversations.messages(thread["id"], limit=40)
            said = [m["text"] for m in transcript.get("messages", [])]
            return ActionResult.success(
                data={"thread": thread["title"], "is_live": thread["id"] == live,
                      "messages": said})

    return ActionResult.success(data=[], summary="Nothing on that topic yet.")

update() is where the naming rule lives. Set a title through it and the thread is considered human-named permanently — the automatic namer will not overwrite it. You do not need to pass a title_generated flag yourself; the client does it.


ctx.users

Reading and editing a platform user: the record itself, the per-user settings blob, which surfaces they have connected, and which apps they can reach.

Anything about money lives in ctx.billing. Anything about roles and permissions lives in ctx.rbac. Keeping those apart is deliberate — a namespace that did all three would be the place every unrelated change lands.

Authority, not convenience. Most of these routes are administrative: they act on another user, and the platform enforces who may do that. Your extension needs the standing to make the call — the SDK will not grant it, and will not pre-check it either.

The record

Prop

Type

Settings, surfaces and reach

Prop

Type

# Only notify on a surface the user actually has.
surfaces = await ctx.users.surfaces(target_id)
if "telegram" in surfaces.get("connected", []):
    await ctx.notify.send(target_id, "Your export is ready.")

# Settings are merged, so this cannot clobber another app's keys.
await ctx.users.update_settings(target_id, {"digest_hour": 9})

ctx.apps

An app as a platform object: its settings blob, who can reach it, and the moderation lifecycle that decides whether it is visible in the marketplace at all.

Calling into another app is a different thing and stays where it is — that is ctx.extensions.call. This namespace is about the app record, not the app's behaviour.

Prop

Type

Moderation

These four have real consequences — a developer's release becomes visible, or does not:

Prop

Type

approve() and reject() are separate methods rather than set_status("approved") on purpose: a reader of the call site should see which one happened without having to know the status vocabulary — and rejecting takes a reason that approving does not.

for app in (await ctx.apps.list_pending()).get("apps", []):
    if app.get("validation_score", 0) < 18:
        await ctx.apps.reject(app["app_id"], "Validation below the 18/21 bar.")
    else:
        await ctx.apps.approve(app["app_id"])

ctx.rbac

Roles, scopes, and the question that actually matters: what may this user do?

Why roles and scopes share one namespace

They are one question asked from two ends. A scope is a permission; a role is a bundle of them; and the answer you usually want — effective_scopes(user) — needs both. Splitting roles and scopes into two separate namespaces would put the interesting call in neither.

Prop

Type

cascade is explicit for a reason. Editing a role can rewrite permissions for everyone who holds it — a real blast radius. So it is a named argument you must pass deliberately, never a default that quietly does the larger thing. Read it as: "yes, apply this to every existing holder too."

scopes = (await ctx.rbac.effective_scopes(user_id)).get("scopes", [])
if "billing:write" not in scopes:
    return ActionResult.error("You do not have permission to change billing.")

Which namespace holds what

Prop

Type

Available since SDK 5.12.0. See the changelog for the release notes, and ctx in the API surface for every other namespace on the context object.

On this page