createClient & the Recall class
Every option accepted by createClient(), the Recall class shape, and per-request identity via forUser() and withScope().
createClient & the Recall class
The factory comes in four overloads so common cases stay one-liners:
createClient(); // env vars
createClient('https://api.recall.dev'); // url only
createClient('https://api.recall.dev', process.env.RECALL_API_KEY!); // url + key
createClient({ url, apiKey, timeoutMs: 30_000 }); // full optionsAll four resolve to the same internal createRecall(...) factory, which builds an HttpTransport and constructs a Recall from it.
createClient(options)
function createClient(): Recall;
function createClient(url: string): Recall;
function createClient(url: string, apiKey: string): Recall;
function createClient(options: CreateClientOptions): Recall;Options
urlstringdefault: —env: RECALL_BASE_URLBase URL of the Recall server. Must start with http:// or https:// — anything else throws RecallError('INVALID_URL'). Trailing slashes are stripped. Required when the env fallback is also unset, in which case the call throws RecallError('MISSING_CONFIG').
apiKeystring | () => Promise<string>default: —env: RECALL_API_KEYAuthentication. Pass either a static key (rcall_… for live keys, rcall_test_… for test keys, sk_live_… and sk_test_… legacy prefixes also work) or an async resolver for token rotation. The resolver is awaited on every request — cache inside the closure if your token issuer is expensive. When the apiKey is undefined, the SDK does not send an Authorization header at all, which is useful for hitting public endpoints like /v1/health.
apiVersionstringdefault: 2026-04-30API version pin (date-based). Sent as the Recall-Api-Version header on every request. Pinning at the client guards you against silent server-side schema changes — if a future API version changes a response shape, your code stays on the old version until you bump.
timeoutMsnumberdefault: 60000Per-request timeout in milliseconds. Composed with caller-supplied AbortSignal (whichever fires first wins). Streaming endpoints share this timeout — an SSE request that does not produce its first byte within the timeout aborts.
maxRetriesnumberdefault: 3Maximum automatic retries on retriable HTTP statuses (408, 429, 500, 502, 503). Set to 0 to disable retries. Each retry waits according to Retry-After if the server provided one, otherwise exponential back-off with full ±50% jitter starting at retryDelayMs.
retryDelayMsnumberdefault: 500Base delay for the back-off curve. Effective delay on attempt n is retryDelayMs × 2ⁿ × (0.5 + Math.random()). The random factor is centred at 1.0 — earlier versions multiplied by Math.random() alone, which made the back-off shrink instead of grow.
telemetryTelemetryOptionsdefault: —env: RECALL_TELEMETRYOpt-in telemetry. When provided, every request lifecycle event (request_start, request_end, request_retry, error) is delivered to your onEvent callback. The SDK does NOT phone home — events stay inside your process. Setting RECALL_TELEMETRY=1 in the environment switches on a default sink that writes one line of JSON per event to stderr.
scopeClientScopedefault: —Optional client-side cache of (userId, agentId, namespace) — purely a hint. The server still derives the authoritative scope from the API key, so this field exists only so frameworks can avoid an extra GET /v1/auth/me round-trip when they already know what the key implies.
scope does NOT cause the SDK to send a scope on the wire. Recall is identity-by-key — the server reads the bound (user, agent, namespace) from the API key on every request and rejects mismatched body fields. The scope option is a local cache only.
Errors thrown synchronously
createClient() is the rare SDK function that throws synchronously — most others reject. The synchronous throws cover misconfiguration:
MISSING_CONFIGfatalNo url argument was passed and RECALL_BASE_URL is unset.
INVALID_URLfatalurl does not start with http:// or https://.
LIVE_KEY_IN_BROWSERfatalA sk_live_… key was passed in a browser environment (window global present, process absent). Use a server-side proxy or short-lived token instead.
When to use which overload
The four overloads exist to keep the common case short while leaving room for full configuration. Reach for the URL-only or URL+key forms in scripts and tests where you want one line. Reach for the options form once you need timeouts, retries, telemetry, or a custom API version.
The zero-argument form is convenient in serverless functions where RECALL_BASE_URL and RECALL_API_KEY come from the platform's secret manager — you spend zero lines wiring environment variables into the constructor.
The Recall class
createClient() returns an instance of Recall. The class has two responsibilities: it owns the transport and exposes the resource handles. Every public method is documented on its own page in this section.
class Recall {
// data plane
readonly memory: Memory; // get / list / patch — see /sdk-typescript/memories
readonly entity: Entity; // get / search — see /sdk-typescript/entities
// admin plane (requires API key with admin scope)
readonly agents: Agents; // create / list / get / patch / delete / stats
readonly keys: ApiKeys; // create / list / get / delete / rotate
readonly org: Org; // profile / members / plan
// top-level pipeline methods
remember(input, options?): Promise<RememberResult>; // streaming overload available
search(input, options?): Promise<SearchResult>; // streaming overload available
context(input, options?): Promise<BuildContextResult>;
correct(id, correction, options?): Promise<CorrectResult>;
explain(id, options?): Promise<MemoryExplanation>;
forget(input, options?): Promise<ForgetResult>;
health(options?): Promise<HealthResult>;
// per-request user identity
forUser(userId: string): Recall;
// SSR rebind
withScope(opts: WithScopeOptions): Recall;
}You should think of recall.remember(), recall.search(), recall.correct(), recall.explain(), and recall.forget() as the pipeline operations — each one runs through the server's typed pipeline and produces a structured outcome. recall.memory.get / list / patch and recall.entity.get / search are the escape hatches — direct CRUD on the underlying tables, useful for dashboards and admin tools.
recall.agents, recall.keys, and recall.org are the admin plane — operations that manage the identity and tenancy infrastructure rather than memories themselves. They require an API key with admin permission scope (the key's bound user must have admin or owner role in the org). If you call these from a key without admin scope, you receive 403 FORBIDDEN.
recall.context() is the context aggregator — a single endpoint that returns a Markdown prompt section ready to inject into an LLM system prompt, plus a structured per-category breakdown for programmatic use.
forUser() — per-request user identity
recall.forUser(userId) returns a new Recall instance that sends X-Recall-User-ID: userId on every request. One API key serves many end-users; the user being served is specified per-request.
recall.forUser(userId: string): Recall// One key, many users
const recall = createClient({ url, apiKey: process.env.RECALL_API_KEY });
// In your request handler:
const alice = recall.forUser(req.user.id);
await alice.remember({ messages });
await alice.search({ query: 'what does alice prefer?' });The returned client shares the parent's HTTP config (connection pool, timeout, retries) — no new connections are opened. forUser() calls can be chained with withScope().
How it works
forUser() sets the X-Recall-User-ID header on every request. The Recall server reads this header to determine the user scope for data operations. All memories written or read through this sub-client are isolated to userId.
userId format
userId can be any stable identifier for your user — a UUID, email address, database ID, or opaque token. Constraints:
- Max 256 characters
- Allowed characters: letters, digits,
_-@.+
Alternative: set a default at construction time
// All calls from this client serve alice
const aliceRecall = createClient({ url, apiKey, userId: 'alice' });withScope() — per-request rebind (SSR / advanced)
recall.withScope({ apiKey, ...optionalHints }) returns a new Recall instance bound to a different API key. Use this when each user has their own API key (SSR pattern). For the common case where one API key serves many users, use forUser() instead.
The new client inherits url, apiVersion, all of the HTTP knobs, and the telemetry sink — only the key (and optional scope hints) change.
recall.withScope(opts: WithScopeOptions): Recall
interface WithScopeOptions {
apiKey?: string | (() => Promise<string>);
userId?: string;
agentId?: string;
namespace?: string;
}The canonical use case is server-side rendering for a multi-tenant app where each user has their own API key:
// One module-scope client for cheap, key-less calls (health, etc.)
const root = createClient({ url: process.env.RECALL_URL! });
export async function GET(request: Request) {
const session = await getSession(request);
const userClient = root.withScope({ apiKey: session.recallApiKey });
const ctx = await userClient.context({ query: 'continue planning the trip' });
return Response.json(ctx);
}Because the returned instance owns its own HttpTransport (and therefore its own outstanding-request graph), one user's slow tail latency cannot back-pressure another user's requests through a shared connection pool.
The userId, agentId, and namespace fields on WithScopeOptions are hints only — they update the client's local ClientScope cache for inspection. The server still derives the authoritative scope from the supplied API key.
withScope is the only sanctioned way to switch API keys. Mutating the original recall instance's API key in place is not supported — there is no setter, by design.
Custom transports
Both createClient(options) and withScope use the built-in HttpTransport. If you need to inject a custom one — for tests, for a request-recording proxy, or for a future native binding — instantiate Recall directly:
import { Recall, type Transport } from '@arc-labs/recall';
class MockTransport implements Transport {
async request<T>(method: string, path: string): Promise<T> { /* … */ }
async requestStream(): Promise<Response> { /* … */ }
}
const recall = new Recall({
transport: new MockTransport(),
_internal: { /* see source */ },
});For most users this is overkill — see Transport for the shape of the interface and a worked-out example.
Lifecycle
Recall instances are cheap to construct and have no destructor — there is no recall.close(). Connections are owned by the global fetch, which manages keep-alive automatically. You can hold a long-lived module-scope client without worrying about cleanup.
For long-running Node servers, consider a single module-scope client per Recall account, then withScope per request for tenant isolation. For serverless functions, construct lazily inside the handler — Node's lambda warm-pool will keep the same instance alive between invocations on the same container.
Was this page helpful?