Brainby arc-labs/docs
Sdk pythonIdentity
Python SDK

Identity

Inspect the API key's bound (org, user, agent, namespace) scope via recall.identity.me — caching, rotation, and the no-with_scope rule.

The Recall identity model is identity-by-key: every API key carries a server-side binding to a specific (org, user, agent, namespace) tuple. Client code never constructs that tuple; the server derives it on every request. The identity resource is the only way to inspect that binding from inside the SDK.

me()

GET/v1/auth/meAPI key or JWTstable
def me(self) -> dict[str, Any]: ...

Return the active identity. Cached on the client instance — the first call triggers an HTTP round trip; subsequent calls are free.

Returns

{
  "authMethod": "apiKey",
  "orgId": "org_…",
  "userId": "u_…",
  "namespaceId": "ns_…",
  "agentId": "a_…",
  "agentName": "support-bot",
  "permissions": ["memories.read", "memories.write", ]
}

The exact field set depends on the auth method:

OptionTypeDefault / Env
authMethod = 'apiKey'str

All five binding fields are populated. Permissions reflect the API key's grant.

authMethod = 'jwt'str

agentId may be null if the JWT is admin-plane only. permissions reflects the JWT scope claim.

Examples

me = recall.identity.me()
if me['authMethod'] == 'apiKey':
    print(f"Bound to agent {me['agentId']} ({me['agentName']})")
    print(f"Namespace: {me['namespaceId']}")
me = await recall.identity.me()
if me['authMethod'] == 'apiKey':
    print(f"Bound to agent {me['agentId']} ({me['agentName']})")
    print(f"Namespace: {me['namespaceId']}")

Caching

The result is cached on the resource instance. The cache lives for the lifetime of the Recall/AsyncRecall instance — destroying the client or calling refresh() invalidates it.

This is safe because identity does not change for a given API key: the key itself is what carries the binding, and rotating to a new key means constructing a new client. If you do somehow swap the key on the same client (via a callable api_key= resolver), the cached me() becomes stale — call refresh() to update.

refresh()

GET/v1/auth/meAPI key or JWTstable
def refresh(self) -> dict[str, Any]: ...

Force a fresh GET /v1/auth/me and update the cache. Returns the new identity dict.

me = recall.identity.refresh()
me = await recall.identity.refresh()

When to use

  • After rotating an API key via POST /v1/api-keys/{id}/rotate so the next me() reflects the new key's permissions.
  • In long-running processes where the bound permissions might be edited server-side (admin granted/revoked permission to the agent).
  • In tests, to confirm an injected key is bound correctly.

Why there is no with_scope()

Pre-0.2.0 versions of the SDK accepted a scope= constructor kwarg and exposed a with_scope(...) helper that mutated the active scope. Both are gone, intentionally:

  • Identity-by-key is a design wedge. The server is the source of truth for which user/agent/namespace the API key is bound to. Letting the client override the scope re-introduces the entire class of bugs ("client passes a stale scope, server sees mismatched user") that identity-by-key exists to prevent.
  • Permissions are key-bound. Even if the SDK could pass a different scope, the server's authorization layer still checks against the key's grants — so client-side scope override would silently fail more often than it would succeed.

If you genuinely need to operate across multiple scopes — say, a worker that processes events for many users — the canonical pattern is one client per scope. Construct an API key per agent on the server side, then keep a small pool of Recall instances keyed by agent ID:

from recall import Recall

clients: dict[str, Recall] = {}

def client_for_agent(agent_id: str) -> Recall:
    if agent_id not in clients:
        api_key = secrets_manager.fetch(f'recall-key/{agent_id}')
        clients[agent_id] = Recall(url=URL, api_key=api_key)
    return clients[agent_id]

Each client carries its own connection pool. The pool is small enough (8-16 sockets per httpx.Client) that holding hundreds of clients in memory is fine.

Rotating keys without changing clients

If your access pattern is "one client, key rotates periodically", use a callable api_key=:

import time

class CachingKeyResolver:
    def __init__(self) -> None:
        self.token: str = ''
        self.expires_at: float = 0.0

    def __call__(self) -> str:
        if time.time() > self.expires_at - 30:
            self.token = secrets_manager.fetch('recall-api-key')
            self.expires_at = time.time() + 3600
        return self.token

recall = Recall(url=URL, api_key=CachingKeyResolver())

The transport invokes the callable on every request, so you can rotate under the hood without touching the client. After rotation, call recall.identity.refresh() if downstream code reads cached identity.

Authorization and permissions

The permissions array enumerates the grants attached to this key. Common entries:

OptionTypeDefault / Env
memories.readstr
Search, get, list.
memories.writestr
Write, create, update, delete.
entities.readstr
List, get, get_graph.
entities.writestr
Merge.
namespaces.adminstr
Admin-plane CRUD on namespaces (JWT only).

A 401/403 from any resource method usually means the key is missing the required permission. The RecallAuthError carries code (often PERMISSION_DENIED or INVALID_TOKEN) so you can branch on it.

API key vs JWT

The identity resource works with both auth modes:

  • API keys (rcall_…) carry the full (org, user, agent, namespace) binding and are bearer tokens you can rotate on the server side.
  • JWTs are issued by your IDP, used for admin-plane endpoints (namespaces, jobs, observability). The JWT's claim set is server-validated and may carry a partial scope.

Most application code uses API keys. JWTs are reserved for the control-plane operations described in Namespaces and Jobs.

Was this page helpful?

On this page