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()
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:
authMethod = 'apiKey'strAll five binding fields are populated. Permissions reflect the API key's grant.
authMethod = 'jwt'stragentId 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()
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}/rotateso the nextme()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:
memories.readstrmemories.writestrentities.readstrentities.writestrnamespaces.adminstrA 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?