Skip to main content

Overview

Before your application can ingest memories or retrieve context, you must initialize the Synap SDK. Initialization validates your API key, establishes a secure connection, and prepares local caching. The SDK follows a strict initialize, use, shutdown lifecycle.

Basic Initialization

The simplest way to get started requires only your SYNAP_API_KEY environment variable. Generate an API key from the Synap Dashboard and set it in your environment.
You must call await sdk.initialize() before invoking any SDK operations. Calling methods like sdk.memories.create() before initialization raises an AuthenticationError.

What initialize() Does

When you call initialize(), the SDK performs the following steps in order:
1

Resolve API Key

The SDK picks up the API key from the api_key= argument (if passed) or the SYNAP_API_KEY environment variable.
2

Resolve Identity

The SDK asks Synap who the key belongs to. Your API key is the authoritative identity: this is why you never have to plumb an instance or client ID through your application yourself.
3

Connection Establishment

The SDK opens an authenticated connection to Synap and, if real-time streaming is enabled, an additional streaming channel.
4

Cache Initialization

If a cache_backend is configured (default: sqlite), the SDK sets up the local cache database at the storage path, namespaced to the client the key resolved to.
Identity is resolved here, at initialize(), not when you construct the SDK. Construction only records which API key this SDK will use, and that key is what decides whether you get a new SDK or the existing one for that credential (see Singleton Pattern).

Initialization with Custom Configuration

Pass an SDKConfig object to customize SDK behavior at construction time.
See SDK Configuration for a complete reference of all configuration options. For the full initialize() signature and parameters, see the API Reference.

Singleton Pattern

The SDK keeps one live instance per API key. Construct it twice with the same key (from two modules, or on every request) and the second construction hands you the SDK that already exists instead of opening a second set of connections and caches.
This prevents accidental duplication of connections and caches in applications that construct the SDK from multiple modules.
sdk_a is sdk_b evaluates to False. The two objects share their internal state; they are not literally the same object. Use the SDK, don’t identity-check it.
SYNAP_INSTANCE_ID in the environment is fine. instance_id= in the constructor is not. They behave differently on purpose.The environment variable records which instance you are on and leaves the SDK keyed on your credential, so two keys stay independent. The constructor argument makes the id the identity: a second key used under it is silently discarded, and rotating a key that way has no effect. The dashboard gives you both variables to paste, and your API key already determines which instance you reach either way, so there is nothing to gain from the constructor form.

Running multiple API keys in one process

Construct the SDK with different API keys and you get independent SDKs, each with its own credentials, connections, caches and short-term stores. This is the supported pattern for a backend serving several tenants, and for pointing one SDK at staging and another at production inside a single process.
The per-instance segment of that path arrived in 0.4.3. Up to 0.4.2 the cache was namespaced by client_id alone: the Synap account, not the memory store, so two instances belonging to one account shared the same cache files. Where the same customer_id or user_id appeared under both, one instance could be served the other’s cached context. Server-side scoping was never affected; this was local disk only. Upgrading moves the cache to the new path, so the first request per entity after the upgrade is a miss.
Requires maximem-synap 0.4.1 or newer. In earlier versions the singleton was keyed on the instance ID, which is empty at construction time and only resolved from your API key during initialize(). Every SDK built without an explicit instance_id therefore landed in the same slot, and the second construction silently adopted the first one’s credentials, so the second tenant’s reads and writes were executed against the first tenant’s instance, with no error raised.If your process constructs SDKs for more than one API key, upgrade before relying on this section:
From 0.4.2 this also holds once an SDK knows its own instance. initialize() resolves the instance from your API key, and the SDK answers to that instance ID from then on, so constructing with it returns the same live SDK rather than standing up a second set of connections, caches and streams for one instance.

Two keys, one instance

The one case that gives you duplication rather than reuse is two different API keys issued against the same instance. Each key is its own identity, so each gets its own SDK: two Listen streams, two anticipation caches and two short-term stores, all for one instance. That is deliberate. Merging them would mean one caller transacting on the other’s credential, which would make key rotation, revocation and per-key attribution all silently wrong. The cost is that short-term context recorded through one SDK is not visible to the other until the server round-trips it, and that the pair consumes two of the instance’s concurrent streams. initialize() logs a warning when it detects this, naming the instance, so it does not stay invisible. Unless you specifically want two separately-authenticated SDKs, use one API key per instance per process.

Rotating a key in a long-running process

An SDK keeps the credential it was constructed with for its whole life, so rolling a new key into your secrets manager does not by itself change what a running process authenticates as. What happens next depends on how you construct: If you construct by instance_id, call await sdk.shutdown() before reconstructing (that releases the slot, so the next construction builds a fresh SDK on the new key), or restart the worker. Do it before revoking the old key, or in-flight requests start failing authentication.
Two keys issued against the same instance are one identity only on the instance_id path. Constructed the usual way, with api_key=, each key gets its own SDK authenticating as itself, which is why rotation on that path needs nothing special.

Overriding the Singleton for Testing

