Node.js & TypeScript SDK
Build server-side memory with typed capture, recall, streaming uploads, cancellation, and durable replay.
1. Install and authenticate
Use Node.js 20 or newer. The published package includes TypeScript declarations and has no runtime npm dependencies. These examples run as server-side ES modules; save executable examples with a .mjs extension. For TypeScript, use your application’s existing compiler configuration.
npm install @contexara-ai/sdk@0.1.0
export CONTEXARA_API_KEY="ctx_live_replace_with_your_key"On PowerShell, set the variable with the following command. Do not use a NEXT_PUBLIC_ environment variable for an API key.
$env:CONTEXARA_API_KEY = "ctx_live_replace_with_your_key"Create an API key in the dashboard at /keys. Enable capture permission for writes and recall permission for searches. Keep the key on your server or in your secret manager; never expose it in browser JavaScript, a public repository, or logs. The key determines the authenticated account. A workspace ID selects an authorized destination; it does not grant access to another account.
2. Capture your first turn
Save as capture.mjs and run node capture.mjs. The constructor reads CONTEXARA_API_KEY. This is a real write; a successful response acknowledges submission, not immediate search availability.
import { Contexara } from "@contexara-ai/sdk";
const ctx = new Contexara();
if (!(await ctx.verify())) throw new Error("Invalid or inactive API key");
const receipt = await ctx.memory.add(
"The deployment region is Mumbai.",
"I will use Mumbai for deployment.",
);
console.log(receipt.eventId, receipt.turnId, receipt.status);verify() returns false for an invalid/inactive key, but throws for network failures. A true result does not confirm capture/recall permissions or quota. AddResult contains eventId, sessionId, turnNumber, turnId, and status. Identity fields are prepared locally; status comes from the server.
3. Recall existing memories
Run after processing, or query memories already available in your account. The result is a typed RetrieveResult. Empty results are valid.
import { Contexara } from "@contexara-ai/sdk";
const ctx = new Contexara();
const result = await ctx.memory.search("What deployment region did we choose?", {
limit: 5,
scope: "general",
});
for (const memory of result.results) {
console.log(memory.memoryId, memory.text, memory.sourceType);
}
if (!result.results.length) console.log("No matching memories yet.");SDK models use camelCase; JSON on the wire uses snake_case. Memory fields include memoryId, text, rrfScore, entities, eventTime, recordTime, sourceType, sourceId, modality, contentHash, pageNumber, slideNumber, mimeType, and speaker. Optional provenance can be null. Entity name, text, and type are all optional. rrfScore is not a confidence percentage; retain server ordering. timingsMs is an extensible numeric map. Successful HTTP bodies are validated at runtime, not only by TypeScript.
Capture parameters and sessions
| Argument | Meaning |
|---|---|
| text | First argument; non-blank user text, maximum 100,000 characters. |
| assistantMessage | Second argument; optional text or null, maximum 100,000 characters. |
| options.sessionId | Stable conversation ID, maximum 512 characters. |
| options.turnNumber | Explicit integer starting at 1, or local allocation if omitted. |
| options.projectId / workspaceId | Authorized destination. |
| options.signal | AbortSignal for the network operation. |
A client creates one default session ID and increments turn numbers locally. For an application handling multiple conversations, pass one stable session ID per conversation. Counters are in memory, not shared between workers or persisted across restarts. For resumed or distributed sessions, allocate turn numbers atomically in your own durable storage and pass them explicitly. Never mix automatic and explicit numbering for the same session in one client. Start a new session when the intended project or workspace changes. Each distinct session consumes one of the client registry’s 100,000 slots; existing sessions continue working at capacity, but new sessions are rejected.
When passing options without an assistant reply, pass null as the second argument.
import { randomUUID } from "node:crypto";
import { Contexara } from "@contexara-ai/sdk";
const ctx = new Contexara();
const sessionId = randomUUID(); // Persist for subsequent turns.
await ctx.memory.add("Compare Rust and Go.", null, { sessionId });
await ctx.memory.add("Choose Rust for the parser.", null, { sessionId });Project and workspace scope
Use general for unassigned memories in the selected workspace, or project with an active project ID. Omit the workspace ID for your personal workspace; pass an authorized workspace ID for a team workspace. A project must belong to that workspace. Do not pass a project ID with general scope. all_active is outside the supported stable contract. Capture and recall destinations are separate: explicitly use the same intended project and workspace in both calls.
Replace this project UUID with an active project you own. The fragment assumes an existing ctx client.
const result = await ctx.memory.search("deployment region", {
scope: "project",
projectId: "11111111-1111-4111-8111-111111111111",
limit: 10,
});
// Add workspaceId for an authorized team workspace.| SearchOptions field | Default / constraint |
|---|---|
| limit | 10; integer 1–50. |
| scope | general or project. |
| projectId | Required for project; omit for general. |
| workspaceId | Omitted means personal workspace. |
| deep / forceFast | false; cannot both be true. |
| signal | Optional AbortSignal. |
Build context for your model
Format an already-fetched result locally to avoid a second retrieval call. The formatter preserves memory IDs and source labels.
import { formatContextBlock } from "@contexara-ai/sdk";
// result is the RetrieveResult returned by memory.search().
const context = formatContextBlock(result, 500);
console.log(context);ctx.contextBlock(query, { tokenBudget: 500 }) performs its own general search. It does not accept project/workspace scope or a signal. For scoped or cancellable retrieval, call memory.search() first, then formatContextBlock(). The default count is a character-based approximation. Supply { tokenCounter: text => tokenizer.encode(text).length } as the third formatter argument for model-specific counting. Empty results produce an empty string. Keep retrieved content separate from trusted model instructions.
Deep Recall
Deep Recall is opt-in and can fall back to Fast Recall when plan or quota conditions do not allow synthesis. A completed Deep Recall run may still have a null synthesis. Check whether Deep Recall ran separately from whether an answer was produced. Its final result count is independently budgeted: limit constrains the initial Fast Recall pass, not the final synthesized result set. Do not enable deep and force-fast together.
import { deepRecallRan } from "@contexara-ai/sdk";
const result = await ctx.memory.search("Why did we choose Mumbai?", { deep: true });
if (deepRecallRan(result) && result.synthesis != null) {
console.log(result.synthesis);
} else {
for (const memory of result.results) console.log(memory.text);
}Durable capture and replay
prepareAdd() is synchronous and does not send a request. toPersistable() serializes the prepared event; fromPersistable() validates and rehydrates it. This single-event file example uses exclusive creation. A multi-worker service should use a transactional outbox and atomically allocate turnNumber in its own durable store.
import { readFile, writeFile } from "node:fs/promises";
import { Contexara, toPersistable, fromPersistable } from "@contexara-ai/sdk";
const ctx = new Contexara();
const path = "pending-capture.json";
let saved;
try {
saved = await readFile(path, "utf8");
} catch (error) {
if (error.code !== "ENOENT") throw error;
const event = ctx.memory.prepareAdd("The deployment region is Mumbai.");
saved = JSON.stringify(toPersistable(event));
await writeFile(path, saved, { flag: "wx" });
}
const receipt = await ctx.memory.send(fromPersistable(JSON.parse(saved)));
console.log(receipt.eventId, receipt.status);
// Record delivery in your outbox after acknowledgement.
// Re-running this example replays the same logical event.Upload documents and images
Create notes.md before running this example. Pass a file path, or a binary buffer with filename and mimeType options. upload() returns an async generator; consume it with for await.
import { Contexara, StreamError } from "@contexara-ai/sdk";
const ctx = new Contexara();
try {
for await (const event of ctx.upload("notes.md", {
caption: "Roadmap notes", explain: false,
})) {
if (event.attachmentId) console.log("Stored:", event.attachmentId);
if (event.chunk) process.stdout.write(event.chunk);
}
} catch (error) {
if (error instanceof StreamError) {
console.error("Partial attachment:", error.attachmentId);
}
throw error;
}UploadOptions includes caption, explain, sessionId, projectId, workspaceId, groupId, filename, mimeType, and signal. workspaceId maps to explicit_workspace_id on the wire. groupId is meaningful for image batches.
| File type | Accepted MIME type | Maximum bytes |
|---|---|---|
| PNG / JPEG / WebP | image/png, image/jpeg, image/webp | 20,000,000 |
| application/pdf | 50,000,000 | |
| DOCX | application/vnd.openxmlformats-officedocument.wordprocessingml.document | 20,000,000 |
| PPTX | application/vnd.openxmlformats-officedocument.presentationml.presentation | 50,000,000 |
| TXT / Markdown | text/plain, text/markdown | 5,000,000 |
Timeouts, retries, and cancellation
Default policy: 5,000 ms timeout, one retry, 200 ms base backoff, 4,000 ms maximum backoff, and no overall deadline. Backoff uses jitter. Verify and Fast Recall retry transient failures within the configured budget; Deep Recall retries at most once because generation may already have incurred cost. Capture retries reuse the unchanged prepared event. Uploads never retry automatically. A usable Retry-After header is honored for 429 responses; a 429 without it is not automatically retried. An overall deadline includes attempts and backoff for ordinary requests; upload has separate connection/read-idle behavior rather than one total stream deadline.
import { Contexara, ResiliencePolicy, CancellationError } from "@contexara-ai/sdk";
const ctx = new Contexara({
resilience: new ResiliencePolicy({
timeoutMs: 5000, maxRetries: 1, deadlineMs: 8000,
}),
});
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
try {
await ctx.memory.search("deployment region", { signal: controller.signal });
} catch (error) {
if (error instanceof CancellationError) console.error("Caller cancelled");
else throw error;
} finally {
clearTimeout(timer);
}Caller cancellation produces CancellationError and is not retried. An SDK timeout produces ContexaraTimeoutError. Cancellation cannot undo a write the server already accepted. Signals apply to verify, add/send, search, and upload network calls; local prepareAdd and formatting do not need them.
Handle failures explicitly
| Condition | SDK exception | What to do |
|---|---|---|
| 401 | AuthenticationError | Replace an invalid or inactive key. verify returns false for this status. |
| 402 | QuotaExceededError | Check account or team usage and allowance. |
| 403 | PermissionDeniedError | Check key scope, capture/recall preferences, and workspace membership. |
| 400 / 422 or local validation | ValidationError | Correct the request; do not retry unchanged. |
| 409 | ConflictError | Investigate turn identity in synchronous capture deployments. |
| 429 | RateLimitError | Inspect retry-after; distinguish rate limits from upload allowance limits. |
| Malformed successful response | ResponseValidationError | Preserve diagnostic details and investigate schema drift. |
| Timeout | ContexaraTimeoutError | Outcome may be uncertain; apply operation-specific replay rules. |
| SSE failure | StreamError | Inspect partial attachment metadata before deciding next steps. |
| Other non-2xx | ContexaraAPIError | Inspect the status and error detail; avoid unlimited retries. |
Network failures use ContexaraConnectionError. All SDK operation exceptions extend ContexaraError. RateLimitError.retryAfterSeconds may be null. Constructor configuration errors should be handled during application startup.
import { Contexara, ContexaraError, RateLimitError } from "@contexara-ai/sdk";
const ctx = new Contexara();
try {
await ctx.memory.search("deployment region");
} catch (error) {
if (error instanceof RateLimitError) {
console.error("Retry-after seconds:", error.retryAfterSeconds);
} else if (error instanceof ContexaraError) {
console.error(error.name, error.message);
}
throw error;
}Test without API calls
The included mock records captures and returns seeded data regardless of query text. It does not perform extraction or semantic search. This TypeScript fixture uses the real Memory interface without any casts.
import { MockContexaraClient, type Memory } from "@contexara-ai/sdk";
const memory: Memory = {
memoryId: "m1", text: "Use Mumbai.", rrfScore: 0.9, entities: [],
eventTime: null, recordTime: null, confidenceBucket: null,
speaker: null, sourceType: "sdk", modality: null, sourceId: null,
contentHash: null, pageNumber: null, slideNumber: null, mimeType: null,
};
const ctx = new MockContexaraClient();
ctx.seed([memory]);
const result = await ctx.memory.search("deployment region");
if (result.results[0].text !== "Use Mumbai.") throw new Error("Unexpected result");
await ctx.memory.add("Use Mumbai.");
if (ctx.recordedCaptures.length !== 1) throw new Error("Capture not recorded");For HTTP-level tests, inject a fetch implementation through new Contexara({ apiKey: "test-key", fetch: mockFetch }). Test cancellation during retries, malformed responses, and upload streams that end without [DONE].
Hosts and supported surface
| Configuration | Default |
|---|---|
| captureBaseUrl / CONTEXARA_CAPTURE_BASE_URL | https://api.contexara.com |
| retrievalBaseUrl / CONTEXARA_RETRIEVAL_BASE_URL | https://retrieval.contexara.com |
| apiKey / CONTEXARA_API_KEY | Required credential. |
| timeoutMs / maxRetries | 5000 / 1; overridden by a resilience policy. |
baseUrl and CONTEXARA_BASE_URL are legacy retrieval aliases. Conflicting explicit baseUrl and retrievalBaseUrl values throw. The Node client uses fetch and has no close() method. CommonJS applications can use const { Contexara } = require("@contexara-ai/sdk").
The 0.1.0 SDK exposes verify, capture, search, context formatting, and uploads. It does not expose list, delete, chat, or stats methods. Public API keys cannot call Loop’s internal chat endpoints. Capture acknowledgements cannot report processing errors that occur later in the asynchronous worker.