In test environments, use _force_new=True to bypass the singleton entirely and build a fresh SDK on every construction, even for a key that already has one.
An SDK built this way is never registered as the singleton for its key. It is therefore invisible to other constructions, and shutting it down leaves any live SDK for the same key untouched, which is what makes it safe to create and discard one per test.
_force_new is intended for tests and for framework adapters that manage SDK lifetime themselves. You do not need it to run several tenants in one process, because different API keys already give you separate SDKs. Reaching for it in application code means opting out of connection and cache reuse, so each extra SDK pays for its own connections, its own streaming channel, and its own cache handles.

Environment Variable Initialization

For CI/CD, containers, serverless, and production, just set the environment variable:
The SDK reads SYNAP_API_KEY automatically and resolves the instance ID from the server.

The configure() Method

If you need to adjust configuration after constructing the SDK but before calling initialize(), use the configure() method.
Calling configure() after initialize() raises InvalidInputError('Cannot reconfigure after initialization'). All configuration must be finalized before the SDK is initialized.

SDK Lifecycle

The SDK follows a strict three-phase lifecycle:
SDK lifecycle: initialize, use, shutdown

1. Initialize

Call await sdk.initialize() to validate the API key and establish connections.

2. Use

Invoke SDK operations: sdk.memories.*, sdk.conversation.context.*, sdk.cache.*, etc. All operations are async and must be awaited.

3. Shutdown

Call await sdk.shutdown() to gracefully tear down the SDK.
Always call shutdown() before your application exits to ensure pending telemetry is flushed and active streaming connections are closed cleanly. Failing to call shutdown() may result in lost telemetry data and lingering connections.

Initialize and Shut Down Cleanly

For cleaner lifecycle management, wrap your application logic in a try/finally so shutdown() always runs, even if an error is raised mid-flight.

Full Example with Error Handling

The following example demonstrates a production-ready initialization pattern with comprehensive error handling.
The API key is read fresh on every start. There is no one-time setup step: the same SYNAP_API_KEY works forever (until you revoke it in the Dashboard).

Next Steps

Ingesting Memories

Send conversations and documents into Synap’s memory system.

Retrieving Memories

Query contextual memories for your AI agent.

SDK Configuration

Explore all configuration options in detail.

JavaScript: namespaced and flat surfaces

The client exposes two call styles against the same instance. Namespaced methods mirror the Python SDK one to one, so the same call shapes work in both languages. They return the raw snake_case response.
Flat methods are the JavaScript-idiomatic surface. They return the normalised camelCase shape.
customer_id depends on your instance. On a B2B instance (user_context_isolation = strict) it is required. On a B2C instance (equals_customer) it is not accepted: the user_id is the whole identity, and a call carrying a customer_id is rejected. The SDK checks this at the call site once initialize() has learned which mode you are on, so the mistake surfaces immediately rather than as a silently empty fetch. See B2C vs B2B.
The two surfaces return different shapes on purpose. user.context.fetch() gives you the raw snake_case response; fetchUserContext() gives you the normalised camelCase one. Pick one style per codebase rather than mixing them.

How it differs from the Python SDK

The two SDKs share one behaviour contract and the same method set, so a Python example translates call for call. Seven things still differ, and every one of them has bitten someone.
wait_for_completion, record_message, stop_listening, batch_create, get_context_for_prompt, create_from_file. All of them keep Python’s spelling so a snippet ports without renaming.memories.waitForCompletion is undefined, not an alias, so the mistake surfaces as is not a function at the call site rather than at import.
Arguments are the opposite: both spellings work everywhere, so { user_id } and { userId } are equally valid.
Python returns a Pydantic model whose facts, preferences, episodes, emotions and temporal_events always exist, empty at worst. The namespaced JavaScript surface returns the raw JSON, where a collection the server omitted is undefined.
Reading context.facts.length on a response with no facts throws.
Python caches to SQLite (cache_backend defaults to "sqlite"), so a restarted process keeps its cache. JavaScript caches in memory.This is a billing difference, not only a latency one: a cache miss is a metered retrieval. Long-lived servers are barely affected. Short-lived processes and serverless functions will see more cloud fetches than the equivalent Python deployment.
Every error carries a stable .code. With a dual ESM/CJS dependency graph a consumer can end up holding two copies of the same error class, and instanceof then fails against the copy it was not built from.
instanceof is made to work across copies as well, but .code is the documented contract and the one to rely on at a package boundary.
storage_path, cache_backend, session_timeout_minutes, log_level and logger exist in ConfigureOptions so a config object can be shared between the two SDKs without a type error. They do nothing here: there is no SQLite backend and no global logger to replace.Timeouts and the retry policy are real, and their defaults match Python exactly: connect 5s, read 30s, write 10s, 3 attempts, backoff base 1s capped at 10s.
Each memory type names its text differently: facts, preferences and temporal events use content, episodes use summary, emotions use context. Concatenating the collections and reading .content across them is wrong in both languages.JavaScript ships a helper that flattens every collection and normalises the text into .memory:
Python has no free-function equivalent. Its format_for_prompt is a method on the cross-scope sdk.fetch() result, not on a scoped fetch.
Every HTTP path (ingestion, retrieval, profiles, credits) runs on Edge, Workers and in the browser. listen() is gRPC, so it needs Node and the two optional peers. Importing the SDK in an Edge route stays safe; only the stream is unavailable there. See Where it runs.